因翻译太耗时了,还是看中文比较快,先做笔记如下
我们通常希望,在某个容器中查找某个值。如果为每个容器都提供这样的一个成员函数,那是非常繁琐的操作。
此时,c++标准库提供了一组通用函数,来达到这种效果。
例如:
int val = 42;
auto result = find(vec.cbegin(),vec.cend(),val);
string val = "a value";
auto result = find(lst.cbegin(),lst.cend(),val);
int ia[] = {27,210,12,47,109,83};
int val = 83;
int * result = find(begin(ia),end(ia),val);
上面三组程序,都使用了同一个函数find,来查找,容器中给定范围是否有某个值。
这里的find就是泛型算法中的一种算法。
通常将这些泛型算法分为三类:读算法,写算法,排序算法
读算法:只会读取容器中的元素,不会修改容器的元素,例子如下:
//accumulate,前两个参数表示需要求和的元素的范围,后一个表示和的初始值。
int sum = accumulate(vec.cbegin(),vec.cend(),0);
accumulate的第三个参数的类型,决定了函数调用哪个加法运算符以及返回值的类型
思考下面的写法:
string sum = accumulate(v.cbegin(),v.cend(),"");
上面的写法会报错误,因为第三个参数决定了函数调用哪个加法,因为第三个参数是字符串字面量,类型为char *,他没有可以使用的加法,所以报错,正确的做法如下:
string sum = accumulate(v.cbegin(),v.cend(),string(""));
对于只读取而不改变元素的算法,通常最好使用cbegin()和cend().但是,如果你使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()的结果作为参数
再来个例子:
//equal 比较两个容器中的元素是否相等,如果相等个,返回true,否则返回false
equal(roster1.cbegin(),roster1.cend(),roster2.cbegin());
上面函数能够正确运行的前提是:roster2中的元素不能比rouster1的元素少
写算法:表示会修改容器元素值的算法,如下面:
//fill使用第三个参数,来赋值容器中的每一个值
fill(vec.begin(),vec.end(),0);
fill(vec.begin(),vec.begin()+vec.size()/2),10);
注意上面的注释,是赋值,而不是重新创建。思考下面的例子:
//fill_n 第二个参数表示需要赋值的元素的多少
vector<int> vec;
fill_n(vec.begin(),10,0);
这种是错误的,因为,我们想给vec中的十个元素,赋值为0,但是vec中没有任何元素,它是一个空的容器。
为了达到赋值的时候,就能够自动创建一个元素,此处引入一种新的迭代器,叫做插入迭代器。给这个迭代器赋值,就相当于给这个插入一个新值。后面将会有详细的插入迭代器的笔记,此处只需要熟悉下面的代码即可
vector<int> vec;
auto it = back_inserter(vect);
*it = 42; //向容器中插入一个元素,值为42
vector<int> vec;
fill_n(back_inserter(vec),10,0);//向容器中插入10个元素
下面再记一个拷贝算法
int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a1)] ;
auto ret = copy(begin(a1),end(a1),a2);
//replace ,查找范围内,所有的0,并替换成42
replace(ilst.begin(),ilist.end(),0,42);
//replace_copy,查找范围内的,所有的0,并将其替换成42,替换的结果,放入第三个迭代器指示的位置中
replace_copy(ilst.cbegin(),ilist.cend(),back_inserter(ivec),0,42);
排序算法:对容器内部的元素进行排序的算法
介绍两个算法,一个为sort,一个为unique,sort将使用<运算符,排序容器中的元素。
unique算法,将不相同的元素排列在前面,然后返回不重复元素的尾后迭代器。
很多算法都使用了元素的小于<或者等于==运算符。但是有些元素并没有定义这两个运算符。此时,如果需要使用泛型算法,就需要向泛型算法,传递一个操作,来代替默认的小于或者等于运算符,这个操作,称为谓词。
为了达到和小于或者等于相同的效果,这个操作需要满足下面几个条件:
满足上面条件的,可以是一个自定义的函数,也可以是一个可调用的对象,还可以是一个lambada表达式。
下面先记下函数这种类型,后面会有lambada和可调用对象的笔记
如果我们想要sort按照字符串的长度进行排序。那么就不能使用默认的小于运算符,此时我们需要传递给sort一个操作。这个操作需要满足上面的两个条件,这里以函数为例:
bool isShorter(const string &s1,const string &s2){
return s1.size()<s2.siz();
}
//按照长度排序
sort(words.begin(),words,end(),isShorter);
在上面的例子中,可能疑惑的有两点,我如何知道自定义函数的形参的个数和类型???
个数:每一个算法(此例中是sort)都规定了,他能接受的操作(此例中为isShorter)的参数个数,sort规定为两个
类型:传递过来的操作(此例中为isShorter),接受的参数类型为容器的元素类型
一个算法规定了,自定义操作,所能接受的参数个数。如果自定操作,需要更多额外的信息,需要外部传递进来,此时使用lambda更加合适
lambda
可以这样理解lambda,他生成一个匿名的可调用对象。语法格式如下:
[捕获列表](参数列表)->返回类型{执行代码}
捕获列表表示:生成的匿名对象需要访问的局部变量,因为一个对象无法访问局部变量。所以需要将局部变量放在捕获列表中。
参数列表:生成的匿名对象,被调用时,需要传递的实参参数类型列表
返回类型:即可调用对象,调用完成之后返回的类型
例子:
auto f = [] {return 42;};
cout << f() << endl;//输出42
当参数列表为空的时候,可以忽略
如果忽略掉返回类型,则编译器从执行代码中推断,这要求执行代码,只有一条return语句。否则,编译器推断其返回类型为void
注意:如果lambda 的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void
假如,sz是一个局部变量,出现在一个函数内部,例如下面的代码:
[](const string & a){
return a.size() >= sz;
}
因为,上面的lambda是一个匿名对象,这个对象对局部对象无法进行直接访问,所以,在执行代码内部,直接使用sz是错误的。此时应该使用捕获列表。如下:
[sz] (const string &a){
return a.size() >= sz;
}
现在来思考一下,lambda的实现。在后面的笔记中将会详细介绍lamda的实现。这里先给出一个感性的概念。
编译器对lambda会生成一个未命名的类类型。当我们使用这个lambda表达式时,使用的是这个未命名类类型的一个对象。
那么这个类类型,为了能够访问局部变量,需要把局部变量放入捕获列表中。
那么这个类类型是如何达到这种效果的呢?
实际上是,类类型中定义相应的数据成员。这些数据成员的值就是捕获列表中的局部变量的值。所以这个未命名类型的对象访问自己的成员,就相当于访问了捕获列表中的变量。
那么变量有这么几种类型存在:引用,指针,拷贝
而在lambda中生成的类类型,就可以让捕获列表,使用引用和,拷贝这两种。他们分别称为引用捕获和值捕获,引用捕获只需要在需捕获的变量前面加上取地址符即可。见下面例子
值捕获的例子:
void fcn1(){
size_t v1 =42;
auto f = [v1] {return v1;};
v1 = 0;
auto j= f();//j为42
}
引用捕获的例子
void fcn2(){
size_t v1 = 42;
auto f2 = [&v1]{return v1;}
v1 = 0;
auto j = f2();//j为0
}
注意:尽量保持lamda的变量捕获简单明了
隐式捕获
除了自己能够显示在捕获列表中写下需要捕获的变量以外,还可以让编译器智能决定哪些变量需要捕获。只需要在捕获列表中,写上&或者=,就表示让编译器智能决定需要捕获的变量
当然,也可以即使用显示捕获,也可以使用隐式捕获。但是有一个前提:隐式捕获和显示捕获的方式必须是不同。也就是说,当隐式捕获是引用捕获时,显示捕获必须是值捕获;当隐式捕获是值捕获的时候,显示捕获必须是引用捕获。
可变lambda
默认情况下,lambda不准改变其捕获的值。如果想要改变,需要使用mutable关键字。如下:
void fcn3(){
size_t v1 = 42;
auto f = [v1] () mutable {return ++v1;};
v1 = 0;
auto j = f();//j为43
}
lambda返回类型
考虑下面的写法:
transform(vi.begin(),vi.end(),vi.begin(),
[](int i){return i < 0? -i:i;});
再将上面的用法写成如下的形式:
transform(vi.begin(),vi.end(),vi.begin(),
[](int i){if(i<0) return -i;else return i;});
前面提到:如果lambda执行体的语句含有多个return语句,且没有指明返回类型,那么编译器将认为返回类型为void
上面第二个例子,返回类型为void,他是错误的。正确的写法应该是第一种写法。
bind参数绑定
auto newCallable = bind(callable,arg_list);
bind输入一个可调用对象,输出也是一个可调用对象,但是,可以根据arg_list参数,进行灵活处理
更一般的例子如下:
using std::placeholder::_1;
using std::placeholder::_2;
auto g = bind(f,a,b,_2,c,_1);
//等价于调用f(a,b,3,c,1)
g(1,3);
注意:上面使用的_1,定义在std::placeholder命名空间中,如果想方便使用,需要加上:
using namespace std::placeholder;
绑定引用参数
思考下面的例子:
//os是一个局部变量,引用一个输出流
//c 是一个局部变量,类型为char
for_each(words.begin(),words.end(),[&os,c](const string &s){os << s << c;})
可以编写一个类似的函数,完成同样的工作:
ostream& print(ostream &os, const string & s,char c){
return os << s << c;
}
如果用bind来调用print如下
for_each(words.begin(),words.end(),bind(print,os,_1,' ');
上面这个例子,出现一个问题,因为print第一个参数是一个引用,而bind传递过来的参数,确是值传递。下面才是正确的写法
for_each(words.begin(),words.end(),bind(),bind(print,ref(os),_1,' '));
上面例子中使用了ref函数,这个函数返回一个对象,这个对象包含给定的引用,并且这个对象是可以拷贝的。同样的会有一个cref函数,生成一个保存const引用的对象。
除了前面记下的迭代器以外,标准库文件iterator中还定义了额外的几种迭代器。如下:
为插入迭代器赋值,就相当于给这个迭代器所指的容器,插入一个新值。
插入迭代器有下面三种,他们的不同,主要在于插入的位置。
下面给出插入迭代器的赋值,自增,等运算符的操作。
inserter说明如下:
*it = val;
//等价于
it = c.insert(it,val);//it指向新加入的元素
++it;//递增it使他指向原来的元素
front_inserter说明如下:
list<int> lst = {1,2,3,4};
list<int> lst2,lst3;
//lst2 包含 4 3 2 1
copy(lst.cbegin(),lst.end(),front_inserter(lst2));
//lst 3 包含 1 2 3 4
copy(lst.cbegin(),lst.end(),inserter(lst3,lst3.begin()));
两种类型的流迭代器:
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从标准输入中读取的例子。
istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;
while(in_iter != eof)
vec.push_back(*in_iter++);
我们还可以使用如下的代码,达到同样的目的
istream_iterator<int> in_iter(cin),eof;
vector<int> vec(in_iter,eof);
上例中,用一个迭代器来构造一个vector,而这对迭代器指向了输入流,表示从输入流中,读取并构造vector
下面给出输入流迭代器常见的操作
istream_iterator允许使用懒惰求值
当将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取。标准库中的实现保证:在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成了。
对于大多数程序来说,立即读取还是延迟读取没什么差别。但是,如果我们创建了一个istream_iterator,没有使用就销毁了,或者我们正在从两个不同的对象同步读取同一个流,那么何时读取可能就很重要了。
ostream_iterator操作
所有具有输出运算符的类型,都可以定义相应的ostream_iterator.当创建一个ostream_iterator时,我么可以提供第二个可选的参数,这个参数是一个字符串。在输出每个元素之后,都会打印这个字符串。
此字符串必须是一个c风格的字符串。必须将ostream_iterator绑定到一个指定的流,不允许空的或者表示尾后位置的ostream_iterator
下面给出ostream_iterator的操作
例子如下:
ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
*out_iter++ = e;
cout << endl;
此程序中的每个元素都写到了cout中,每个元素后面加一个空格。
由上面的表格可以知道,还可以写成下面这种形式
for(auto e:vec)
out_iter = e;
cout << endl;
但是推荐第一种写法,迭代器的使用与其他的保持一致。如果想改成其他迭代器的操作,修改起来较容易。
下面这种写法更简单
copy(vec.begin(),vec.end(),out_iter);
cout << endl;
反向迭代器:在容器中从尾元素向首元素移动的迭代器。
这样递增一个反向迭代器,会移动到前一个,递减一个反向迭代器会移动到下一个。
除了forward_list以外,其他容器都支持反向迭代器,可以使用rbegin,rend,crbegin,crend来获取。
例子如下:
vector<int> vec = {0,1,2,3,4,5,6,7,8,9};
for(auto r_iter = vec.crbegin();r_iter!=vec.crend();++r_iter)
cout << *r_iter << endl;
上例打印: 9 8 7 6 5 4 3 2 1 0
注意:反向迭代器需要递减运算符
因此,对于forward_list和流迭代器来说,不能创建对应的反向迭代器
注意:获取反向迭代器对应的普通迭代器,使用其base成员。
因为实在不知怎么做笔记好,直接复制吧,又太多,还是直接参考原书即可
《primer c++ 第五版,中文版》10.5小节
标准库提供的算法,可以用于list和forwrd_list,但是消耗太大,因为这些算法通常需要移动元素。但是对于链表来说,不需要移动元素,只需要移动链接就可以,因此链表提供了自己的一套的算法。