@taqikema
2018-01-12T10:45:30.000000Z
字数 5300
阅读 1170
C++Primer
学习记录
泛型算分
==
运算符、sort算法需要使用<
运算符。泛型算法本身不会执行容器的操作,它们只会运行与迭代器之上,执行迭代器的操作。即,一个算法永远不会直接改变底层容器的大小。accumulate(vec.cbegin(), vec.cend(), 0);
这里面有三个编程假定: string sum = accumulate(vec.cbegin(), vec.cend(), "")
就是错误的,因为 const char*并没有+
运算符。写入算法。向目的位置迭代器写入数据的算法都假定目的位置足够大,能容纳要写入的元素,算法本身不会检查写操作。可以使用插入迭代器来向容器中添加元素,back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。
vector<int> vec; // 空向量
fill_n(vec.begin(), 10, 0); // 错误,向空向量写入元素!
fill_n(back_inserter(vec), 10, 0); // 添加 10个元素到 vec
重排算法。unique,只对相邻的重复元素有效,所以使用之前需要先排序,并且其结果会使得重复元素出现在尾部!
lambda表达式,可以理解为是一个未命名的内联函数。与普通函数不同,lambda必须使用尾置返回类型。形式如[捕获列表](参数列表) ->返回类型 {函数体}
。
它可以忽略参数列表和返回类型,但必须永远包括捕获列表和函数体,如auto f = [] { return 42; };
。如果忽略返回类型, lambda根据函数体中的代码推断出返回类型。
lambda不能有默认实参,因此,一个 lambda调用的实参数目永远与形参数目相等。
捕获列表只用于局部非 static变量,lambda可以直接使用局部 static变量和在它所在函数之外声明的名字。在下面代码段中, cout不是自定义的局部变量,而是定义在头文件 iostream中的,但 lambda表达式中仍然可以使用该变量,只要改代码段出现的作用域中包括了头文件 iostream就可以了。
for_each(wc, words.end(),
[](const string &s){cout << s << " ";});
当定义一个 lambda时,编译器会隐式地生成一个与 lambda对应的新的未命名的类类型。其中,捕获列表中的参数就是构造函数的参数,且是这个未命名类的数据成员 ,并且在 lambda对象创建时被初始化。而 lambda表达式中的参数与函数调用运算符的参数对应。
值捕获。与参数不同,被捕获的变量的值是在 lambda创建时被拷贝,而不是调用时拷贝。
size_t v1 = 42;
auto f = [v1] {return v1;};
v1 = 0;
auto j = f(); // J为 42;v1在 lambda创建时被
// 拷贝,随后对外面的 v1的修改与 lambda中的 v1无关
引用捕获。当我们在 lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。不过,当以引用方式捕获一个变量时,必须保证在 lambda执行时变量是存在的。
size_t v1 = 42;
auto f = [&v1] {return v1;};
v1 = 0;
auto j = f(); // J为 0;lambda中的 v1只是外面 v1的引用
隐式捕获。编译器会根据 lambda体中的代码来推断我们要使用哪些变量,&表示引用捕获,=表示值捕获。当混合使用了隐式和显式捕获时,捕获列表中的第一个元素必须是一个 &或=,来制定默认引用方式。另外,显式捕获的变量必须使用与隐式捕获不同的方式。
可变 lambda。
对于值捕获变量,默认情况下是不可以在 lambda表达式中改变其值的。如果希望改变一个值捕获的变量的值,在参数列表后加上关键字 mutable。
size_t v1 = 42;
// f能够改变它所捕获的局部变量
auto f = [v1] () mutable {return v1;};
v1 = 0;
auto j = f(); // J为 43
而引用捕获的变量,如果此引用指向的是 const变量,则是否添加 mutable都不能在 lambda表达式中修改其值;而如果指向的是非 const变量,则不需要 mutable,默认情况下就可以在 lambda表达式中修改其值。
某些标准库算法只能接受一元谓词,而我们可能需要向其传递两个或多个参数,之前使用捕获列表的 lambda表达式可以完成这一任务。这里,还可以使用 bind
函数,它可以看作是一个函数适配器。
它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。_n
是占位符,表示新调用对象的参数。这些名字都定义在名为 placeholders的命名空间中,使用之前需要声明。
using std::placeholders::_1;
// check6是一个可调用对象,接受一个 string类型的参数
// 并用此 string和值6来调用 check_size
auto check6 = bind(check_size, _1, 6);
可以用 bind来绑定给定的可调用对象的参数或重新安排其顺序。
auto g = bind(f, a, b, _2, c, _1);
// g(x, y)会调用 f(a, b, y, c, x)
// 按单词长度由短至长排序
sort(w.begin(), w.end(), isShorter);
// 按单词长度由长至短排序
sort(w.begin(), w.end(), bind(isShorter, _2, _1));
bind的那些不是占位符的参数都是被拷贝到 bind返回的可调用对象中的,因此,对于有些我们希望以引用方式传递或无法拷贝的类型的参数,需要使用 ref/cref。其中,ref返回一个保存给定对象的普通引用的类对象,而返回一个保存给定对象的 const引用的类对象。
void f(int& n1, int& n2, const int& n3) {
++n1;
++n2;
}
int n1 = 1, n2 = 2, n3 = 3;
auto bf_1 = bind(f, n1, ref(n2), cref(n3));
n1 = 10;
n2 = 11;
n3 = 12;
bf_1(); // 此时,n1 == 1, n2 == 12, n3 == 12。这是因为 n1是值拷贝,
// 函数之外的 n1并没变化。而 n2是引用,值会发生变化。
// 下面的语句是错误的,cref中保存的是 const引用,f中相应的参数是普通引用!
auto bf_2 = bind(f, n1, cref(n2), cref(n3));
插入迭代器。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。
iostream迭代器。通过使用流迭代器,可以用泛型算法从流对象读取数据以及向其写入数据。流迭代器在绑定了输入/输出流后,就可以当做是普通的数据容器的迭代器来使用。
istream_iterator,使用>>
来读取流,因此 istream_iterator要读取的类型必须定义了输入运算符。此外,默认初始化迭代器,可以作为尾后值的迭代器,来表明流数据的结束。
istream_iterator<int> in_iter(cin), eof; // 从 cin读取 int
vector<int> vec(in_iter, eof); // 从迭代器范围构造 vec
ostream_iterator,要输出的类型必须定义了<<
输出运算符。可以提供第二个参数,表示在输出每个元素后都会打印的字符串。另外,结合 copy算法一起使用,比编写循环更为简单。不允许空的或表示尾后位置的 ostream_iterator。
ostream_iterator<int> out_iter(cout, " ");
for (auto e : vec)
*out_iter++ = e; // 该赋值语句实际上将元素写到 cout
cout << endl;
反向迭代器,从尾元素向首元素反向移动的迭代器。递增(++it)一个反向迭代器会移动到前一个元素,递减(--it)会移动到下一个元素。
++
,也需要支持--
,因此不能从 forward_list或一个流迭代器创建反向迭代器。递增反向迭代器会在容器中反向移动,为了能在容器中正向移动,需要使用 base
成员函数将其转换回普通迭代器。
// 在一个逗号分隔的列表中查找最后一个元素并将其输出
auto rcomma = find(line.crbegin(), line.crend(), ',');
// 错误,将逆序输出单词的字符
cout << string(line.crbegin(), rcomma) << endl;
// 正确,从逗号的下一个位置开始读取字符直到末尾
cout << string(rcomma.base(), line.cend()) << endl;
[line.crbegin(), rcomma)
和[rcomma.base(), line.cend())
必须指向 line中相同的元素范围。因此,从一个普通迭代器初始化一个反向迭代器,或是给一个反向迭代器赋值时,结果迭代器与原始迭代器指向的并不是相同的元素。移动迭代器。一般来说,普通迭代器的解引用运算符返回一个指向元素的左值,而移动迭代器的解引用运算符则生成一个右值引用。可以使用标准库的 make_move_iterator
函数将一个普通迭代器转换为一个移动迭代器。某些算法会根据迭代器解引用后得到的是左值或右值引用来调用元素类型的拷贝构造或移动构造函数。
按照迭代器所提供的操作从低到高来分类,分为输入、输出、前向、双向和随机访问迭代器。除输出迭代器外,一个高层类别的迭代器支持低层类别迭代器的所有操作。
算法除了参数规范,还遵循一套命名和重载规范。
一些算法使用重载形式传递一个谓词。
unique(beg, end); // 使用 == 运算符比较元素
unique(beg, end, comp); // 使用 comp 运算符比较元素
接受一个元素值的算法通常有一个不同名的(非重载)版本,该版本接受一个谓词代替元素值,接受谓词参数的算法都有附加的 _if
后缀。
find(beg, end, val); // 查找输入范围中 val第一次出现的位置
find(beg, end, pred); // 使用 comp 运算符比较元素
区分拷贝元素的版本和不拷贝的版本
reverse(beg, end); // 反转输入范围中元素的顺序
reverse_copy(beg, end, dest); // 将元素按逆序拷贝到 dest
链表类型 list和 forward_list定义了几个成员函数形式的算法,如 sort、merge、remove、reverse和unique。
对于链表类型,应该优先使用成员函数版本的算法而不是通用版本。因为链表可以通过改变元素间的链接而不是真的交换它们的值来“交换”元素,因此,其性能要比通用算法好得多。其中,通用 sort算法要求随机访问迭代器,因此不能用于list和 forward_list。