Effective STL:02vector和string

Posted gqtcgq

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective STL:02vector和string相关的知识,希望对你有一定的参考价值。

         在STL容器中,vector和string的使用频率会更高一些。设计vector和string的目标就是为了替换大多数应用中要使用的数组。

 

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

         一旦要使用new动态分配数组,将要面临很多问题:必须确保delete、必须使用正确的delete形式;必须保证只delete一次。每当发现自己要动态分配一个数组时,都应该考虑用vector和string来代替。vector和string 消除了上述的负担,因为它们自己管理内存。如果你担心还得继续支持旧的代码,而它们是基于数组的,使用vector和string也没有问题,条款16会介绍把vector和string中的数据传递给期望接受数组的API是多么容易。

 

目前我只能想到在一种情况下,用动态分配的数组取代vector和string是合理的,而且这种情形只对string适用。许多string实现在背后使用了引用计数技术,这种策略可以消除不必要的内存分配和不必要的字符拷贝,从而可以提高很多应用程序的效率。但是如果你在多线程环境中使用了引用计数的string,你会发现,由避免内存分配和字符拷贝所节省下来的时间还比不上花在背后同步控制上的时间。如果你在多线程环境中使用了引用计数的string,那么注意一下因支持线程安全而导致的性能问题是很有意义的。

    为了确定你是否在使用以引用计数方式实现的string,最简单的办法是查阅库文档。因为引用计数被视为一种优化,所以供应商通常把它作为一个特征而着意指出;另一条途径是检查库中实现string的源代码,最容易检查的或许是该类的拷贝构造函数,看看它是否在某处增加了引用计数。如果增加了,那么string是用引用计数来实现的。

如果string是以引用计数方式来实现的,而你又运行在多线程环境中,并认为string的引用计数实现会影响效率,那么,你至少有三种可行的选择,首先,检查你的库实现,看看是否有可能禁止引用计数,通常是通过改变某个预处理变量的值;其次,寻找或开发另一个不使用引用计数的string实现(或者是部分实现);第三,考虑使用vector<char>而不是string。vector的实现不允许使用引用计数,所以不会发生隐藏的多线程性能问题。当然,如果你转向了vector<char>,那么你就舍弃了使用string的成员函数的机会,但大多数成员函数的功能可以通过STL算法来实现,所以当你使用一种语法形式而不是另一种时,你不会因此而被迫舍弃功能。

 

总结起来很简单,如果你正在动态地分配数组,那么你可能要做更多的工作。为了减轻自己的负担,请使用vector或string。

 

 

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

对于vector和string,当需要更多空间时,它会作出下面的动作:

分配一块大小为当前容量的某个倍数的新内存,在大多数实现中,vector和string        的容量每次以2的倍数增长;

把容器的所有元素从旧的内存拷贝到新的内存中;

析构掉旧内存中的对象;

释放旧内存;

涉及到内存的分配、释放,以及对象的复制和析构,因此这个过程会非常耗时。而且每当这些步骤发生时,vector或string中所有的指针、迭代器和引用都将变得无效。因此,应该尽可能的把重新分配的次数减少到最低限度,从而避免重新分配和指针、迭代器、引用失效带来的开销。这时可以使用reverse成员函数。

在解释reserve怎样做到这一点之前,还需要介绍4个相互关联,但有时会被混淆的成员函数。在标准容器中,只有vector和string提供了所有这4个函数:

size()返回该容器中当前有多少个元素;

capacity()返回容器利用已经分配的内存可以容纳多少个元素,这是容器当前所能容        纳的元素总数,而不是它还能容纳多少个元素;

resize(Container::size_type n)强迫容器改变到包含n个元素的状态。在调用resize之后,        size将返回n。如果n比当前的size要小,则容器尾部的元素将会被析构;如果n比当前的size要大,则通过默认构造函数创建的新元素将被添加到容器的末尾;如果n比当前的容量要大,那么在添加元素之前,将先重新分配内存;

