Effective STL 读书笔记

来源:互联网 发布:雅思阅读 杂志 知乎 编辑:程序博客网 时间:2024/06/06 23:44

第一章容器

条款1:仔细选择你的容器

C++中各种标准或非标容器:

  • 标准STL序列容器:    vector、string、deque和list(双向列表)。
  • 标准STL管理容器:    set、multiset、map和multimap。
  • 非标准STL序列容器: slist(单向列表)和rope(重型字符串?)。
  • 非标准STL关联容器: hash_set、hash_multiset、hash_map和hash_multimap。(c++11引入了unordered_set、unordered_multiset、unordered_map和unordered_multimap,其亦基于hash表,但属于最新的标准关联容器,所以相对hash_*拥有更高的效率和更好的安全性)
  • 标准非STL容器:        数组(c++11中有新的array标准)、bitset、valarray(用于数值计算,但是一般很少使用)、stack、queue和priority_queue(常用于模拟最大堆和最小堆)
  • 其他:                          vector<char>在某些情况下可以替换string, vector在某些情况下可以替换标准关联容器。

如何选择容器?需要考虑一下几点:是否要求内存连续(随机访问)?是否有频繁的插入/删除?是否要求容器内元素有序?是否有查找速度的要求?本书作者认为没有一个默认容器。(而《c++ primer》认为如果没有非常正当的理由,就应该选vector)

条款2:小心对“容器无关代码”(container-independent code)的幻想

STL是建立在泛型的基础上,但由于不同容器的特性不同(尤其是迭代器、指针和引用的类型与失效规则不同),支持所有容器的相同接口是不存在的。比如:

  • 只有序列容器支持: push_front和push_back,
  • 只有关联容器支持: logN时间复杂度的lower_bound、upper_bound和equal_range;
如果在工程中需要跟换容器类型,可以通过用typedef来减少跟换所带来的代码跟新量。
class Widget { ... };typedef vector<Widget> WidgetContainer;typedef WidgetContainer::iterator WCIterator;WidgetContainer cw;Widget bestWidget;...WCIterator i = find(cw.begin(), cw.end(), bestWidget);
同样,typedef可以用来简化一个经常被运用到的容器的定义,并减少维护的成本。但是typedef只是其他类型的同义字,
如果不想暴露所使用的容器类型,可以将所用的容器封装到一个class中。可以在这个class实现额外的功能,并对原始容器的操作进行封装。

条款3:使容器里对象的拷贝操作轻量而正确

容器内元素的改变、扩充或删除总是伴随着拷贝。如果将一个对象插入容器中,实际上是将这个对象拷贝进这个容器。拷贝是基于此对象对应class的拷贝构造函数和拷贝赋值操作符。

那么问题来了,如果对于某个class来说,他的拷贝非常昂贵,那么拷贝和可能成为容器的瓶颈。此外,拷贝还有可能带来分割的问题,若以基类建立容器,而插入派生类,那么派生部分会被切割。

解决这一问题的一个办法是建立指针的容器,尤其是智能指针的容器。

当然,STL容器在设计时,已经避免了绝大多数无谓的拷贝,相比内置数组,STL vector显然效率更高。

条款4:用empty来代替检查size()是否为0

对于一般的容器,例如vector,v.size()==0 与c.empty()等价。但是对于list来说,l.size()的时间复杂度为o(N)。

理论上,list每次插入元素时,可以跟新其size,这样能够保证l.size()的时间复杂度为o(1)。但list的特殊之处在于splice()成员函数,splice()可以将一个list插入到另一个list中,并且在o(1)内完成。如果需要在调用splice()时跟新size的值,唯一的方法是遍历待插入的list,并计算其长度。换而言之,l.size()和l.splice()之中,只能有一个为o(1)。而在大多数STL的实现中,前者时间复杂度为o(N),而后者时间复杂度为o(1)。

而对于list的empty()的实现,其源代码为

