C++ Primer阅读心得(第十章、第十一章)

 1. 泛型算法是独立于容器的;泛型算法只通过迭代器与容器联系,所以泛型算法不执行容器操作,也绝对不会改变容器的大小。泛型算法通过插入迭代器向容器中插入内容;通过流迭代器从输入流读取数据或者向输出流写入数据。

2. 泛型算法可分为三类:

  1. 只读算法,只是读取迭代器范围内的值不做任何修改,例如:find、find_first_of、accumulate
  2. 写算法,不仅读取而且还会修改迭代器范围内的值,这里需要注意泛型算法不负责被写入迭代器范围大小,使用者需要自己确保空间足够,例如:fill、fill_n、copy、replace、replace_copy
  3. 排序算法,对迭代器范围内的数据进行排序,例如:sort、stable_sort、unique

3. 谓词(谓词函数):一些泛型算法允许用户自定义函数取代默认的比较操作符来判断迭代器范围内的值是否满足条件,这种函数被称为谓词。谓词包含接受一个参数的一元谓词和接受两个参数的二元谓词。

4. 可调用对象:对于一个对象,如果可以对它使用调用操作符(也就是一对括号),那么就称为“可调用对象”,包括:

  • 函数
  • 函数指针
  • 重载了函数调用运算符的类的对象
  • lambda表达式

5. lambda表达式:

         [捕获列表] (参数列表) mutable throw() -> 尾置返回类型 { 函数体 }

  • 捕获列表:捕获所在函数中定义的局部变量,全局变量和局部静态变量都可以在函数体中直接使用。捕获分为值捕获和引用捕获;值捕获类似于函数调用中的值传递,但是默认情况下被捕获的值是不允许修改的,只有声明了mutable的lambda函数才可以在修改;引用类似于函数调用中的引用传递,直接传入地址(注意定义lambda函数后修改被捕获的值在引用捕获情况下会影响lambda函数的行为),引用捕获的变量可以修改并且会影响到lambda函数外部的局部变量。可以指示编译器自动分析并捕获外部的局部变量;[=]表示自动值捕获,[&]表示自动引用捕获;可以将命名捕获和自动捕获混合使用,但是要注意:自动捕获必须在命名捕获之前,自动捕获和命名捕获的类型必须不同。使用捕获列表还要注意,被捕获的对象在函数被调用时必须存在并且内容是我们需要的。
    void func()
    {
        int i = 0, j = 1;
        auto func1 = [i, &j]{return i+j;} //ok,值捕获i,引用捕获j
        i++, j++;
        int k = func1(); //k等于2,值捕获的i在lambda函数定义时被确定为0,引用捕获的j在调用时确定为2
        auto func2 = [i]{return ++i;} //错误,i不可修改
        auto func3 = [i]mutable{return ++i;} //ok,有mutable声明可以修改值捕获的i
        auto func4 = [&j]{return ++j;} //ok,引用捕获的j可以修改,并且调用时会影响到外侧
        int kk = func4(); //lambda内部引用被修改,j现在是3
        auto func5 = [=,&j]{return i+j;} //ok,自动值捕获i
        auto func6 = [i, &]{return i+j;} //错误,自动捕获必须在前
        auto func7 = [&,&j]{return i+j;} //错误,混合使用时,自动捕获必须和命名捕获类型不一致
        auto func8 = [i,&j]{cout<<i+j;} //ok,可以使用全局的cout对象,不需要捕获
        int *p = new int[10];
        p[0] = 1;
        auto func9 = [p]{cout<<p[0];} //ok
        delete p;
        func9(); //错误,p已经被释放 
    }
  • 参数列表:与普通函数的参数列表基本上一致,只不过不允许默认参数值,所以调用lambda函数时请传入所有的参数。
  • mutable:声明后,值捕获对象在lambda函数内部可以修改。(注意:不会影响到外侧)
  • throw():声明后,lambda函数可以向外抛出异常。
  • 返回类型:与普通函数不同,lambda函数必须使用尾置返回类型。当lambda函数的函数体只包含一句return时,编译器会自动推断返回类型;但是,当函数体包含两个以上的语句时,如果不声明返回类型,则自动推断此函数的返回类型为void。
    auto func1 = []{return 18;} //返回类型自动推断为int
    auto func2 = []{cout<<"Hello lambda!";} //返回类型自动推测为void
    auto func3 = []{cout<<"Hello lambda!";return 18;} //错误,自动推断返回类型为void
    auto func3 = []->int{cout<<"Hello lambda!";return 18;} //ok
  • 函数体:可以直接使用全局变量和静态变量,使用外侧的局部变量必须捕获。
  • lambda背后的实现机制是:编译器为lambda表达式生成了一个临时未命名可调用类类型;当把lambda表达式赋值给一个自动对象或者当做函数参数时,编译器自动生成了这个类类型的一个临时对象。lambda表达式生成的类类型中有对应每一个捕获对象的数据成员,它们在lambda对象创建时被初始化(而不是调用时)。
    size_t v1 = 42;
    auto f1 = [v1]{return v1;}; //值捕获v1,此时lambda内部的值为42
    v1 = 0; //修改了v1的值,但是由于已经初始化过,所以对lambda对象f1里面的v1没有影响
    size_t v2 = f1(); //v2是42
    size_t v3 = 42;
    auto f2 = [&v3]{return v3;}; //引用捕获v3,将v3的地址传入
    v3 = 0; //修改了v3的值,由于传入的是地址,所以lambda对象f2内部的值也被修改了
    size_t v4 = f2(); //v4是0

