条款一
谨慎选择你的容器
简单地说:
array 不可变序列数组
vector 可变序列数组
string 字符串
list 含头结点的双向链表
deque 可双头访问的逻辑上线性的序列
forward_list 单向链表
条款二
不要尝试编写容器类型无关的代码
原因是没有真的统一的接口,每个容器的实现基本没有什么相似之处,统一拥有的接口少之又少,与其绞尽脑汁写出通用代码,倒不如为不同的容器准备不同的版本以优化各个实现。
(1)使用类型定义来进行语意上的封装,避免修改过多的类型定义。
(2)可以用一个类来包装你的底层容器,提供相应的接口给客户即可,隐藏实现细节。
条款三
确保容器中的对象拷贝正确且高效
容器中存入取出一个对象均是使用了拷贝的行为,拷贝一个对象的副本传入容器或是取出一个容器内对象的副本来传给调用者,故可以想象把派生类传递给装载基类的容器时会发生裁切,介于容器无法存储引用类型,所以想要产生多态则需要指针,智能指针。
创建容器可以只指定大小而不初始化对象。
而内置数组则在创建时就已经构造了同等数目的默认初始化对象。
条款四
使用empty()而不是size == 0来判定容器是否为空
因为empty永远是常数级的,而某些节点容器的size是线性实际的,因为要一个一个遍历节点。也就是说list是没有用一个数据成员来存放容器内元素数量的。这一点是为了范围插入/删除时的常数时间操作,若保持一个size数据成员,则不可避免得需要在范围插入删除时遍历来计算范围内的节点数量以 增加/减少 size,所以不维护这样一个size。那么empty则需要另一种手段来检测是否拥有元素,方式是检测头结点是否为空。
条款五
区间成员函数由于相应的单元素成员函数,也优于对应的非成员函数
单一成员函数加自写循环造成的影响:
1、多余的函数调用,每个循环都需要调用单元素成员函数,而区间函数只需要一次调用。
2、多余的元素移动,对于vector,string等等容器来说,每插入一个元素都需要把之后的元素全部后移一位,而区间版本只需要统一进行一次移动即可。对于链接型容器,则是需要多次更改链接值。
3、多余的内存分配,对于某些容器,插入/删除元素等操作是需要改变容器容量的,也就是说当容量不足时需要申请新的内存,然后把旧空间上的全部元素移动(在c++11之前只能复制)到新的地址,然后释放原内存;故多次的单元素成员函数调用可能会引起多次这样的操作,区间函数则一步到位。
4、语法繁杂,边界检测,迭代器值更新等需要自己来做,这一点非常容易出错,基于这一点,区间的非成员函数都要比自写循环更优,简而言之有系统实现的就用系统实现的,有区间就用区间而不用单个。虽然stl算法也是基于一般的循环加上成员函数调用来编写的,但是stl算法进行了许多优化并且语意清晰,符合一般编程人员的直觉。
区间函数种类:
1、区间创建(begin, end);
2、区间插入(position, begin, end);
对于关联容器(begin, end)//你不需要指定位置,因为关联容器有自己的排列方式
3、区间删除(begin, end);
一般容器: iterator erase(begin, end);
关联容器: void erase(begin, end);
4、区间赋值(begin, end,)
容器.assign(begin, end); //把容器的值换为begin ~ end
条款六
当心分析机制
type object( t1(), t2() );//这是一句声明,使用t1, t2的默认初始化临时对象来初始化一个type型的object
然而这一句将被解析为函数,即object变成了一个:返回type,接受t1,t2为参数的函数。
因为t1,t2后的小括号会被系统忽略,视为t1,t2类型的无名参数(当然,如果你使用有参数版本来构造t1,t2就没有这问题),
这样想,type object()声明了一个函数(默认初始化是type object;即没有小括号),而包含形参名的括号可以省去例如:
void fun( int (i), int (i2) ){...}
这里的两个括号都可以省略,所以开头的object的两个括号可以省略也是情理之中(即使你不打算让它们被省去),要解决分析机制,有两种做法。
type object((t1()),(t2()));//在t1(),t2()外面再加一层括号,现在编译器要对它们整个求值,那么只能解释成对象
type object( t1{}, t2{} );
//使用大括号来进行默认初始化,不必担心你定义了initializer_list<T>版本的构造函数,因为空的{}优先采用默认初始化
条款七
若容器包含内置指针,记得析构它们
首先我要提出,别用内置指针,用智能指针,当然,你还会看到我提很多次(因为effective stl是在没有真的智能指针时写的)
这一条的原因很简单,容器会析构存放其中的元素--通过使用它们的析构函数,显然容器里的指针和放在其他类中的内置指针一样,不会被自动释放,所以你需要手动delete(这真的很麻烦)。
其他:
(1)考虑把模板类改成一个拥有模板函数的普通类,特别是当你使用一个仿函数的时候,一个模板型的operator()可能会比模板型的仿函数好用的多(最起码你减少了自己写出类型的麻烦,因为拥有了自动类型推断)。
(2)用智能指针代替内置指针,甚至是自写的RAII类来代替内置指针。
条款八
切勿包含auto_ptr的容器
这一条是一条过时的注意,或者说被弃用的,因为我们有了unique_ptr和移动操作。
那么还是说说原因,auto_ptr不具有拷贝构造/赋值成员的(或者说,按照原来的设计,它们是private的,这导致你无法使用它们),然而所有容器都是基于复制(或赋值)来完成功能的,也就是说你不能把任何成员存进去,更用不了许多stl算法。
当然,现在你可以使用unique_ptr的容器,并不是说unique_ptr的拷贝构造/拷贝赋值符可用,而是它的移动成员可用,stl算法也可以切换到其移动版本,同样你也可以把右值unique_ptr存入容器,所以这又是一个使用智能指针的理由。
条款九
慎重选择删除元素的方法
erase - remove方法:
使用remove( begin, end, v);来得到一个应当删除的元素范围的开头
然后erase( remove(begin, end, v), end);//移除这个开头到容器尾的全部元素
remove做的是把需要留下的元素全部保持原序地移动到容器开始的一段区间,然后返回一个迭代器指向这个区间后一个位置,即从这个位置开始到结尾都是按照你的规定需要删除的元素。当然,remove并没有把不需要的元素移动到尾部一段区间,而是用需要的元素值去覆盖这些不需要的元素的位置,所以结尾这一段元素的值倒地是什么无法保证,你要做的就是把它们删除。
举例:
定值删除:
容器.erase( remove(begin, end, v), end);//一般序列容器
list.remove(v);
关联容器.erase(k);//基于红黑树,对数时间,无序容器则可能是常数时间,k为key
条件删除
容器.erase(remove_if(begin, end, 条件), end);
list.remove_if(条件);
关联容器:
(1)使用remove_copy_if(begin, end, result, 条件);
remove_copy_if会把不符合条件的元素拷贝到新容器,仿佛很不符合逻辑,但介于remove_if是把符合条件的剔出,所以按照这个寓意,不符合条件的需要被留下,或被拷贝到新的容器。
所以构造一个临时容器temp,把原容器orginal
remove_copy_if(orginal.begin(), orginal.end(), temp.begin, 条件); //选出符合条件的到temp
orginal.swap( temp ); //交换两容器
大功告成。
(2)自写循环(不是什么好主意)
略。
条款十
了解分配子的约束
allocater<T>用以分配stl容器的内存,它拥有
using value_type = _Ty;
using size_type = typename _Mybase::size_type;
using difference_type = typename _Mybase::difference_type;
using pointer = typename _Mybase::pointer;
using const_pointer = typename _Mybase::const_pointer;
using reference = _Ty&;
using const_reference = const _Ty&;
我们只看等号左边,value_type代表元素类型T,pointer表示存放的元素指针类型T *,reference则是T &,size_type代表表示容器大小的数值类型,difference_type是用来存放两个指针间距离的数值类型(有符号)。
另外我们要意识到一点:一个容器<T>的allocater并不是分配一个T的空间,因为每个容器的“单位”不同,节点容器需要的是分配一个节点,某些表格形态的容器需要的是“一行”,等等。
条款十一
分配子的合理用法
分配子是用来管理容器分配内存的方式(不是多少,或分配怎样的结构,这些你无需过问),故可以通过改写它来把所有的成员放到同不同的堆,或把他们连续存放(如果你知道怎么做的话)。
条款十二
别过于依赖stl的线程安全性
简单说,你能依靠的只有:同时读取同一容器
同时写入不同容器
别的需要你自己进行线程管理,若是想要stl在设计上就线程安全则需要:
(1)锁住容器直到成员函数结束
(2)锁住容器直到迭代器生存期结束
(3)锁住容器直到任何stl算法结束(介于算法无关容器类型,你几乎不可能做到这点)
以上,你应该手动上锁等。