bool empty() const {return node->next == node;}
list的本质为双向循环列表,其尾部(或称为头部)有一个空白节点。当list为空时,此空白节点的next指向其本身。
slist与list类似,其size()也为o(N),empty()为o(1)。slist的empty()是判断空白头节点的next是否为空。

条款5:尽量使用区间成员函数代替他们的单元素兄弟

典型的区间成员函数如下

//区间构造container::container(InputIterator begin, // 区间的起点InputIterator end); // 区间的终点//区间插入void container::insert(iterator position, // 区间插入的位置(对于关联容器,无需position参数)InputIterator begin, // 插入区间的起点InputIterator end); // 插入区间的终点//区间删除iterator container::erase(iterator begin, iterator end);//序列容器返回iterator,关联容器返回void//区间赋值void container::assign(InputIterator begin, InputIterator end);
单元素变量操作指运用循环,将单个元素一一进行的操作,这类操作主要是插入(insert,push_front,push_back)和删除操作(erase)。值得注意的是如下copy操作虽然看起来是
区间操作,其本质上还是单元素操作。

copy(data, data + numValues, inserter(v, v.begin()));
使用区间操作的好处是什么呢?以insert函数为例:一、减少函数调用,区间操作只需调用一次insert(),而单元素操作需要调用N次insert();二、对于内存连续型容器,每次插入时,需要移动后面的所有元素。所以区间操作只需移动一次,二单元素操作需要移动N次(list型不需要移动元素,但需要设置prev和next指针,也会造成类似的效率影响);三、对于vector和string,当所分配内存已满,需要重新分配内存并移动容器内已有元素。区间操作会实现计算所需内存,只需最多一次的内存重新分配和元素移动,而但单元素操作需要logN次内存重新分配和元素移动。

条款6:警惕C++最令人恼怒的解析

假设有一个int的文件,我们需要将这些int拷贝到一个list中,那么和可能写出如下list的构造形式。

ifstream dataFile("ints.dat");list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>()); 
事实上,这声明了一个函数data,他的返回类型是list<int>。这个函数data带有两个参数:

  • 第一个参数叫做dataFile,它的类型是istream_iterator<int>。
  • 第二个参数没有名字,它的类型是指向一个没有参数且返回值istream_iterator<int>的函数指针。

我们平时还可能遇到如下类似的错误。

class Widget{...}; //假设Widget有默认构造函数Widget w();    //这里实际上是声明了一个函数,而不是实例化一个对象。
那么,我应该用如下方式构造这个list。
ifstream dataFile("ints.dat");istream_iterator<int> dataBegin(dataFile);istream_iterator<int> dataEnd;list<int> data(dataBegin, dataEnd);

条款7:当使用new得指针的容器时,记得在销毁容器钱delete那些指针

STL的容器在自己被销毁时,会自动销毁其容纳的每一个对象。但是当其容纳的对象是指针时,容器并不能正确销毁指针所指向的对象,因为指针的析构函数是无操作。

面对这种容器时,最直观的做法是,在容器销毁前,用for循环遍历容器的每个元素(即指针),并用delete。这样做有两个缺点:一是相比for_each,代码量多得多;二是非异常安全,即当异常抛出在delete掉所有容器内指针指向的内存之前时,会造成内存泄露。

void doSomething(){  vector<Widget*> vwp;  for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)    vwp.push_back(new Widget);  ...  for (vector<Widget*>::iterator i = vwp.begin(); i != vwp.end(); ++i) {    delete *i;  }}
如果需要用for_each来完成这一操作,我们需要将delete转入一个函数对象中。
struct DeleteObject {   template<typename T>   void operator()(const T* ptr) const  {    delete ptr;  }}void doSomething(){  deque<SpecialString*> dssp;  ...  for_each(dssp.begin(), dssp.end(), DeleteObject());}
这样虽然简洁很多,但是依然不是异常安全的。要解决异常安全,一个最好的方法是使用智能指针shared_ptr。
void doSomething(){  typedef boost::shared_ ptr<Widget> SPW;   vector<SPW> vwp;  for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)    vwp.push_back(SPW(new Widget)); // 从一个Widget建立SPW, 然后进行一次push_back... // 使用vwp} 

