c++中stl不是面向对象编程,而是提供一种新的方式:泛型编程。当然这个话题说起来几天几夜也说不完,然而如果对于stl有一个一般的理解,比如可以使用标准容器对数据进行存储,然后可以对标准容器进行简单的增删查改,知道stl中的大致algorithm,并且纠结于平时如何使用stl可以更加高效、易懂、安全,那么这篇文章所介绍的《Effective STL》适合学习。不过这并不是一本入门的书。文章对作者的中心观点进行总结,但是具体内容如果不清楚还是应该找到相应的章节进行详细阅读。
条款1:仔细选择你的容器
- 你需要“可以在容器的任意位置插入一个新元素”的能力吗? 选择序列容器,关联容器做不到。
- 你关心元素在容器中的顺序吗? 如果不,散列容器就是可行的选择。否则,你要避免使用散列容器
- 必须使用标准C++中的容器吗? 如果是,就可以除去散列容器、slist和rope
- 如果必须使用随机访问迭代器: 就只能限于vector、deque和string;
- 不想插入删除时影响其他数据? 如果是,你就必须放弃连续内存容器
- 容器中的数据的内存布局需要兼容C吗? 如果是,你就只能用vector
- 查找速度很重要吗? 如果是,你就应该看看散列容器,排序的vector和标准的关联容器
- 介意如果容器的底层使用了引用计数吗? 如果是,应将string类型换成vector<char>
- 你需要插入和删除的事务性语义(回退插入删除)吗? 如果是,你就需要使用基于节点的容器
- 迭代器,指针失效最少: 如果是,你就需要使用基于节点的容器
- 随机访问+无删除操作+尾部插入+10 如果是,deque
条款2:小心对“容器无关代码”的幻想
数组泛化为容器,参数化了所包含的对象的类型。函数泛化为算法,参数化了所用的迭代器的类型。指针泛化为迭代器,参数化了所指向的对象的类型。很多程序员在写自己的容器、迭代器和算法时,试图继续泛化,但是很多成员函数只存在于其中一类容器中。不同的序列容器所对应的不同的迭代器、指针和引用的失效规则是阻碍这的源头。实际上让容器无关算法虽然出发点很好,但是几乎是无用功。一种最简单的方法是通过自由地对容器和迭代器类型使用typedef。如果你不想暴露出用户对你所决定使用的容器的类型,你需要更大的火力,那就是class。要限制如果用一个容器类型替换了另一个容器可能需要修改的代码,就需要在类中隐藏那个容器,而且要通过类的接口限制容器特殊信息可见性的数量。如果你做好了对class地实现细节做好封装的话,那改变class中容器类型对class的使用的影响将会很小。
条款3:使容器里对象的拷贝操作轻量而正确
一个使拷贝更高效、正确而且对分割问题免疫的简单的方式是建立指针的容器而不是对象的容器。拷贝指针很快,它总是严密地做你希望的(指针拷贝比特),而且当指针拷贝时没有分割。智能指针时另一种好的方式。
分割问题:由于继承的存在,拷贝会导致分割。那就是说,如果你以基类对象建立一个容器,而你试图插入派生类对象,那么当对象(通过基类的拷贝构造函数)拷入容器的时候对象的派生部分会被删除。
条款4:用empty来代替检查size()是否为0
对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现,size花费线性时间。
条款5:尽量使用区间成员函数代替它们的单元素兄弟
给定两个vector,v1和v2,使v1的内容和v2的后半部分一样的最简单方式是什么?
1 v1.assign(v2.begin() + v2.size() / 2, v2.end());
其他实现方式1:
1 vector<Widget> v1, v2; // 假设v1和v2是Widget的vector 2 v1.clear(); 3 for (vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2; 4 ci != v2.end(); 5 ++ci) 6 v1.push_back(*ci);
其他实现方式2:
1 v1.clear(); 2 copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
着眼于显式循环可以更容易地了解效率冲击都在哪里, 三种不同的性能税:1. 把numValues个元素插入v,每次一个,自然会花费你numValues次调用insert。使用insert的区间形式,你只要花费一次调用,节省了numValues-1次调用。2. 把numValues个新对象每次一个地插入容纳了n个元素的vector<Widget>的前部需要花费n*numValues次函数调用:(n-1)*numValues调用Widget赋值操作符和numValues调用Widget拷贝构造函数,标准要求区间insert函数直接把现有元素移动到它们最后的位置,也就是,开销是每个元素一次移动。总共开销是n次移动,numValues次容器中的对象类型的拷贝构造函数,剩下的是类型的赋值操作符。 3. 重复使用单元素插入而不是一个区间插入就必须处理内存分配,插入numValues个新元素会导致最多log2numValues次新内存的分配。一个区间插入可以在开始插入东西前计算出需要多少新内存(假设给的是前向迭代器),所以它不用多于一次地重新分配vector的内在内存。
条款6:警惕C++最令人恼怒的解析
假设你有一个int的文件,你想要把那些int拷贝到一个list中,
1 ifstream dataFile("ints.dat"); 2 list<int> data(istream_iterator<int>(dataFile), // 警告!这完成的并不 3 istream_iterator<int>()); // 是像你想象的那样
这声明了一个函数data,它的返回类型是list<int>。这个函数data带有两个参数:
- 第一个参数叫做dataFile。它的类型是istream_iterator<int>。dataFile左右的括号是多余的而且被忽略。
- 第二个参数没有名字。它的类型是指向一个没有参数而且返回istream_iterator<int>的函数的指针。
学会识别这个失言(faux pas)是成为C++程序员的一个真正的通过仪式。修改方式是退一步,即不使用以上代码中的匿名迭代器而使用明明迭代器:
1 ifstream dataFile("ints.dat"); 2 istream_iterator<int> dataBegin(dataFile); 3 istream_iterator<int> dataEnd; 4 list<int> data(dataBegin, dataEnd);
条款7:使用容器中new的指针,应在容器销毁前释放(书中标题:当使用new得指针的容器时,记得在销毁容器前delete那些指针)
void doSomething() { vector<Widget*> vwp; for (int i = 0; i < SOME_MAGIC_NUMBER; ++i) vwp.push_back(new Widget); ... // 使用vwp } // Widgets在这里泄漏!
通常删除操作可以在后续手动执行,比如这样:
1 void doSomething() 2 { 3 vector<Widget*> vwp; 4 ... // 同上 5 for (vector<Widget*>::iterator i = vwp.begin(); 6 i != vwp.end(), 7 ++i) { 8 delete *i; 9 } 10 }
不过这段代码不是异常安全的。如果在用指针填充了vwp和你要删除它们之间抛出了一个异常,你会再次资源泄漏。不过可以用for_each方法来解决这个问题,而且for_each比for代码少的多。
template<typename T> struct DeleteObject : // 条款40描述了为什么 public unary_function<const T*, void> { // 这里有这个继承 void operator()(const T* ptr) const { delete ptr; } }; void doSomething() { ... // 同上 for_each(vwp.begin(), vwp.end(), DeleteObject<Widget>); }
但是仍然有需要警惕的事情, 那就是你指定了DeleteObject将会删除的对象的类型,比如,有的人恶意地故意从string继承,所有的标准STL容器,缺少虚析构函数,而从没有虚析构函数的类公有继承是一个大的C++禁忌。for_each删除时将删除string*指针。解决方法是有的:
1 struct DeleteObject { // 删除这里的 2 // 模板化和基类 3 template<typename T> // 模板化加在这里 4 void operator()(const T* ptr) const 5 { 6 delete ptr; 7 } 8 };
但仍不是异常安全的。如果在SpecialString被new但在调用for_each之前抛出一个异常,就会发生泄漏。所以相对较好的方法就是写一个智能指针
1 void doSomething() 2 { 3 typedef std::shared_ ptr<Widget> SPW; //SPW = "shared_ptr(原为boost:shared_ptr) 4 // to Widget" 5 vector<SPW> vwp; 6 for (int i = 0; i < SOME_MAGIC_NUMBER; ++i) 7 vwp.push_back(SPW(new Widget)); // 从一个Widget建立SPW, 8 // 然后进行一次push_back 9 ... // 使用vwp 10 } // 这里没有Widget泄漏,甚至 11 // 在上面代码中抛出异常
条款8:永不建立auto_ptr的容器
当你拷贝一个auto_ptr时,auto_ptr所指向对象的所有权被转移到拷贝的auto_ptr,而被拷贝的auto_ptr被设为NULL。你正确地说一遍:拷贝一个auto_ptr将改变它的值:
1 auto_ptr<Widget> pw1(new Widget); // pw1指向一个Widget 2 auto_ptr<Widget> pw2(pw1); // pw2指向pw1的Widget; 3 // pw1被设为NULL。(Widget的 4 // 所有权从pw1转移到pw2。) 5 pw1 = pw2; // pw1现在再次指向Widget; 6 // pw2被设为NULL
条款9:在删除选项中仔细选择
1 c.erase(remove(c.begin(), c.end(), 1963), c.end());// 当c是vector、string 2 // 或deque时,erase-remove惯用法是去除特定值的元素的最佳方法 3 c.remove(1963); // 当c是list时, 4 // remove成员函数是去除 5 // 特定值的元素的最佳方法 6 c.erase(1963); // 当c是标准关联容器时 7 // erase成员函数是去除 8 // 特定值的元素的最佳方法
bool badValue(int x); // 返回x是否是“bad” c.erase(remove_if(c.begin(), c.end(), badValue),c.end()); // 当c是 //vector、string或deque时这是去掉badValue返回真的对象的最佳方法 c.remove_if(badValue); // 当c是list时这是去掉 // badValue返回真的对象的最佳方法 AssocContainer<int> c; // c现在是一种 ... // 标准关联容器 AssocContainer<int> goodValues; // 用于容纳不删除 // 的值的临时容器 remove_copy_if(c.begin(), c.end(), // 从c拷贝不删除 inserter(goodValues, // 的值到 goodValues.end()), // goodValues badValue); c.swap(goodValues); // 交换c和goodValues的内容
在循环内做某些事情(除了删除对象之外):
如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你的迭代器。
如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。
条款10:注意分配器的协定和约束
如果你想要写自定义分配器,让我们总结你需要记得的事情。
- 把你的分配器做成一个模板,带有模板参数T,代表你要分配内存的对象类型。
- 提供pointer和reference的typedef,但是总是让pointer是T*,reference是T&。
- 决不要给你的分配器每对象状态。通常,分配器不能有非静态的数据成员。
- 记得应该传给分配器的allocate成员函数需要分配的对象个数而不是字节数。也应该记得这些函数返回T*指针(通过pointer typedef),即使还没有T对象被构造。
- 一定要提供标准容器依赖的内嵌rebind模板
TBC