注意:参数列表为空时可以省略,mutable和throw()不需要都可以省略,返回类型自动推断时也可以省略,捕获列表和函数体无论如何都不能省略,最简lambda函数:[ ]{ }

6. 参数绑定:c++11中标准库增加了一个bind函数,它返回一个可调用对象。bind是一个函数适配器,它可以改变一个函数的调用方式(比如改变参数的个数或者参数的顺序),来适应原对象的参数列表(例如:某些泛型算法要求传入单参函数)。bind使用占位符“_n”代表调用时的第n个参数,_1第一个参数、_2第二个参数....... (这些占位符在std::placeholders中定义)把引用参数传递给bind函数时,要使用标准库的 ref 或者 cref 函数。

bool isShorter(const string &s1, const string &s2)
{
    return s1.size() < s2.size();
}
auto func1 = bind(isShorter, _1, "test"); //ok,func1是个单参函数对象,它的行为就是使用自己的第一个参数和“test”调用isShort
bool bRet = func1("abc"); //ok
auto func2 = bind(isShorter, _2, _1); //ok,交换两个参数的传入次序

void swap(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}
auto func3 = bind(swap, ref(_1), ref(_2)); //ok,使用ref 保持了引用特性的传入

7. 插入迭代器:插入迭代器不同于普通迭代器,为插入迭代器的解引用赋值(*it = val)具有插入的效果,会为容器插入一个指定值。back_inserter函数会产生一个效果等同push_back的插入迭代器,需要目标容器支持push_back函数;front_inserter函数会产生一个效果等同push_front的插入迭代器,需要目标容器支持push_front函数;inserter函数会产生一个效果等同insert插入迭代器,需要目标容器支持insert函数。

8 .流迭代器:iostream也具有迭代器istream_iterator和ostream_iterator,其中istream_iterator只能用于读取数据,ostream_iterator只能用于写数据。流迭代器是模板,只要某一类型支持>>就可以定义istream_iterator,支持<<就可以定义ostream_iterator。对istream_iterator使用++就会持续读出数据;对ostream_iterator使用=(赋值)就会向输出流写数据,ostream_iterator支持++但没有什么作用。istream_iterator定义时不提供参数则生成流末尾下一位的迭代器(流结束的eof),ostream_iterator没有末尾可无限输出。

9. 逆序迭代器:逆序迭代器与正常的迭代器正好相反,适合从尾到头访问一个容器。特别需要注意的是,逆序迭代器是右闭合,所以与正常迭代器的转换为:rbegin = end - 1,rend = begin - 1,也可以通过riter.base()来取得相对应的正常迭代器的值。

10. 五种迭代器:迭代器可以分为五种

五种迭代器
名称 支持操作 举例
输入迭代器 == != ++ * ->,单向只读(不可比较大小,不能后退) istream_iterator
输出迭代器 ++ *,单向只写(不可比较大小,不能后退,不能判断相等) ostream_iterator
单向迭代器 == != ++ * ->,单向读写(不可比较大小,不能后退) forward_list
双向迭代器 == != ++ -- * ->,双向读写(不可比较大小) list map set
随机访问迭代器 == != ++ -- * -> + += - -= > >= < <= [],随机读写(全部运算) vector deque string

 

 

 

 