条款8:永不建立auto_ptr的容器

建立一个shared_ptr的容器时令人愉悦的,但是建立一个auto_ptr的容器是不可行的。

主要原因是,当拷贝一个auto_ptr时,auto_ptr所指向对象的所有权会被转移到拷贝的auto_ptr,二被拷贝的auto_ptr将被设为NULL。这将导致容器操作过程中,其所包含的元素会被意外地改变,导致未定义的结果。幸运的是,当你尝试建立一个auto_ptr的容器时,一般都会在编译阶段报错。

条款9:在删除选项中仔细选择

假设有一个包含int类型的容器 “container<int> c;”,这个容器可以使vector、deque、string、list、set、multiset、map或multimap。

而我们想删除c中所有值为1963的元素,而我们总是希望能有一个同一的操作,适用所有类型的容器。然而,并没有这样一个大一统的操作。

如果这是一个连续内存的容器(vector、deque或string)那么最好的办法是使用erase-remove操作。在begin()到end()的范围内,remove先找到第一个等于1963的元素,将后面的值往前移动,并跳过所有等于关键值得元素。在remove操作结束之后,容器的size并没有变化,需要调用erase将无用的元素真正删除。

c.erase(remove(c.begin(), c.end(), 1963), c.end());
如果这是一个list,这个操作同样适用,但是对于list,更好的做法是调用其remove成员函数。

c.remove(1963);
如果这是关联容器,容器并没有remove成员函数,而且使用remove可能会覆盖容器的值,并且会破坏容器,所以当尝试对关联容器使用remove会编译报错。其正确方法是调用erase算法,而且这样做非常高效,只花费对数时间。

c.erase(1963);


如果我们基于一个判断函数,并删除所有符合这个判断函数的元素。我们应该怎么做呢?

对于序列容器(vector、string、deque和list),我们只要把remove换成remove_if即可。

c.erase(remove_if(c.begin(), c.end(), badValue), c.end()); // 当c是vector、string或deque时c.remove_if(badValue); // 当c是list时
对于关联容器,并没有直接的解法。有两种犯法可以处理该问题,一种容易编写,另一个更加高效。

容易胆小了低的办法是用remove_copy_if将我们需要的值拷贝到一个新容器中,然后将原容器和新的交换。

AssocContainer<int> c; // c现在是一种标准关联容器AssocContainer<int> goodValues; // 用于容纳不删除的值的临时容器remove_copy_if(c.begin(), c.end(),               inserter(goodValues, goodValues.end()),                badValue); // 从c拷贝不删除的值到goodValuesc.swap(goodValues); // 交换c和goodValues

但是这样做的开销可能不能满足我们的要求,然而关联容器没有remove_if这样的操作,所以我们需要自己实现一个类似的功能。可能遇到的问题是,我么并不能直接用erase删除元素,因为一旦调用erase,当前迭代器就已经失效了。我们需要在当前迭代器还没有失效前,就跳转到下一个容器。

AssocContainer<int> c;...for (AssocContainer<int>::iterator i = c.begin(); i != c.end(); /*nothing*/ ){   if (badValue(*i)) c.erase(i++);     else ++i;} 


进一步,如果我们在删除的同时,需要打印相关删除信息该怎么做呢?如果是关联容器,我们只需要在erase的同时,加入打印语句即可。