reserve(Container::size_type n)强迫容器把它的容量变为至少是n,前提是n不小于当        前的大小。这通常会导致重新分配,因为容量需要增加;

因此,避免重新分配的关键在于,尽早地使用reserve,把容器的容量设为足够大的值,最好是在容器刚被构造出来之后就使用reserve。

通常有两种方式来使用reserve以避免不必要的重新分配。第一种方式是,若能确切知道或大致预计容器中最终会有多少元素,则此时可使用reserve;第二种方式是,先预留足够大的空间,然后,当把所有数据都加入以后,再去除多余的容量,条款17条会介绍如何去除多余的容量。

 

 

15:注意string实现的多样性

         string的实现方式可能有多种,在有些string的实现中,string和char*指针的大小相同,而在另一些实现中,string的大小是char*的7倍。几乎每个string实现都包含如下信息:

         字符串的大小,也就是string包含的字符的个数;用于存储字符串中字符的内存容量大小;字符串的值。除了以上这些,string还可能包括分配器的副本,建立在引用计数基础上的string实现可能还包含了引用计数。下面介绍4种string的实现。

         实现A:string对象包含分配器的副本、字符串的大小、字符串的容量、以及一个指针。指针指向动态分配的内存,其中包含了引用计数以及字符串的值。这种实现中,使用默认分配器的string对象大小是一个指针的4倍。若使用了自定义的分配器,则string对象会更大一些:

 技术分享图片

         实现B:如果使用默认的分配器,则string对象大小与指针相同,因为它只包含了一个指向某种结构的指针。如果使用了自定义的分配器,则string对象的大小相应的加上分配器的大小。在该实现中,由于用到了优化,所以使用默认分配器不需要多余的空间。

         B实现中指针所指向的结构包含了字符串的大小、容量和引用计数,以及一个指向一块动态分配的内存的指针,该内存中包含了字符串的值。该对象可能还包含了一些与多线程环境下的同步控制相关的额外数据:

 技术分享图片

         实现C:string对象的大小总是与指针相同,该指针指向一块动态分配的内存,其中包含了与该字符串相关的一切数据:它的大小、容量、引用计数和值。没有对单个对象的分配器支持。该内存中也包含了一些与值的可共享性有关的数据,这里将它标记为X:

 技术分享图片

实现D:string对象是指针大小的7倍(仍然假定使用的是默认的分配器)。这一实现不使用引用计数,但是每个string内部包含一块内存,最大可容纳15个字符的字符串。因此,小的字符串可以完整地存放在该string对象中,这一特性通常被称为“小字符串优化”特性。当一个string的容量超过15时,该内存的起始部分被当作一个指向一块动态分配的内存的指针,而该string的值就放在这块内存中。

 技术分享图片

像string s(“Perse”);这样的语句,在实现D中将不会导致任何动态分配,在实现A和实现C中将导致一次动态分配,而在实现B中会导致两次动态分配(一次是为string对象所指向的对象,另一次是为该对象所指向的字符缓冲区);

在以引用计数为基础的设计方案中,string对象之外的一切都可以被多个string所共享,实现A比实现B或实现C提供了较小的共享能力,实现B和实现C可以共享string的大小和容量,从而减少了每个对象存贮这些数据的平均开销。有趣的是,实现C不支持单个对象的分配器,这意味着所有的string必须使用同一个分配器;在实现D中,所有的string都不共享任何数据。

很多实现默认情况下会使用引用计数,但它们通常提供了关闭默认选择的方法,只有当字符串被频繁复制时,引用计数才有用;string对象大小的范围可以是一个char*指针的大小的1倍到7倍;创建一个新的字符串值可能需要零次、一次或两次动态分配内存;string对象可能共享,也可能不共享其大小和容量信息;string可能支持,也可能不支持针对单个对象的分配器;不同的实现对字符内存的最小分配单位有不同的策略。

 

 

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