除了输出迭代器之外,其余四个排成一个由低到高的序列,算法要求的迭代器类型由低向高兼容。

11. forward_list和list容器的迭代器不是随机访问迭代器,不可随机访问,所以对于它们使用泛型函数效率很低。在forward_list和list中,定义了很多成员函数来补充这一空缺,比如说sort、unique 、reverse、merge、remove等等,forward_list还特有splice函数实现元素的“搬家”,使用时需要注意。

12. 关联容器分为有序容器(基于红黑树实现)和无序容器(基于hash表实现),包含存储key-value的map(a.k.a. 关联数组)和只存储key的set,map和set分为允许key重复的multimap、multiset和不允许key重复的map、set。2×2×2共计8种:map、set、multimap、multiset、unorder_map、unorder_set、unorder_multimap和unorder_multiset。

13. 有序关联容器的键(key)要求严格弱排序,即:对于任意两个key的对象k1和k2,他们之间要么属于<(小于),要么属于=(等于),不可能存在第三种关系。所以使用迭代器访问有序关联容器中的内容时,是按照key的顺序访问的;但是对于无序关联容器,使用迭代器访问时key是无序的。有序容器默认使用<操作符,我们指定使用自己定义的操作。

class A{...}
bool compareA(const A& a1, const A& a2){...}
multiset<A, decltype(compareA)*> someset(compareA); //ok,使用自定义比较操作

14. map容器的迭代器解引用后得到pair类型,pair类型在utility头文件中定义,pair的两个数据成员(first和second)是public的,可以使用构造函数、makepair调用以及列表初始化来创建一个pair对象。

map<string, int> word_counter;
auto it = word_counter.begin(); //*it的类型是pair<string, int>
auto p1 = make_pair("abc", 3); //ok,make_pair返回pair<string, int>类型
pair<string, int> p2("abc", 3); //ok,直接调用构造函数
pair<string, int> p3 = {"abc", 3}; //ok,列表初始化

15. 关联容器中定义了key_type、mapped_type和value_type三种类型,key_type是关联容器的关键字的类型,mapped_type是map容器中值的类型,value_type是关联容器中保存的元素的类型(set中等于key_type,map中是pair<const key_type, mapped_type>类型)。注意:map的value_type中的关键词都是const的,不允许修改。另外,使用迭代器访问map和set时,不允许修改key内容(对于map只允许修改value,对于set则是完全不能修改)。

16. 下标操作符:对map使用下标操作符时应当填入key值,该操作符返回一个value的左值(mapped_type的引用),如果该key不存在,那么map会自动添加该key-value对,并将value默认初始化。所以,为了避免使用下标操作符导致的插入行为,可以使用count 或者find 函数确定一个key是否存在map中。set容器不支持下标操作符,multimap也不支持下标操作符。

17. 在有序multimap和multiset中,允许key出现多次,同一个key将被连续的存储。使用upper_bound和lower_bound能够获得其中一个key的迭代器范围,使用equal_range也可以达到同样的目的。

multimap<string, string> somemultimap;
........... //为multimap填充数据,省略
//使用lower_bound和upper_bound
for(auto beg=somemultimap.lower_bound(key), end=somemultimap.upper_bound(key); beg!=end; ++beg)
{
    //do some thing
}
//使用equal_range,等价于使用lower_bound和upper_bound
for(auto pos=somemultimap.equal_range(key); pos.first!=pos.second; ++pos.first)
{
    //do some thing
}

18. 无序容器使用hash表实现,一个hash值对应一个桶,相同hash值的元素保存在同一个桶之中。unorder容器提供了一系列桶管理函数,我们可以使用它们提升unorder容器的性能。

19. 无序容器对关键字类型有两个要求:

  • 支持==运算符
  • 具备hash函数:标准库为内置类型、string和智能指针定了hash函数模板,可以直接使用这些类型作为key
如果我们使用自己定义的类类型作为key,需要为它重载==运算符,并且提供一个hash函数。

你可能感兴趣的:(C++ Primer阅读心得(第十章、第十一章))