《Effective STL》读书笔记(二):vector和string

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

当使用new动态分配内存时,我们需要关注以下内容

  1. 必须保证动态分配的内存会被delete,否则会造成资源泄露
  2. 必须确保使用了正确的delete形式。如果分配了单个对象,则必须使用delete;如果分配了数组,则需要使用delete[]。如果使用了不正确的delete形式,结果将是不确定的。可能会导致程序运行时崩溃,也有可能是资源泄露
  3. 必须确保只delete了一次。如果多次delete,结果同样是不确定的

vectorstring就消除了上述的负担,当元素加入到容器中时,它们的内存会增加;当vectorstring被析构时,它们的析构函数会自动析构容器内的元素。

许多string实现背后使用了引用计数技术,可以消除不必要的内存分配和不必要的字符拷贝,但是这种优化在多线程环境下可能会适得其反,所以如果认为string的引用计数实现会影响效率,那么可能有下面这几种选择

  • 看是否可以禁止引用计数
  • 寻找另外一个不使用引用计数的string实现
  • 考虑使用vector来替代stringvector的实现不使用引用计数,不会发生隐藏的多线程性能问题。string的大多数功能可以通过算法库中的函数来替代。

使用reserve来避免不需要的内存分配

如果确切知道容器内最后会有多少元素的话,可以直接让容器预留合适的容量;如果不知道容器内最后有多少元素的话,可以先预留足够大的空间,然后当把所有数据都加入后,再去除多余的容量。

string实现的多样性

几乎每个string实现都包含以下信息

  • 字符串的大小,即包含字符的个数
  • 该字符串的最大容量
  • 字符串的值,即构成该字符串的字符

除此之外还可能包含

  • 它的分配器的一份拷贝

建立在引用计数基础上的string实现可能还包含

  • 对值的引用计数

不同的实现方式之间string的差别很大:

  • string的值可能被引用计数,也可能不会
  • string对象大小的范围可能是一个char*指针的大小的1倍到7倍
  • 创建一个新的字符串值可能需要零次、一次或两次动态分配内存
  • string对象可能共享,也可能不共享其大小和容量信息
  • string可能支持,也可能不支持针对单个对象的内存分配器
  • 不同的实现对字符内容的最小分配单位有不同策略

vector和string数据传给旧的API

要把vector传给使用数组的函数,只需要传入&v[0]即可,vector内部的元素布局保证和数组相同,所以这样的做法是正确的。可能出现问题的地方是如果vector此时的size为0,那么就可能产生不可预知的后果。所以在传参之前需要先判断以下vector.empty()

上面这种方法对string就无效了,因为:

  1. string中数据不一定存储在连续的内存空间中
  2. string的内部表示不一定是以空字符结尾的

所以通常我们使用c_str函数来获取可供C语言使用的字符串指针,即使长度为0也可以。

上面的方法对于要传入const指针的情况是没有问题的,但是如果在函数中要修改vector或者string的值就可能会出现问题。

对于string来说,c_str()返回的并不一定是字符串数据的内部表示,还可能是一个字符串数据的不可修改的拷贝。

对于vector来说,修改其中元素的值通常是没有问题的,但是不能试图修改元素的个数:这会导致vector内部的状态混乱,如果此时vector.size() == vector.capicity(),那么添加新的元素也会产生不可预知的后果。

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

size_t fillArray(double* pArray, size_t arraySize);  // 返回已被写入的double数据的个数

vector vd(maxNumDoubles);		// 创建大小为maxNumDoubles的vector
vd.resize(fillArray(&vd[0], vd.size()));

这样的方法只对vector有效,因为只有vector才保证和数组有同样的内存布局。如果想要使用来自C API的数据初始化一个string,也可以使用其他方法做到。只需要让API把数据放到一个vector中,然后再把数据从vector拷贝到相应的字符串中即可:

size_t fillString(char* pArray, size_t arraySize);

vector vc(maxNumChars);
size_t charsWritten = fillString(&vc[0], vc.size(0));  // 使用fillString向vc中写入数据
string s(vc.begin(), vc.begin() + charsWritten);  // 通过区间构造函数完成

按照这样的方法,也可以初始化其他容器

size_t fillArray(double *pArray, size_t arraySize);
vector vd(maxNumDoubles);
vd.resize(fillArray(&vd[0], vd.size()));

deque d(vd.begin(), vd.end());
list l(vd.begin(), vd.end());
set s(vd.begin(), vd.end());

反过来,其他容器的内容也可以通过vector作为媒介传递到C API中。

void doSomethine(const int* pInts, size_t numInts);
set intSet;

vector v(intSet.begin(), intSet.end());
if (!v.empty()) doSomethine(&v[0], v.size());

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

如果一个vector的容量很大,但是其中元素数量比较少,如果我们想把vector的容量缩减到合适的大小(这种容量的缩减通常被称为“shrink to fit”),我们可以通过下面的方法来实现这种缩减

class Contestant{...};
vector contestants;

vector(contestants).swap(contestants);  // shrink to fit

表达式vector(contestants)通过拷贝构造函数创建了一个临时变量,这个临时变量的容量恰好是contestants的元素数量,然后再通过调用swap来交换临时变量和contestants的数据,交换之后临时变量得到了之前臃肿的容量,而contestants的容量大小刚刚好。在这句话结束之后,临时变量析构,多占用的内存就真正得到了释放。

对于字符串这样的操作也同样有效。

需要注意的是,这个技巧并不能完全保证缩减之后的容器一定没有冗余的容量,这是因为可能STL的具体实现会保留一些容量,这是无法避免的,但是这种技巧还是能保证使用后“在容器当前大小确定的情况下,使容量在该实现下变得最小”。

我们还可以通过这种技巧来清空一个容器:

string s;

vector().swap(v);  // 和空容器进行swap
string().swap(s);

避免使用vector

如果一个对象是STL容器,那么一定下面的条件:如果c是包含对象T的容器,而且c支持operator[],那么下面的代码必须能够被编译

T *p = &c[0];  // 用operator[]返回值的地址初始化一个T的指针

但是这个条件在vector中是不成立的,像下面的代码无法编译通过

《Effective STL》读书笔记(二):vector和string_第1张图片

《Effective STL》读书笔记(二):vector和string_第2张图片

原因是vector是一个假容器,它并不真的存储bool,为了节省空间它存储的是bool的紧凑表示。在一个典型实现中,存储在vector中的bool仅占一个二进制位,所以一个8位的字节可以容纳8个bool。而指向一个二进制位的指针是被禁止的,所以vector::operator[]返回的是一个代理对象。

所以最好使用deque,它里面确实存储的是bool类型的数据,而且相比vector可以看到的省略只有reservecapacity。另外一个替代方法就是使用bitset,它不是STL容器,但是是C++标准的一部分,但是它的容量在创建时就指定了,没有办法动态调整大小或插入删除元素,因为它不是STL容器,所以它也不提供迭代器,但是提供了很多对位的集合有意义的成员函数。

你可能感兴趣的:(c++,开发语言,stl)