对于内存连续的容器,我们无法用erase-remove用法,因为没办法使用erase或remove向日志中写信息。(但其实可以在badValue()函数中写,O(∩_∩)O)我们也无法使用关联容器的循环,因为对于关联容器,其元素基于红黑树排列,所以删除单个节点不影响其他节点。但是对内存连续型容器,删除当前元素,会导致后面所有元素向前移动,导致后面所有元素的迭代器全部失效。型号,对于这类容器,其erase成员函数都会返回被删除元素的下一个元素的有效迭代器。所以我们可以这样做。

for (SeqContainer<int>::iterator i = c.begin(); i != c.end();){  if (badValue(*i)){    logFile << "Erasing " << *i << '\n';    i = c.erase(i);   }   else    ++i;}

对于list,可以使用任何一种方法,其都适用。但是一般我们对list采取与vector一样的方式。

总结:

● 去除一个容器中有特定值的所有对象:
   如果容器是vector、string或deque,使用erase-remove惯用法。
   如果容器是list,使用list::remove。
   如果容器是标准关联容器,使用它的erase成员函数。
● 去除一个容器中满足一个特定判定式的所有对象:
   如果容器是vector、string或deque,使用erase-remove_if惯用法。
   如果容器是list,使用list::remove_if。
   如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。
● 在循环内做某些事情(除了删除对象之外):
   如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你
的迭代器。
   如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

条款10:了解分配子(allocator)的约定和限制


条款11:理解自定义分配子的合理用法


条款12:切勿对STL容器的线程安全有不切实际的依赖


第二章 vector和string

条款13:vector和string优先于动态分配的数组

如果使用new来动态分配内存,程序员需要保证这些内存在使用结束后被正确释放。(当然,如果用智能指针就没有这样的问题,但是将无法使用STL提供的算法或容器内置函数。)

为减轻程序员的负担,推荐使用vector或者string。其中有一个例外是,在多线程环境下使用string,由于string大多使用引用计数来优化内存分配和效率,但是在多线程环境下,同步操作可能带来更来的效率损耗。这时候可以用vector<char>来代替string。

条款14:使用reserve来避免不必要的重新分配

以下四个成员函数经常被混淆,而且只有vector和string提供了所有这四个函数。

  • size():返回该容器中有多少个元素
  • capacity():返回该容器利用已分配的内存可以容纳多少个元素,可以用“capacity()-size()”得知容器在不重新分配内存的情况下还能插入多少个元素。
  • resize(container::size_type n):强迫改变容器改变到包含n个元素的状态。如果n小于当前size,则容器尾部元素北析构;如果n大于当前size,则通过默认工造函数将新元素添加到容器的尾部;;如果n大于当前capacity,则先重新分配内存。
  • reserve(container::size_type n):强制容器吧他的容量变成至少是n,前提是n不小于当前的大小。如果n小于当前容量,vector什么都不做;而string可能把自己的容量见效为size()和n中的最大值。
为了避免内存的重新分配和元素拷贝,可以使用reserve成员函数。第一种方式是,若能确切知道大致预计容器中最终有多少元素,可以在容器声明后,立即使用reserve(),预留出适当大小的空间。第二种方式是,先预留足够大的空间,当把所有元素都加入容器后,去除多余的容器。(使用resize()或参考条款17)

条款15:注意string实现的多样性

string的值可能会被引用计数,也可能不会。很多情况下会默认使用引用计数,但一般都会提供关闭默认选择的方法。

string对象大小的范围是一个char*指针大小的1倍到7倍。

创建一个新的字符串可能需要0次、1次或者2次动态分配内存。

string对象可能共享,也可能不共享其大小和容量信息。

string可能支持,也可能不支持针对单个对象的分配子。

不同的实现对字符内存的最小分配单位有不同的策略。

条款16:了解如何把vector和string数据传给旧的API

对于vector v,需要得到一个指向v中数据的指针,从而可以把v中的数据座位数组来对待。

  • 需要保证v不为空,否则&v[0]将导致未定义行为
  • 不应该使用v.begin(),如果必须使用begin(),请使用&*v.begin(),因为这和&v[0]产生相同的指针。
