读了《Effective STL》,记录一下。
条款1:仔细选择你的容器
- 标准STL序列容器:vector、string、deque和list。
- 标准STL关联容器:set、multiset、map和multimap。
- 连续内存容器在一个或多个(动态分配)的内存块中保存它们的元素。如果一个新元素被查入或者已存元素被删除,其他在同一个内存块的元素就必须向上或者向下移动来为新元素提供空间或者填充原来被删除的元素所占的空间。这种移动影响了效率(参见条款5和14)和异常安全(就像我们将会看到的)。标准的连续内存容器是vector、string和deque。
- 基于节点的容器在每个内存块(动态分配)中只保存一个元素。容器元素的插入或删除只影响指向节点的指针,而不是节点自己的内容。所以当有东西插入或删除时,元素值不需要移动。表现为链表的容器,比如list,是基于节点的,所有的标准关联容器set、multiset、map和multimap也是(它们的典型实现是平衡树)。
条款2:不要试图编写“容器无关代码”
- 编写独立于容器的通用代码往往会出问题,容器之间有很多不同点,这让你只能使用他们功能的交集。
- 如果你需要有拓展性:有可能会更改容器,那么将这个类设计出来,将容器隐藏于这个类内。例如你要用list来保存顾客信息,那么创建一个customer_list类。这样当你换容器时只需要更改这个类,而不是每一个容器实例。
条款3:确保容器中的对象的拷贝操作正确且高效
- 拷贝是STL的工作方式。
- 大多数元素的添加,插入、remove等操作带来的元素移动等都会使用拷贝操作。通常是拷贝构造函数和赋值操作符。
- 如果拷贝操作符效率低,那么容器操作也会陷入性能瓶颈。
条款4:使用empty()而不是检查size()是否为0
- 对于其他标准容器,这俩一样。但是对于list,size()函数可能花费线性时间,而empty()则花费常数时间。
- 所以再任何场合,使用empty()是没错的。
条款5:区间成员函数 优先于 单元素成员函数
-
给定两个vector,v1和v2,使v1的内容和v2的后半部分一样的最简单方式是什么?
v1.assign(v2.begin() + v2.size() / 2, v2.end());
答案如上。assign()是赋值函数。标准序列容器都有,需要完全替换一个容器的内容时,可以用。
-
也可以使用copy算法:
v1.clear(); copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
-
但所有利用插入迭代器来限定目标区间的copy调用都可以且应该使用区间insert()成员函数来替换:
v1.clear(); v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
-
这个问题还可以用一组循环来实现,但效率上就差多了。
-
分析:基于copy的代码与循环类似,都不好。
- 首先,在于不必要的函数调用,需要将元素逐个插入。
- 第二在于频繁插入带来的元素移动,而区间insert只移动一次。
- 第三是内存分配上的问题。新元素的插入可能空间不足而重新分配,重新分配则会带来元素拷贝的问题。最多可能导致log2(numVlaues)次分配,每次都要整体挪动。而区间insert()则可以一次到位。
-
区间函数有: ** **
条款6: 警惕C++的解析
- 有些操作可能会被当做函数定义,比如是传一对istream_iterator给list的区间构造函数,把int从文件拷贝到list中:
但其实会被当做函数声明。ifstream dataFile("ints.dat"); list<int> data(istream_iterator<int>(dataFile), // 警告!这完成的并不 istream_iterator<int>()); // 是像你想象的那样
- 所以尽量一步步操作。
条款7: 如果容器中有new的指针,记得容器析构前delete
- new创建的元素,delete是程序员的责任。
- for循环delete的另一种更好的做法是: for_each()函数。需要将delete变成一个函数对象传入。
- 以上还没注意到异常安全问题,出现异常还是会泄露内存。那么可以采用智能指针的方式。注意可以采用shared_ptr。
条款8: 切勿将auto_prt作为容器元素
- auto_prt在拷贝时,会将元素转移到新的智能指针中,而将老的变为nullptr。
- 而在容器中,拷贝是常见操作,当一些排序等算法中复制拷贝时,可能会让容器中的值改变,这是不对的。所以不要使用就是解决办法。
条款9:慎重选择删除元素的方式
-
对于vector、string、deque连续内存序列容器:采用erase-remove方式进行删除。
-
remove()函数不会改变成员,只会将指定元素放在序列末尾,然后返回元素第一个的迭代器。那么需要调用成员函数erase来将这个迭代器直到end的元素进行删除。
c.erase(remove(c.begin(), c.end(), 1963), // 当c是vector、string、或deque c.end());
-
对于list: 上述也适用,但是采用remove成员函数进行删除更好。
-
而对于标准关联容器,没有remove这个操作。而是erase成员函数。
-
假如需要手动写循环单个删除。
-
对于序列容器,只需要循环,并且erase(i++)即可:
for (AssocContainer<int>::iterator i = c.begin(); i != c.end();){ / if (badValue(*i)) c.erase(i++); // 对于坏的值,把当前的 else ++i; // i传给erase,然后 }
-
但是对于序列容器vector,deque,string,erase操作不仅使得迭代器失效,也会使得 i 之后的所有迭代器都失效,则 i++ 也不行。但是erase()函数会返回删除元素的下一个有效迭代器。所以操作如下:
for (SeqContainer<int>::iterator i = c.begin();i != c.end(); ) { if (badValue(*i)){ logFile << "Erasing " << *i << '\n'; i = c.erase(i); // 通过把erase的返回值 } // 赋给i来保持i有效 else ++i; }
-