标准库容器很小,并未给每个容器添加大量功能,而是提供了一组算法,这些算法大多数都独立于任何特定的容器,这些算法是通用的,或者说是泛型的(generic),可用于不同类型容器和元素。
大多数泛型算法定义在头文件algorithm中,头文件numberic中也定义了一些。
一般,这些算法不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。
find:
int val = 42;
auto result = find(vec.cbegin(), vec.cend(), val); //如找到,返回指向它的迭代器,如没找到,返回vec.cend()
find操作的是迭代器,但数组上的指针也可以。
find工作步骤:
1.访问序列首元素。
2.比较。
3.匹配时返回标识此元素的值。
4.否则,找下一个元素,重复2、3。
5.如到达序列尾,find停止并返回一个指出元素未找到的值,此值的类型必须和步骤3的返回值类型具有相容类型。
以上步骤不依赖于容器保存的元素类型,因此,只要有一个迭代器可用来访问元素,find就完全不依赖于容器类型(甚至无须理会保存元素的是不是容器)。
迭代器令算法不依赖于容器(解引用访问元素、返回指向元素的迭代器、递增迭代器移动到下一个元素、尾后迭代器用来判断find是否到达序列末尾),但算法依赖于元素类型的操作(find用==完成每个元素与给定值的比较,其它算法可能要求元素类型支持<运算符,但大多数算法提供了一个方法,允许我们使用自定义的操作来代替默认的运算符)。
将int类型的值输入到向量中,之后使用count函数计算给定值出现的次数:
#include
#include
#include
using namespace std;
int func(int toBeFund) {
vector<int> ivec;
int i = 0;
while (cin >> i) {
ivec.push_back(i);
}
return count(ivec.begin(), ivec.end(), toBeFund);
}
int main() {
cout << func(5) << endl;
}
泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器操作,这带来了一些特性:算法永远不会改变底层容器的大小,但可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加(push_back等)或删除(erase等)元素。标准库还定义了一类特殊的迭代器,称为插入器,当给这类迭代器赋值时,它们会在底层的容器上执行插入操作,因此,当算法操作这样一个迭代器时,可以完成向容器添加元素的功能,但算法自身不会做这样的操作。
除少数例外,标准库算法都对一个范围内的元素进行操作,此元素范围称为输入范围,接受输入范围的算法总是使用前两个参数来表示此范围,两参数分别是指向要处理的第一个元素和尾元素之后位置的迭代器。
一些算法只会读取其输入范围内的元素,而从不改变元素,如find、count,称为只读算法。
accumulate也是一个只读算法,它定义在头文件numeric中,它的作用是将输入范围内的数字求和,第三个参数是和的初始值:
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
accumulate的第三个参数决定了函数中使用哪个类型上的加法运算法以及返回值的类型。
accumulate将第三个参数作为求和的起点,这意味着元素类型加到和的起点上的操作必须是可行的。
accumulate用于string时:
string sum1 = accumulate(v.cbegin(), v.cend(), string("")); //正确
string sum2 = accumulate(v.cbegin(), v.cend(), ""); //错误,const char *类型上没有定义+操作,造成编译错误
对于只读取而不改变元素的算法,最好使用cbegin和cend,但如果计划使用算法返回的迭代器来改变元素的值,就需要使用begin和end。
equal也是只读算法,它将第一个序列中的每个元素与第二个序列中的对应元素进行比较,如所有对应元素都相等,返回true,否则返回false。此算法接收三个参数,前两个与以往一样表示一个输入范围,第三个参数表示第二个序列的首元素的迭代器:
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
equal函数比较的两个序列中的元素类型可以不同,只要两者能用==比较大小,如string和const char *。
equal有一个重要假设,第二个序列中元素数至少与输入范围中的元素数一样长。
vector<double> ivec = { 0, 1.3, 2.5, 3, 4, 5 };
cout << accumulate(ivec.begin(), ivec.end(), 0) << endl; //输出15
cout << accumulate(ivec.begin(), ivec.end(), 0.0) << endl; //输出15.8
vector<const char *> roster1 = { "aaa", "bbb", "ccc" };
vector<const char *> roster2 = { "aaa", "bbb", "ccc" };
cout << equal(roster1.begin(), roster2.end(), roster2.begin()) << endl; //输出0,因为比较的是两个指针,只有当指针值相等时才会为true
const char* c1 = "aaa";
const char* c2 = "aaa";
cout << (c1 == c2) << endl; //输出1,因为内存中只有一份"aaa",两指针指向同一位置
一些算法将新值赋予序列中的元素,在使用这类算法时,必须保证序列原大小至少不小于我们要写入的大小,因为算法不会执行容器操作,因此它们不能改变容器大小。
如fill接受一对迭代器表示输入范围,还接受第三个参数,将第三个参数填充到输入范围内:
fill(vec.begin(), vec.end(), 0); //将每个元素置为0
一些算法从两个序列中读取元素,构成这两个序列的元素可以来自不同类型的容器,而且两个序列中元素的类型也不要求严格匹配,算法要求的只是能够比较两个序列中的元素。
传递第二个序列有两种方式,第一种是接受一个迭代器参数表示第二个序列中的首元素;第二种是接受两个迭代器表示第二个序列的范围。第一种时,假定第二个序列至少和第一个一样长,确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。
fill_n接受一个单迭代器、一个计数值和一个值:
vector<int> vec;
fill_n(vec.begin(), vec.size(), 0); //将所有元素置为0
fill_n假定写入指定个元素是安全的,即假定dest指向一个元素,而从dest开始的序列至少包含n个元素。超出部分写入的结果是未定义的。
一种能保证算法有足够元素空间来容纳输入的方法是使用插入迭代器,我们通过插入迭代器赋值时,一个与赋值号右侧值相等的元素被添加到容器中。
函数back_inserter定义在头文件iterator中,它接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:
vector<int> vec;
back_insert_iterator<vector<int>> it = back_inserter(vec);
*it = 42; //将42放到vec的尾部
vector<int> vec;
fill_n(back_inserter(vec), 10, 0); //正确
拷贝算法(copy)接受三个迭代器,前两个表示输入范围,第三个表示目的序列的起始位置。此算法将输入范围内的元素拷贝到目的序列中,目的序列至少要包含与输入范围一样多的元素:
int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1) / sizeof(*a1)]; //a2大小与a1相同,sizeof的返回结果是常量表达式
auto ret = copy(begin(a1), end(a1), a2); //把a1内容拷贝给a2,返回值为a2的尾后元素的指针
replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值:
relpace(ilst.begin(), ilst.end(), 0, 42); //将序列中所有0改为42
如希望保留原序列不变,可调用replace_copy,此算法第三个位置接受的参数为第三个迭代器:
replace_copy(ilst_cbegin(), ilst_cend(), back_inserter(ivec), 0, 42); //ilst未改变,ivec包含替换后的ilst副本,其中所有0改为42
vector<int> vec;
vec.reserve(10);
fill_n(vec.begin(), 10, 0); //错误
cout << vec.size() << endl; //还是0
算法sort会重排容器中元素的顺序,它是利用元素的<运算符实现的。
为消除容器中重复的单词,可先排序vector,使得重复的单词都相邻出现,再使用函数unique,使得所有相邻的重复项消除只剩一个,unique返回值为最后一个不重复的值的后一个位置:
void elimDups(vector<string> &words) {
sort(words.begin(), words.end());
auto end_unique = unique(words.begin(), words.end()); //返回指向不重复区域之后一个位置的迭代器
words.erase(end_unique, words.end());
}
unique并不真正删除重复元素,只是将不重复元素调整到序列开始部分,不重复元素区域后的元素仍存在。
很多算法允许我们提供自己定义的操作来代替默认运算符,如sort默认使用元素类型的<运算符,但我们希望的排序顺序与<所定义的顺序不同,或序列中元素的类型没有定义<操作,这两种情况下,需要重载sort的默认行为。
我们可以使用接受第三个参数的重载版的sort,第三个参数是一个谓词,谓词是一个可调用表达式,返回结果是一个能用作条件的值。标准库算法使用的谓词分两类,一是一元谓词,即接受单一参数;二是二元谓词,它有两个参数。接受谓词参数的算法对输入序列中的元素调用谓词,因此,元素类型必须能转化为谓词的参数类型。
bool isShorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
sort(words.begin(), words.end(), isShorter); //由短到长排序words,即谓词为true的在前面(原版本时使用<,也是<返回true时在前面)
将words按长度重排的同时,还希望具有相同长度的元素按字典序排列,可以使用stable_sort函数,它可以在排序后维持那些值相等的元素的顺序(即稳定排序):
elimDups(words); //删除重复元素并将元素按字典序排列
stable_sort(words.begin(), words.end(), isShorter); //按长度排序,但同一长度的元素的先后位置不变,即同一长度还是按原顺序(字典顺序)排
partition算法接受一个谓词,将容器内容划分,谓词为true的值会排在容器前半部分,返回值为指向最后一个使谓词为true的元素之后的位置。
bool isShorterThan5(const string& s) {
return s.size() < 5;
}
void FindLongerThan5(vector<string>& words) {
auto it = partition(words.begin(), words.end(), isShorterThan5);
for (; it < words.end(); ++it) {
cout << *it << endl;
}
}
根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数,但有时我们希望传更多的参数,如上例中,将5硬编码到划分序列的谓词中,如果5能由我们输入就更有实际价值。
我们可以向一个算法传递任何类别的可调用对象,对于一个对象或一个表达式,如果可以对其使用调用运算符(),则它称为可调用的。即,如果e是一个可调用表达式,我么可以编写代码e(args),args是一个以逗号分隔的一个或多个参数的列表。
目前我们了解的可调用对象只有函数和函数指针。
C++11新标准的lambda表达式也是一个可调用的代码单元,可以将其理解为一个未命名的内联函数。一个lambda具有一个返回类型、一个参数列表和一个函数体,但与函数不同,lambda可能定义在函数内部:
[capture list](parameter list) -> return type {function body}
其中,capture list是一个lambda所在函数中定义的局部变量列表(通常为空),lambda必须使用尾置返回。
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:
auto f = [] { return 42; }; //忽略参数列表和括号,等价于指定一个空参数列表;忽略了返回类型,由return语句推断出为int
上例中,我们定义了一个可调用对象f,它不接受参数,返回42。
调用lambda表达式:
cout << f() << endl; //打印42
当lambda表达式的函数体包含着return语句之外的语句且没有指定返回类型时,返回void。(但我测试时并没有返回void,返回类型是return的内容的类型,应该是编译器优化)
lambda不能有默认实参,一个lambda调用的实参数永远与形参数相同。
[] (const string &a, const string &b) {
return a.size() < b.size();
}
上例中,空捕获列表表明此lambda不使用它所在函数中的任何局部变量。使用它:
//当stable_sort需要比较两个元素时,它就会调用给定的这个lambda表达式
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); }
函数不能定义在函数内。函数外只能定义全局变量或者对象 ,而不能执行语句及调用函数。
我们可以使用标准库find_if算法来查找第一个具有特定大小的元素,它接受一对迭代器作为输入范围,第三个参数是一个谓词,find_if算法对输入序列中的每个元素调用给定的谓词,它返回第一个使谓词返回非0(即true)的元素的迭代器,如果不存在这样的元素,返回尾后迭代器。
打印一个string序列中长度大于等于sz的所有串:
void biggies(vector<string> &words, vector<string>::size_type sz) {
elimDups(words); //将words按字典排序并删除重复单词
stable_sort(words.begin(), words.end(), isShorter); //按长度排序,长度相同的维持字典序(稳定排序)
auto wc = find_if(words.begin(), words.end(), [sz](const string &s){ return a.size() >= sz; }); //lambda捕获了sz,wc为第一个长度为sz的元素的迭代器
auto count = words.end() - wc; //计算满足长度大于等于sz的字符串个数
cout << count << " " << make_plural(count, "word", "s") << " of length " << sz << " or longer" << endl; //make_plural为自定义函数,当count>1时,返回将s加到word后的串,当count=1时,返回word
for_each(wc, words.end(), [](const string &s){ cout << s << " "; }); //此泛型算法对于迭代器范围内的每个元素执行第三个参数
cout << endl;
}
上例中find_if只能接受一元谓词,因此,如果没有lambda表达式捕获的sz,只能将长度硬编码到函数中。for_each算法中使用了cout,是因为lambda可以直接使用当前函数内的static变量和当前函数之外的名字。
只有当lambda在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
#include
#include
#include
#include
using namespace std;
void func(vector<string>& svec, vector<string>::size_type sz) {
auto it = partition(svec.begin(), svec.end(), [sz](const string& s) { return s.size() < sz; });
for_each(it, svec.end(), [](const string& s) { cout << s << " "; }); //打印长度大于等于sz的元素
cout << endl;
}
int main() {
vector<string> svec = { "aaa", "bbbb", "c", "dd" };
func(svec, 2);
}
stable_partition与partition作用类似,但它是稳定的分区函数。
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。可以理解为当我们向函数传递lambda时,我们定义了一个新的类类型和这个新类类型的一个对象,函数中的参数(如find中第三个参数位置定义的lambda表达式)就可以代表这个新类类型的一个对象,类似地,使用auto定义被lambda初始化的变量时,我们定义了一个从lambda生成的新类类型的对象。
默认情况下,从lambda生成的类都包含一个对应lambda所捕获的变量的数据成员,类似于普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
类似于参数传递,变量的捕获方式也分为值或引用:
采用值捕获的前提是变量可以拷贝,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝:
size_t v1 = 42;
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); //j为42,f保存的是我们创建它时v1的拷贝
引用捕获:
size_t v1 = 42;
auto f = [&v1] { return v1; };
v1 = 0;
auto j = f(); //j为0,f保存的是v1的引用
引用捕获必须保证被引用的对象在lambda执行时是存在的,如,lambda捕获一个局部变量,而出了该局部变量的作用域时,它就不复存在了。由于函数可以返回一个可调用对象,如函数返回一个lambda,则lambda不能包含引用捕获,因为函数不能返回一个局部变量的引用或指针。
当要捕获的对象不能拷贝时,只能使用引用捕获。
建议避免捕获指针或引用,它们绑定的对象可能会被销毁。
隐式捕获:
wc = find_if(words.begin(), words.end(), [=] (const string &s) { return s.size() >= sz; };
上例中的sz是隐式捕获的,捕获方式为值捕获。
混用隐式和显式捕获方式时,捕获列表中第一个元素必须是一个&或=,此符号指定了默认捕获方式为引用捕获或值捕获,并且显式捕获的变量必须使用与隐式捕获不同的方式,如隐式捕获是引用方式,则显式捕获的变量必须采用值方式,因此不能在其名字前面使用&:
//os为隐式引用捕获,c为显式值捕获,c前面不能加&
for_each(words.begin(), words.end(), [&,c] (const string &s) { os << s << c; };
//os为显式引用捕获,c为隐式值捕获,os前必须加&
for_each(words.begin(), words.end(), [=,&os] (const string &s) { os << s << c; };
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值,如果我们希望能改变一个被捕获的变量的值,必须在参数列表尾加上关键字mutable,因此,可变lambda不能省略参数列表:
size_t v1 = 42;
auto f = [v1] () mutable ->size_t { return ++v1; }; //如没有mutable关键字,++v1会编译报错,不能更改
v1 = 0;
auto j = f(); //j为43
而对于引用捕获的变量是否可以修改依赖于此引用指向的是一个const类型还是一个非const类型:
size_t v1 = 42;
auto f = [&v1] () { return ++v1; };
v1 = 0;
auto j = f(); //j为1
默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。被推断返回void的lambda不能返回值。
使用transform算法和一个lambda来将一个序列中每个负数替换为其绝对值:
transform(vi.begin(), vi.end(), vi.begin(), [](int i) { return i < 0 ? -i : i; });
函数transform接受三个迭代器和一个可调用对象,前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写入目的位置。上例中的lambda体是单一的return语句,返回一个表达式结果,我们无需指定返回类型,因为可以根据条件运算符的类型推断出来,但如果将程序改为:
transform(vi.begin(), vi.end(), vi.begin(), [](int i) { if (i < 0) return -i; else return i; });
上例会产生编译错误,编译器推断这个版本的lambda返回类型为void,但它返回了一个int值。(但我测试时,能通过编译并完成功能,应该是编译器优化)因此给上例的lambda定义返回类型,lambda的返回类型必须为尾置返回类型:
transform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int { if (i < 0) return -i; else return i; });
count_if函数接受一对迭代器,表示一个输入范围,还接受一个谓词,会对输入范围中的每个函数执行。count_if返回一个计数值,表示谓词多少次为true,使用它统计序列中单词长度超过6的个数:
vector<string> svec = { "bad", "good", "computer", "keyboard", "screen" };
auto i = count_if(svec.begin(), svec.end(), [](const string& s) { if (s.size() > 6) return true; else return false; }); //自动推断的i的类型为ptrdiff_t
cout << i << endl; //输出2
lambda捕获局部int变量,并递减变量值,直到为0,一旦变量变为0,再调用lambda应该返回true表示捕获的变量是0:
int i = 5;
auto f = [&i]() ->bool { if (i > 0) { --i; cout << i << endl; return false; } else return true; };
while (!f());
如果是那种只在一两个地方使用的简单操作,lambda表达式是最有用的,如果需要在很多地方使用相同操作,最好用函数。
如果lambda表达式的捕获列表为空,通常可以用函数代替。但是对于捕获局部变量的lambda,用函数来替换它就不容易了,如之前在find_if中调用lambda比较一个string的长度和一个给定大小,我们可以很容易地编写一个接受两个参数的函数:
bool check_size(const string &s, string::size_type sz) {
return s.size() >= sz;
}
但不能用这个函数作为find_if的参数,因为find_if接受一个一元谓词。
我们可以解决这个问题,通过使用一个新的名为bind的标准库函数,这是C++11新标准,它定义在头文件functional中,可以将bind函数看做一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适配原对象的参数列表:
auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable参数,即当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置,即_1表示newCallable的第一个参数,以此类推。
auto check6 = bind(check_size, _1, 6);
上例中,bind调用只有一个占位符,表示check6只接受单一参数,占位符出现在arg_list的第一个位置,表示check6的第一个位置的参数对应check_size的第一个参数,此参数是一个const string &。因此,调用check6必须传递给它一个string参数:
string s = "hello";
bool b1 = check6(s); //调用check_size(s, 6)
用在find_if中:
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, sz));
名字_n都定义在一个名为placeholders的命名空间中,这个命名空间本身定义在std命名空间,为使用这些名字,两个命名空间都要写上,上例中使用bind函数前已经使用了:
using std::placeholders::_1;
对每个占位符名字,我们都必须提供一个单独的using声明,很烦人,可以这样:
using namespace std::placeholders;
说明希望来自namespace_name中的名字在我们程序中都可以直接使用。与bind一样,placeholders命名空间也定义在functional头文件中。
auto g = bind(f, a, b, _2, c, _1);
g(X,Y); //调用f(a, b, Y, c, X)
上例的一个实用例子:
sort(words.begin(), words.end(), isShorter); //从短到长排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1)); //从长到短排序
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中,但有时我们希望以引用传递,或类型无法拷贝,如:
for_each(words.begin(), words.end(), bind(print, os, _1, ' '); //错误,不能拷贝os,原因在于bind拷贝其参数
为了解决以上问题,即直接传递给bind一个对象,而不是拷贝它,我们必须使用ref函数:
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' ');
函数ref返回一个对象,这个对象包含给定的引用,且这个对象是可以拷贝的。标准库中还有一个cref函数,生成一个保存const引用的类。与bind一样,ref和cref也定义在头文件functional中。
在C++旧版本中,标准库定义了两个分别名为bind1st和bind2nd的函数,类似bind,它们作用相似,但只能分别绑定第一个参数和第二个参数,局限性太大,在新标准中已被弃用,即新版本不再支持此特性。新的C++程序中应该使用bind。
使用函数代替lambda,统计出长度小于等于6的单词数量:
#include
#include
#include
using namespace std;
bool IsShorterThan6(const string& s) {
return s.size() < 6;
}
int main() {
vector<string> svec = { "aaa", "bbbbbbb", "c", "dddddddd", "ee" };
vector<string>::iterator it = partition(svec.begin(), svec.end(), IsShorterThan6);
int cnt = it - svec.begin();
cout << cnt << endl;
}
给定一个string,在一个int类型的vector中查找第一个大小大于string长度的值:
#include
#include
#include
#include
#include
using namespace std;
using namespace std::placeholders;
bool check_size(const string& s, string::size_type sz) {
return s.size() < sz;
}
int main() {
auto f = bind(check_size, "ddddd", _1);
vector<int> ivec = { 0,1,2,3,4,5,69,7 };
vector<int>::iterator pos = find_if(ivec.begin(), ivec.end(), f);
if (pos != ivec.end()) {
cout << *pos << endl;
}
}
除了为每个容器定义的迭代器之外,标准库在头文件iterator中还定义了额外几种迭代器:
1.插入迭代器:绑定到一个容器上,可用来向容器插入元素。
2.流迭代器:绑定到输入或输出流上,可用来遍历相关联的IO流。
3.反向迭代器:向普通迭代器相反的方向移动。除了forward_list外,所有标准库容器都有反向迭代器。
4.移动迭代器:这些迭代器移动元素,而不是拷贝它们。
插入器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素,当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
插入迭代器支持的操作:
插入器三种类型:
1.back_inserter:创建一个使用push_back的迭代器。
2.front_inserter:创建一个使用push_front的迭代器。
3.inserter:创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须是指向给定容器的迭代器,元素将被插入到给定迭代器所表示的元素之前。
只有容器支持push_front的情况下,我们才能使用front_inserter,back_inserter类似。
对于使用insert的插入器:
*it = val;
//相当于以下代码
it = c.insert(it, val); //it指向新加入的元素
++it; //递增it使它指向原来的元素
而当我们使用front_inserter时:
list<int> lst = { 1, 2, 3, 4 };
list<int> lst2, lst3;
copy(lst.cbegin(), lst.cend(), front_inserter(lst2)); //lst2内容为4321
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin())); //lst3内容为1234
标准库还定义了unique_copy,它接受第三个迭代器,表示拷贝不重复元素的目标位置,使用unique_copy将vector中不重复的元素拷贝到一个初始值为空的list中:
#include
#include
#include
#include
#include
using namespace std;
int main() {
vector<int> ivec = { 0,1,2,3,4,5,3,2,1 };
sort(ivec.begin(), ivec.end());
list<int> lst;
unique_copy(ivec.begin(), ivec.end(), back_inserter(lst));
for (int i : lst) {
cout << i << endl;
}
}
虽然iostream不是容器类型,但标准库定义了可以用于这些IO类型对象的迭代器。istream_iterator读取输入流,ostream_iterator向一个输出流写数据,这些迭代器将它们对应的流当作一个特定类型的元素序列来处理。通过使用流迭代器,我们可以使用泛型算法从流对象读取、写入数据。
istream_iterator操作:
创建一个流迭代器时,必须指定流迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流,因此,istream_iterator要读取的类型必须定义了输入运算符>>。当创建一个istream_iterator时,我们可以将它绑定到一个流。当然,我们也可以默认初始化流迭代器,这样就创建了一个可以当做尾后值使用的迭代器。
istream_iterator<int> int_it(cin); //从cin读取int
istream_iterator<int> int_eof; //尾后迭代器
ifstream in("afile");
istream_iterator<string> str_it(in); //从afile读取字符串
例子,用istream_iterator从标准输入读取数据,存入vector中:
istream_iterator<int> in_iter(cin); //从cin读取int
istream_iterator<int> eof; //istream尾后迭代器
vector<int> ivec;
while (in_iter != eof) { //当输入的值不是int时,只有此非法输入之前的值会保存在ivec中
ivec.push_back(*in_iter++);
}
for (int i : ivec) {
cout << i << endl;
}
以上循环从cin读取int值,保存在ivec中,在每个循环步中,检查in_iter是否等于eof。eof被定义为空的istream_iterator,可以当作尾后迭代器使用。对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或IO错误,迭代器的值就与尾后迭代器相等。
以上程序可简写为以下形式:
istream_iterator<int> in_iter(cin), eof;
vector<int> ivec(in_iter, eof);
使用算法操作流迭代器:
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl;
上例将输入中的数值求和。
istream_iterator允许使用懒惰求值,即,我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据,标准库保证的是我们第一次解引用迭代器之前,从流中读取数据的操作已经完成。也就是允许用到的时候再读取。对一般程序来说,影响不大,但对于创建了一个istream_iterator但并没有使用就销毁了和两个不同对象同步读取同一个流时,何时读就很重要了。
我们可以对任何具有输出运算符<<的类型定义ostream_iterator。创建ostream_iterator时,我们可以提供可选的第二参数,它是一个字符串,在输出每个元素后都会打印此字符串,此字符串必须是一个C风格字符串。必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。
ostream_iterator操作:
使用ostream_iterator输出值的序列:
vector<int> ivec = {0,1,2,3,4,5};
ostream_iterator<int> out_iter(cout, " ");
for (auto e : ivec) {
*out_iter++ = e; //赋值语句实际上将元素写到cout
}
cout << endl;
以上循环可以简写为:
for (auto e : ivec) {
out_iter = e; //*和++对ostream_iterator对象不做任何事,但不推荐这么写
}
可以使用copy来打印ivec中的元素:
vector<int> ivec = {0,1,2,3,4,5};
ostream_iterator<int> out_iter(cout, " ");
copy(ivec.begin(), ivec.end(), out_iter);
我们可以使用流迭代器处理类类型,我们可以为任何定义了>>(<<)操作符的类型创建i(o)stream_iterator对象。由于Sales_item类型定义了输入运算符和输出运算符,因此可以使用IO迭代器重写统计卖出的商品合计(相同商品一起输入)代码:
istream_iterator<Sales_item> item_iter(cin), eof;
ostream_iterator<Sales_item> out_iter(cout, "\n");
Sales_item sum = *item_iter++; //将第一笔交易记录存在sum中
while (item_iter != eof) {
if (item_iter->isbn() == sum.isbn()) {
sum += *item_iter++;
}
else {
out_iter = sum; //输出当前sum值和一个换行符
sum = *item_iter++;
}
]
out_iter = sum; //记得输出最后一组记录的和
使用流迭代器读取一个文本文件,存入一个vector
#include
#include
#include
#include
#include
using namespace std;
int main() {
ifstream ifs(文件路径);
istream_iterator<string> in_it(ifs), eof;
vector<string> svec;
while (in_it != eof) {
svec.push_back(*in_it++);
}
for (string s : svec) {
cout << s << endl;
}
}
使用流迭代器、sort和copy从标准输入读取一个整数序列,将其排序,并将结果写到标准输出:
#include
#include
#include
#include
using namespace std;
int main() {
istream_iterator<int> inIt(cin), eof;
vector<int> ivec;
while (inIt != eof) {
ivec.push_back(*inIt++);
}
sort(ivec.begin(), ivec.end());
ostream_iterator<int> outIt(cout);
copy(ivec.begin(), ivec.end(), outIt);
}
修改上面的程序,使其输出的结果不重复,只需要将copy函数改为unique_copy函数即可。
反向迭代器是从容器尾向容器首反向移动的迭代器,对于反向迭代器,递增和递减操作的含义会颠倒过来,即++it会移动到前一个元素。
除了forward_list之外,其他容器都支持反向迭代器,我们可以通过rbegin、rend、crbegin、crend成员函数来获得反向迭代器,这些成员返回指向容器尾元素和首元素之前一个位置的迭代器。与如同迭代器一样,反向迭代器也有const和非const版本。
从尾到首打印容器:
vector<int> ivec = { 0, 1, 2, 3 };
for (vector<int>::const_reverse_iterator rb = ivec.crbegin(); rb != ivec.crend(); ++rb) {
cout << *rb << endl;
}
用于sort:
sort(ivec.begin(), ivec.end()); //从小到大排序
sort(ivec.rbegin(), ivec.rend()); //从大到小排序
除了forward_list之外(它不支持递减迭代器),标准容器上的其他迭代器既支持递增迭代器,也支付递减迭代器。流迭代器不支持递减操作。因此不能从一个forward_list或一个流迭代器创建反向迭代器。
假定有一个名为line的string,保存着一个逗号分隔的单词列表,我们希望打印line中的第一个单词:
string line = "aaa,bbb,ccc,ddd";
string::const_iterator comma = find(line.cbegin(), line.cend(), ','); //comma的类型必须为string::const_iterator,因为输入的迭代器是const的
cout << string(line.cbegin(), comma) << endl; //此处的line.cbegin也必须是const的,因为comma是const的
如希望打印最后一个单词:
string line = "aaa,bbb,ccc,abc";
string::const_iterator rcomma = find(line.crbegin(), line.crend(), ','); //comma的类型必须为string::const_iterator,因为输入的迭代器是const的
cout << string(line.crbegin(), rcomma) << endl; //错误,会打印cba
cout << string(rcomma.base(), line.cend) << endl; //正确
以上代码调用reverse_iterator的base成员得到一个正向迭代器:
我们看到图中的rcmma和rcmma.base()指向不同元素,这保证了元素范围无论是正向还是反向处理都是相同的。
任何算法的基本特性是它要求其迭代器提供哪些操作,某些算法,如find,只要求通过迭代器访问元素、递增元素以及比较两个迭代器是否相等这些能力,而其他一些算法,如sort,还要求读、写和随机访问元素的能力,算法所要求的迭代器操作可以分为5个迭代器类别,每个算法都会对它的每个迭代器参数指明须提供哪类迭代器:
还有一种算法分类方式是按照是否读、写或是重排序列中的元素来分类。
类似容器,迭代器也定义了一组公共操作,一些操作所有迭代器都支持。如ostream_iterator只支持递增、解引用、赋值,但vector、string、deque的迭代器除了这些操作外,还支持递减、关系和算术运算。
迭代器是按他们所提供的的操作来分类的,这些分类形成了一种层次(上图中从上到下层级依次增加),除了输出迭代器之外,一个高层别的迭代器支持低层类别迭代器的所有操作。
C++标准指明了泛型和数值算法的每个迭代器参数的最小类别,如,find算法在一个序列上进行一遍扫描,对元素进行只读操作,因此至少需要输入迭代器。向算法传递一个能力更差的的迭代器会产生错误,但很多编译器不会给出任何警告。
输入迭代器:可以读取序列中的元素,它必须支持:
1.用于比较两个迭代器是否相等的运算符(==、!=)。
2.用于推进迭代器的前置和后置递增运算符。
3.用于读取元素的解引用*;解引用只会出现在赋值运算符右侧。
4.->。
输入迭代器只能用于顺序访问。对于输入迭代器,*it++保证是有效的,但递增它可能导致所有其他指向流的迭代器失效,其结果就是,不能保证保存下来输入迭代器的初始值,因为它可能失效,结果就是只能单遍扫描算法。算法find、accumulate要求输入迭代器,而istream_iterator是一种输入迭代器。
输出迭代器:只写而不读元素。它必须支持:
1.用于推进迭代器的前置和后置递增运算。
2.解引用,且只出现在运算符的左侧(向一个解引用的输出迭代器赋值,就是将值写入它所指的元素)。
我们只能向一个输出迭代器赋值一次,用于单遍扫描。用作目的位置的迭代器通常都是输出迭代器,如copy函数的第三个参数。ostream_iterator是输出迭代器。
前向迭代器:可以读写元素,在序列中只能沿着一个方向移动,它支持所有的输入输出迭代器的操作,而且可以多次读写同一个元素,因此我们可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描。算法replace要求前向迭代器,forward_list上的迭代器是前向迭代器。
双向迭代器:可以正反读写序列中的元素。除了支持所有前向迭代器的操作外,还支持前置和后置的–运算符。算法reverse(将容器中的元素逆序)要求双向迭代器。除了forward_list的迭代器外,其他标准库容器都提供双向迭代器。
随机访问迭代器:提供在常量时间内访问序列中任意元素的能力。此迭代器支持双向迭代器的所有功能,还支持:
1.用于比较两个迭代器相对位置的关系运算符<、<=、>、>=。
2.迭代器和一个整数的加减运算+、-、+=、-=,计算结果是迭代器在序列中前进或后退给定整数个元素后的位置。
3.用于两个迭代器上的减法运算符,得到两个迭代器的距离。
4.下标运算符it[n],与*(it + n)等价。
算法sort要求随机访问迭代器,array、deque、string和vector的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。
list的迭代器是双向迭代器。
copy第三个参数要求输出迭代器。unique函数需要前向迭代器。
算法还有一组参数规范,多数算法具有如下4种形式之一:
alg(beg, end, other args);
alg(beg, end, dest, other args); //接受单个目标迭代器的算法
alg(beg, end, beg2, other args); //接受第二个输入序列的算法
alg(beg, end, beg2, end2, other args); //接受第二个输入序列的算法
其中alg是算法名字,beg和end表示输入范围,几乎所有算法都接受一个输入范围。
接受单个目标迭代器的算法的dest参数表示可以写入的目的位置的迭代器,算法假定写入元素的数量是安全的,即此处的空间足够容纳所有输入。
如果dest是一个直接指向容器的迭代器,那么算法将输出数据写到容器中已存在的元素内,更常见的情况是dest被绑定到一个插入迭代器或一个ostream_iterator上,这样不管写入多少元素都是安全的。
接受第二个输入序列的算法用beg2或beg2和end2表示第二个输入范围。如果算法接受beg2和end2,这两个迭代器表示第二个范围;如果算法接受beg2(不接受end2)作为第二个输入范围的首元素,那么此范围的结束位置未指定,则这些算法假定beg2开始的范围与beg和end所表示的范围至少一样大。
一些算法使用重载形式传递一个谓词,接受谓词参数来代替<或==运算符的算法和不接受谓词的版本的算法通常是一对重载函数。
_if版本的算法:第三个参数接受一个元素值的算法通常有一个不同名版本,该版本接受一个谓词代替元素值,接受谓词参数的算法都有附加的_if后缀:
find(beg, end, val); //查找val第一个出现的位置
find_if(beg, end, pred); //查找第一个令谓词pred为真的元素
区分拷贝元素的版本和不拷贝的版本:默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中,这些算法还提供另一个版本,将元素写到一个指定位置,写到指定位置的函数要在名字后面附加一个_copy:
reverse(beg, end);
reverse_copy(beg, end, dest);
一些算法同时提供_copy和_if版本,这些版本接受一个目的位置迭代器和一个谓词:
remove_if(v1.begin(), v1.end(), [](int i) { return i & 1; }); //将奇数删除,但不是真正删除,只是移动到容器后部
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i & 1; }); //将偶数存入v2中,v1不变
根据算法和参数名猜执行什么操作:
replace(beg, end, old_val, new_val); //将输入范围内所有old_val改为new_val
replace_if(beg, end, pred, new_val); //将输入范围内所有使pred为真的值替换为new_val
replace_copy(beg, end, dest, old_val, new_val); //将输入范围内所有old_val改为new_val,但不写回原序列,而是写到dest处
replace_copy_if(beg, end, dest, pred, new_val); //将输入范围内所有使pred为真的值替换为new_val,但不写回原序列,而是写到dest处
链表类型list和forward_list定义了几个成员函数形式的算法,如下表:
特别是,list和forward_list定义了独有的sort、merge、remove、reverse和unique。通用版本的sort要求随机访问迭代器,因此不能用于list和forward_list,因为这两个类型分别提供双向迭代器和前向迭代器。链表类型定义的其他算法的通用版本也可以用于链表,但代价太高,这些算法需要交换输入序列中的元素,但对链表来说,可以通过改变元素间的链接而不是真的交换它们的值来快速交换元素。
建议对于list和forward_list,应优先使用成员函数版本的算法而非通用算法。
链表还定义了splice算法:
以上的操作中,list的splice函数的lst2参数必须是list类型,forward_list的splice函数的lst2参数必须是forward_list类型。
forward_list<int> iflst = { 0,1,2,3,4 };
forward_list<int> iflst2 = { 0,1,2,3,4 };
iflst.splice_after(iflst.before_begin(), iflst2, iflst2.before_begin());
for (int i : iflst) {
cout << i << endl; //输出001234,即若使用forward_list版本的splice时,把第三个迭代器参数之后的元素放到第一个参数之后的位置
}
list的迭代器不能支持加上一个数字的操作,只能递增,不是随机访问迭代器,而是双向迭代器。
list<int> ilst = { 0,1,2,3,4 };
ilst.splice(ilst.begin(), ilst, ++ilst.begin(), ilst.end());
for (int i : ilst) {
cout << i << endl; //输出12340
}
上例中我们可以发现splice这个版本的机制,将迭代器范围内第一个元素放到p迭代器指向的元素之前,然后再将迭代器指向之前p指向的元素,之后删除迭代器范围内第一个元素,接着处理第二个元素。
链表的特有版本和通用版本不完全相同,其区别在于,链表版本会改变底层容器,如remove的链表版本会删除指定元素,unique的链表版本会删除第二个和后续的重复元素:
vector<int> ivec = { 0,1,2,3,4,4,5,6,7 };
cout << ivec.size() << endl; //输出9
remove(ivec.begin(), ivec.end(), 4);
cout << ivec.size() << endl; //输出9
for (int i : ivec) {
cout << i << endl; //会发现输出0123567后面还有两个数字,只是移动了位置,而链表版本的会直接删除指定的元素
}
merge和splice的链表版本会销毁其参数,但通用版本的merge将合并的序列写到一个给定的目的迭代器,两个序列是不变的。