//void doSomething(const int* pInts, size_t numInts);if (!v.empty()) {  doSomething(&v[0], v.size());}

对于string s,相同的方法不一定适用,因为(1)string中的数据不一定存储在连续的内存中;(2)string的内部表示一定以空字符结尾。正确的做法是使用c_str()陈元函数

//void doSomething(const char *pString);doSomething(s.c_str());
先让C API把数据写入到一个vector中,然后把数据复制到最终期望写入的STL容器中,这一思想总是可行的。

// C API:此函数需要一个指向数组的指针,数组最多有arraySize个double// 而且会对数组写入数据。它返回写入的double数,不会大于arraySizesize_t fillArray(double *pArray, size_t arraySize); // 同上vector<double> vd(maxNumDoubles); // 一样同上vd.resize(fillArray(&vd[0], vd.size()));deque<double> d(vd.begin(), vd.end()); // 拷贝数据到dequelist<double> l(vd.begin(), vd.end()); // 拷贝数据到listset<double> s(vd.begin(), vd.end()); // 拷贝数据到set
这意味着,除了vector和string意外的其他STL容器也能把他们的数据传递给C API。只需要将这个容器中的元素赋值到vector中,然后传给盖C API。

void doSomething(const int* pints, size_t numInts); // C API (同上)set<int> intSet; // 保存要传递给API数据的set...vector<int> v(intSet.begin(), intSet.end()); // 拷贝set数据到vectorif (!v.empty()) doSomething(&v[0], v.size()); // 传递数据到API

条款17:使用“swap技巧”除去多余的容量

如何将vector中多余的容量除去?当使用区间erase()后,虽然该vector的大小减小了,但是容量并没有减小。

vector<int>(v)创建一个临时的vector容器,它是v的副本。然而,vector的赋值构造函数只为所复制的元素分配所需要的内存,所欲这个临时容器没有多余的容量。当执行这个临时容器与v的swap操作后,v中多余的容量被除去。而后,临时变量被析构,所占内存被释放。

vector<int> v;...vector<int>(v).swap(v);

同样的方法也适用于string。

string s;...string(s).swap(s);


值得注意的是,这种方法并不能保证size()等于capacity(),只能保证capacity()尽可能的小。

最后,swap还可以用来清空一个容器。

vector<int> v;string s;... // 使用v和svector<int>().swap(v); // 清除v而且最小化它的容量string().swap(s); // 清除s而且最小化它的容量

条款18:避免使用vector<bool>

对于vector<bool> v,它并不是一个STL容器,并且它并不存储bool。 STL用一个bit存储一个bool,然而,C++可以创建一个指向bool的指针,但不能创建指向单个bit的指针。

为了解决这个问题,vector<bool>::operator[]返回一个对象,这个对象表现的像是一个指向单个位的引用,即所谓的代理对象(proxy object)。

vector<bool> v;bool *pb = &v[0]; // 错误!右边的表达式是vector<bool>::reference*类型, 不是bool*
有两种方法解决这一问题。

一是采用deque<bool>。deque<bool>是一个STL容器,并且它确实存储bool。

二是用bitset代替vector<bool>,bitset不是STL容器,但它是标准C++库的一部分。它的大小在编译时就确定了,所以不支持插入和删除操作。
第三章 关联容器

条款19:理解相等(equality)和等价(equivalence)的区别

STL中有两种判断两个元素的值是否相等的方法。

  • 基于相等(以find算法为代表)。它是以operator==为基础。
  • 基于等价(以set::insert),默认以operator<为基础(!(w1<w2) && !(w2<w1))

条款20:位包含指针的关联容器指定比较类型

TODO

条款21:总是让比较函数在等值情况下返回false

TODO

条款22:切勿直接修改set或multiset中的键

TODO

条款23:考虑用排序的vector替代关联容器

TODO

条款24:当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择

TODO

条款25:熟悉非标准的散列容器

TODO