vector 和 string 优先于动态分配数组
当使用new动态分配内存时,我们需要关注以下内容
- 必须保证动态分配的内存会被delete,否则会造成资源泄露
- 必须确保使用了正确的delete形式。如果分配了单个对象,则必须使用delete;如果分配了数组,则需要使用delete[]。如果使用了不正确的delete形式,结果将是不确定的。可能会导致程序运行时崩溃,也有可能是资源泄露
- 必须确保只delete了一次。如果多次delete,结果同样是不确定的
vector
和string
就消除了上述的负担,当元素加入到容器中时,它们的内存会增加;当vector
或string
被析构时,它们的析构函数会自动析构容器内的元素。
许多string实现背后使用了引用计数技术,可以消除不必要的内存分配和不必要的字符拷贝,但是这种优化在多线程环境下可能会适得其反,所以如果认为string的引用计数实现会影响效率,那么可能有下面这几种选择
- 看是否可以禁止引用计数
- 寻找另外一个不使用引用计数的string实现
- 考虑使用
vector<char>
来替代string
。vector
的实现不使用引用计数,不会发生隐藏的多线程性能问题。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
就无效了,因为:
string
中数据不一定存储在连续的内存空间中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<double> vd(maxNumDoubles); // 创建大小为maxNumDoubles的vector
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);
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<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
作为媒介传递到C API中。
void doSomethine(const int* pInts, size_t numInts);
set<int> intSet;vector<int> v(intSet.begin(), intSet.end());
if (!v.empty()) doSomethine(&v[0], v.size());
使用swap技巧来去除多余的容量
如果一个vector
的容量很大,但是其中元素数量比较少,如果我们想把vector
的容量缩减到合适的大小(这种容量的缩减通常被称为“shrink to fit”),我们可以通过下面的方法来实现这种缩减
class Contestant{...};
vector<Contestant> contestants;vector<Contestant>(contestants).swap(contestants); // shrink to fit
表达式vector<Contestant>(contestants)
通过拷贝构造函数创建了一个临时变量,这个临时变量的容量恰好是contestants
的元素数量,然后再通过调用swap
来交换临时变量和contestants
的数据,交换之后临时变量得到了之前臃肿的容量,而contestants
的容量大小刚刚好。在这句话结束之后,临时变量析构,多占用的内存就真正得到了释放。
对于字符串这样的操作也同样有效。
需要注意的是,这个技巧并不能完全保证缩减之后的容器一定没有冗余的容量,这是因为可能STL的具体实现会保留一些容量,这是无法避免的,但是这种技巧还是能保证使用后“在容器当前大小确定的情况下,使容量在该实现下变得最小”。
我们还可以通过这种技巧来清空一个容器:
string s;vector<Contestant>().swap(v); // 和空容器进行swap
string().swap(s);
避免使用vector<bool>
如果一个对象是STL容器,那么一定下面的条件:如果c是包含对象T的容器,而且c支持operator[]
,那么下面的代码必须能够被编译
T *p = &c[0]; // 用operator[]返回值的地址初始化一个T的指针
但是这个条件在vector<bool>
中是不成立的,像下面的代码无法编译通过
原因是vector<bool>
是一个假容器,它并不真的存储bool,为了节省空间它存储的是bool的紧凑表示。在一个典型实现中,存储在vector
中的bool
仅占一个二进制位,所以一个8位的字节可以容纳8个bool
。而指向一个二进制位的指针是被禁止的,所以vector<bool>::operator[]
返回的是一个代理对象。
所以最好使用deque<bool>
,它里面确实存储的是bool
类型的数据,而且相比vector<bool>
可以看到的省略只有reserve
和capacity
。另外一个替代方法就是使用bitset
,它不是STL容器,但是是C++标准的一部分,但是它的容量在创建时就指定了,没有办法动态调整大小或插入删除元素,因为它不是STL容器,所以它也不提供迭代器,但是提供了很多对位的集合有意义的成员函数。