第1条:慎重选择容器类型
- 不同容器之间的差异很大,要根据需要选择适当的容器;
- 考虑因素:是否与标准相符;迭代器类型(单向,双向,随机);元素布局与c的兼容性;容器内元素是否有序;查找速度;引用计数引起的反常;是否实现事务语义;在何种情况下会使迭代器,引用,指针无效;不同容器的内存分配策略;等等
第2条:不要试图编写独立于容器类型的代码
- 标题的意思是:由于不同的容器之间差异很大,优缺点很明显,因此不要试图去编写适用于所有容器的代码。比如它们所支持的成员函数;支持的迭代器类型;使迭代器,指针,引用无效的操作;是否与c接口兼容;容器的存储对象类型是否是bool;各种操作的耗费时间;
- 若必须从一种容器转到另一种,可以使用封装技术,减少客户代码对于具体容器的使用;
- 对容器类型和其迭代器类型使用typedef
- 把具体使用的容器隐藏到一个类的private中,并且尽可能减少可以访问到此容器的类接口。万一需要修改容器,此时需要查看该类的每个成员函数和友元受到的影响,总之客户代码可以尽量减小修改量;
第3条:确保容器中的对象拷贝正确而高效
- 类的拷贝动作:拷贝构造函数;拷贝赋值操作符
- 可能的出错情况和限制:
- 直接拷贝对象时:容器所含的对象越多,时间和空间成本越高;拷贝包含auto_ptr的容器对象会出问题;存在继承关系时,提供一个derived对象给一个需要基类对象的地方,会导致对象切割;
- 为了避免1,可以拷贝对象的指针,更好的选择是智能指针
Widger w(maxNums);
vector<Widget> v;
v.push_back(W1);
vector<Widget> v;
v.reserve(maxNums);
第4条:调用empty而不是检查size()是否为0
- empty()对于所有标准容器都是O(1)操作;
- size()对于一些list有时候是O(n)操作,有时候是O(1)操作,在不同的STL平台上作者可能有不同的实现;
第5条:区间成员函数优先于与之对应的单元素成员函数
- insert的区间成员函数版本和单元素成员函数版本
vector<int> v1{1, 2, 3};
vector<int> v2{11, 12, 13};
v1.insert(v1.begin(), v2.begin(), v2.end());
vector<int>::iterator it = v1.begin();
for(int i = 0; i < v2.size(); ++i){
it = v1.insert(it, v2[i]);
++it;
}
- 使用insert区间函数版本比重复调用单元素版本的优点:
- 效率(写起来更容易,且不容易出错):
①对于insert函数的调用次数:区间函数只需一次,单元素版本需要多次;通过将函数声明为内联可以避免这种影响;所有顺序容器都适用
②v1中元素需要移动的次数:调用区间函数一次性移动到最终位置,单元素版本只能一步一步的移动,造成了很大的拷贝构造和赋值开销;vector,string,deque适用,list不适用(因为list无需移动元素,只需修改指针的值)
③可以避免更多的重新分配内存的出现:当v1内存满了,再向其中insert新元素,会有几步:开辟新内存,拷贝旧元素到新内存,销毁旧元素,释放旧内存,将新元素插入新内存的正确位置处;若只调用一次区间函数,则一开始就知道需要多大内存,会一次性分配够,防止多次开辟新内存的情况出现;vector和string适用,deque和list由于管理内存的方式不同,因此不会出现这种情况
注:对于关联容器,选择区间函数的效率一定不会比重复调用单元素函数差; - 更易懂,能简洁的表达意图
- 增强软件的可维护性
container::container(InputIterator begin, InputIterator end);
void container::insert(iterator pos, InputIterator begin, InputIterator end);
iterator container::erase(iterator begin, iterator end);
void container::erase(iterator begin, iterator end);
注:erase操作不会在内存中元素数量少时自动缩小内存
void container::assign(InputIterator begin, InputIterator end);
第6条:当心C++编译器最烦人的分析机制
- C++的一个普遍规律:尽可能的解释为函数声明;因此需要时刻注意自己写的表达式是否有二义性;
int f(double (d));
int f(double d);
int f(double);
int g(double (*p)());
int g(double p());
int g(double ());
class Widget{};
Widget w();
Widget w;
ifstream datafile("ints.dat");
list<int> data(istream_iterator<int>(datafile), istream_iterator<int>());
istream_iterator<int> dataBegin(datafile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
第7条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
- 如果容器中的对象包含了new创建的指针,要在容器对象析构前将指针delete掉,否则容器仅会将指针本身析构,而不会析构这些指针指向的那些动态分配的内存;
1:
void doSomething(){
vector<Widget*> v;
v.reverse(10);
for(int i = 0; i < 10; ++i){
v.push_back(new Widget);
}
...
措施1:手写循环,依次delete(可以用算法for_each代替)
for(vector<Widget*>::iterator it = v.begin(); it != v.end(); ++it){
delete *it;
}
}
2:
template<typename T>
struct DeleteObjet : public unary_function<const T*, void>{
void operator()(const T* ptr)const{
delete ptr;
}
}
void doSomething(){
vector<Widget*> v;
v.reverse(10);
for(int i = 0; i < 10; ++i){
v.push_back(new Widget);
}
...
措施2:for_each
for_each(v.begin(), v.end(), DeleteObject<Widget>());
}
3.
struct DeleteObjet{
template<typename T>
void operator()(const T* ptr)const{
delete ptr;
}
}
void doSomething(){
deque<SpecialString*> v;
v.reverse(10);
for(int i = 0; i < 10; ++i){
v.push_back(new SpecialString);
}
...
措施3:改进版的for_each
for_each(v.begin(), v.end(), DeleteObject<string>());
}
4.
void doSomething(){
措施4best practice:使用带有引用计数的shared_ptr的自动析构的优点;解决了前面几种措施中在new和delete中间可能抛出异常的问题
shared_ptr<Widget> spW;
vector<spW> v;
v.reverse(10);
for(int i = 0; i < 10; ++i){
v.push_back(spW(new Widget));
}
}
第8条:切勿创建包含auto_ptr的容器对象
- 包含auto_ptr的容器(COAP)被C++标准禁止
- auto_ptr独占对所指对象的所有权
auto_ptr<Widegt> p1(new Widget);
auto_ptr<Widegt> p2 = p1;
p1 = p2;
- 若容器中存储auto_ptr,在进行一些赋值,交换等等操作时,会产生非预期效果,使容器中的若干auto_ptr置为NULL
- 容器中可以使用shared_ptr等其它智能指针,但一定不要包含auto_ptr!
第9条:慎重选择删除元素的方法
container<int> c;
c.erase(remove(c.begin(), c.end()), c.end());
c.remove(1963);
c.erase(1963);
container<int> c;
bool f(int){...}
erase(remove_if(c.begin(), c.end(), f), c.end());
c.remove_if(f);
1.
container<int> temp;
remove_copy_if(c.begin(), c.end(), inserter(temp, temp.end()), f);
c.swap(temp);
2.注意:对于关联容器调用erase,要对传入erase参数的迭代器进行后缀递增
for(container<int>::iterator it = c.begin(); it != c.end(); ){
if(f(*it)) c.erase(it++);
else it++;
}
- 若对顺序容器通过手写循环调用erase:要用erase的返回值更新迭代器it
若对关联容器通过手写循环调用erase:要把传给erase的迭代器参数作后缀递增
for(container<int>::iterator it = c.begin(); it != c.end(); ){
if(f(*it)) it = c.erase(it);
else it++;
}
第10条:了解分配子(allocator)的约定和限制
第11条:理解自定义分配子的合理用法
第12条:切勿对STL容器的线程安全性有不切实际的依赖
- STL容器在多线程环境下,有的STL实现支持,有的不支持,最多最多只能奢望以下两点,其余关于多线程安全性的问题只能手动来,不能指望STL自带
①多个线程同时读一个同容器的内容,但不能对该容器同时写入
②多个线程可以同时对不同的容器写入