C++的精英们一直试图使程序员们从数组中解放出来,转向使用vector。他们同样努力地试图使开发者们从char*指针转向string对象。但是旧的C API还存在,它们使用数组和char*指针来进行数据交换而不是vector或string对象。这样的APl还将存在很长一段时间,如果想有效地使用STL,我们就必须与它们和平共处。

幸运的是,这很容易做到。如果你有一个vector v,而你需要得到一个指向v中数据的指针,从而可把v中的数据作为数组来对待,那么只需使用&v[0]就可以了。对于string s,对应的形式是s.c_str()。

&v[0]是指向vector中第一个元素的指针。C++标准要求vector中的元素存储在连续的内存中,就像数组一样。所以,可以把vector传给一个如下所示的C API:

void doSomething(const int* pInts, size_t numlnts);
doSomething(&v[0], v.size());

这里唯一需要注意的问题是v不能为空,否则&v[0]试图产生一个指针,而该指针指向的东西并不存在。

有些人可能会用v.begin()来代替&v[0],但实际上这是错误的,你不应该依赖于这一点。

 

如果需要将string传递给接收const char*的函数,则需要使用string的c_str函数:

void doSomething(const char *pString);
doSomething(s.c_str());

 

再看一下doSomething的声明:

void doSomething(const int* pints, size_t numlnts);
void doSomething(const char *pString);

          要传入的指针都是指向const指针。也就是vector和string的数据被传递给一个要读取而非改写这些数据的API,对于string,这是唯一所能做的,因为c_str所产生的指针并不一定指向字符串数据的内部表示。对于vector,则允许在C API中改变v元素的值,但被调用的函数不能改变vector中元素的个数。比如,不能试图在vector的未使用的容量中“创建”新元素。不然,v的内部将会变得不一致,因为它从此无法知道自己的正确大小,v.size()将产生不正确的结果。

        

         如果想用C API中元素初始化一个vector,可以利用vector和数组的内存布局兼容性,向API传入vector中元素的存储区域:

size_t fillArray(double *pArray, size_t arraySize);
vector<double> vd(maxNumDoubles); // create a vector whose size is maxNumDoubles
vd.resize(fillArray(&vd[0], vd.size()));

 这一技术只对vector有效,因为只有vector才保证和数组有同样的内存布局。不过,如果你想用来自C API中的数据初始化一个string,也很容易就能做到。只要让API把数据放到一个vector<char>中,然后把数据从该vector拷贝到相应字符串中即可:

size_t fillString(char pArray, size_t arraySize);
vector<char> vc(maxNumChars); // create a vector whose size is maxNumChars
size_t charsWritten = fillString(&vc[0], vc.size()); 
string s(vc.begin(), vc.begin()+charsWritten); 

 实际上,先让C API把数据写入到一个vector中,然后把数据拷贝到期望最终写入的STL容器中,这一思想总是可行的:

size_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()); 
list<double> l(vd.begin(), vd.end()); 
set<double> s(vd.begin(), vd.end()); 

 这意味着,除了vector和string以外,其他STL容器也能把它们的数据传递给C API。只需把每个容器的元素拷贝到一个vector中,然后传给该API即可:

void doSomething(const int* pints, size_t numlnts); 
set<int> intSet; 
… 
vector<int> v(intSet.begin(), intSet.end()); 
if (!v.empty()) doSomething(&v[0], v.size()); 

  

 

17:使用swap技巧去除多余的容量

         假设一个包含Contestant对象的vector,该vector曾经拥有数以万计的Contestant对象,之后又经过erase将大部分Contestant从中删除,该操作缩减了vector的size,但是并没有减少它的capacity。为了避免vector仍占用不再需要的内存,需要有一种方法可以把它的容量从以前的最大值缩减需要的数量。

         使用swap可以实现这一点:

class Contestant {...};
vector<Contestant> contestants;
… 
vector<Contestant>(contestants).swap(contestants);

          表达式vector<Contestant>(contestants)创建一个临时的vector,它是contestants的拷贝,这是由vector的拷贝构造函数来完成的。然而vector的拷贝构造函数只为所拷贝的元素分配所需要的内存,所以这个临时vector没有多余的容量。然后我们把临时vector中的数据和contestants中的数据做swap操作。在这之后,contestants具有了被去除之后的容量,即原先临时变量的容量,而临时变量的容量则变成了原先contestants臃肿的容量。到这时,临时vector被析构,从而释放了先前为contestants所占据的内存。

 

         同样的技巧对string也适用:

string s;
… //make s large, then erase most of its characters
string(s).swap(s); 

 这一技术并不保证一定能去除所有多余的容量。STL的实现者如果愿意的话,他们可以自由地为vector和string保留多余的容量,而有时他们确实希望这样做。例如,他们可能需要一个最小的容量,或者他们把一个vector或string的容量限制为2的乘幂数。所以,这种技巧实际上并不意味着“使容量尽量小”,它意味着“在容器当前的大小确定的情况下,使容量在该实现下变为最小”。

 

另外,swap技巧的一种变化形式可以用来清除一个容器,并使其容量变为该实现下的最小值。只要与一个用默认构造函数创建的vector或string做swap就可以了:

vector<Contestant> v;
string s;
… // use v and s
vector<Contestant>().swap(v); //clear v and minimize its capacity
string().swap(s); // clear s and minimize its capacity

 最后要注意的是:在做swap的时候,不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换(string除外)。在swap发生后,原先指向某容器中元素的迭代器、指针和引用依然有效,并指向同样的元素—但是,这些元素已经在另一个容器中了。

 

 

18:避免使用vector<bool>

vector<bool>并不是一个严格意义上的STL容器,它并不存储bool。一个对象要成为STL容器,就必须满足C++标准的第23.1节列出的所有条件。其中的一个条件是,如果c是包含对象T的容器,而且c支持operator[],那么下面的代码必须能够被编译:T *p = &c[0];

所以,如果vector<bool>是一个STL容器,下面这段代码必须可以被编译:

vector<bool> v;
bool *pb = &v[0];

 但是它不能编译。原因是,vector<bool>并不真的储存bool。相反,为了节省空间,它储存的是bool的紧凑表示。在一个典型的实现中,储存在vector中的每个“bool”仅占一个二进制位,一个8位的字节可容纳8个“bool"。在内部,vector<bool>使用了与位域一样的思想,来表示它所存储的那些bool实际上它只是假装存储了这些bool。

指向单个位的引用是被禁止的,这使得在设计vector<bool>的接口时产生了一个问题,因为vector<T>::operator[]的返回值应该是T&。但由于vector<bool>中存储的并不是bool,所以vector<bool>::operator[]需要返回一个指向单个位的引用,而这样的引用却不存在。    为了克服这一困难,vector<bool>::operator[]返回一个对象,这个对象表现得像是一个指向单个位的引用,即所谓的代理对象,vector<bool>看起来像是这样:

template <typename Allocator>
vector<bool, Allocator> 
{
public:
    class reference {...}; 
    reference operator[](size_type n);
    …
}

          这就是bool *pb = &v[0];编译报错的原因。

 

既然vector<booi>应当被避免,那么当需要vector<bool>时,应该使用什么呢?标准库提供了两种选择,可以满足绝大多数情况下的需求。第一种是deque<bool>,deque几乎提供了vector所提供的一切,但deque中元素的内存不是连续的,所以你不能把deque<bool>中的数据传递给一个期望bool数组的C API;第二种可以替代vector<bool>的选择是bitset。

 

以上是关于Effective STL:02vector和string的主要内容,如果未能解决你的问题,请参考以下文章

Effective STL 读书笔记

effective STL

Effective STL第1条:容器之(慎重选择容器类型)

《Effective STL》阅读笔记

《Effective STL》阅读笔记

迅速读懂:Effective STL