第 13 条:vector 和 string 优先于动态分配的数组。
- 如果使用动态分配的数组(即用
new
来动态分配内存),意味着程序员需要承担三个责任:- 首先必须确保最后会调用
delete
来释放申请的内存; - 其次是必须确保使用了正确的
delete
形式,如果是分配了数组的话,应该使用delete[]
; - 最后必须确保只
delete
了一次,而不是多次。
- 首先必须确保最后会调用
- 而使用
vector
或者string
就不需要承担这样的责任。【因为它们自己管理内存】 - 但这里有一种例外:就是许多
string
的实现是使用了引用计数技术。【在这个例外下,就有可能说动态数组要比string
好】- 这种策略可以消除不必要的内存分配和不必要的字符拷贝,从而可以提供很多应用程序的效率(避免内存分配和字符拷贝节约下来的时间)。
- 然而在多线程的场景下,
string
的引用计数技术又可能导致额外的同步控制,这时效率就可能降低了。 - 在这种情况底下,有三种选择方案:
- 检查
string
实现,看看是否有可能禁止引用计数。 - 寻找或开发不使用引用计数的
string
实现。 - 考虑使用
vector<char>
而不是string
。
- 检查
第 14 条:使用 reserve 来避免不必要的重新分配。
reverse()
函数:通知容器它应该准备多少元素,并不改变容器中元素数量,仅影响预先分配多大内存。【注意本条款是在vector
和string
底下讨论的】vector
和string
都是自动扩容的。- 在当前容量不够的情况下,
vector
或string
会重新申请一个更大的内存(一般大小是当前内存大小的两倍),将元素从旧内存拷贝到新内存; - 最后再析构掉旧元素,释放旧内存。
- 在当前容量不够的情况下,
vector
和string
都提供了以下四个函数:size()
:告诉你容器有多少元素。【元素个数,即容器大小】capacity()
:告诉你当前容器的空间一共可以容纳多少元素。【容器容量】resize()
:强迫容器包含n
个元素。【无论当前元素数目多于n
还是少于n
,最后都会变成n
】【容器大小】reserve()
:把容器容量变为n
个元素大小的空间。【容器容量】
reserve()
成员函数能使重新分配的次数减少到最低,避免重新分配和指针、迭代器、引用失效带来的开销。
第 15 条:注意 string 实现的多样性。
string
的实现有很多种。【不同的实现以不同的方式组织信息】- 首先要知道每个
string
实现中哪些信息是公共的:- 字符串的大小,也就是它包含的字符的数目。
- 容纳字符串字符的内存容量。
- 这个字符串的值,也就是,构成这个字符串的字符。
- 有一些信息则是某个实现特定的:
- 它的配置器的拷贝。【条款 10 中的管理分配器规则有提及】
- 这个值的引用计数。
- 首先要知道每个
- 本条款展示了四种
string
实现:【RefCnt
就是引用计数】 - 实现 A:【一次动态分配,但共享能力比实现 B 和实现 C 都要弱】
-
结构图如下:
-
要注意这里有个分配器的内存占用:
- 如果使用默认分配器,则该
string
对象是指针大小的4
倍; - 如果使用自定义分配器,则
string
对象会随配置器对象的增大而变大。
- 如果使用默认分配器,则该
-
- 实现 B:【两次动态分配内存,有多线程同步控制的支持】
-
结构图如下:
-
该
string
对象就是一个指针大小。 -
这里的
Other
部分是用于多线程系统中与并发控制有关的一些附加数据;Other
部分的大小是指针大小的6
倍。 -
可以看到,
string
的Pointer
指向的结构也是动态分配出来的,而且string
的Value
所在的缓冲区也是再一次动态分配出来的,所以该实现的每次实例创建会带来两次动态分配内存。
-
- 实现 C:【一次动态分配】【没有 per-class allocator 的支持(per-class allocator 是指针对每一个 class 写一个分配器)】【只有实现 C 可以共享分配子:所有的
string
必须使用同一个分配子】-
结构图如下:
-
该
string
对象就是一个指针大小。 -
这里被标记为
X
的是与值共享性有关的数据。【在某些场合下,这些引用计数不希望被共享,才需要这样一个标志;书中提示查阅《More Effective C++》条款 29,去查看在什么场合下需要在引用计数实现中添加一个共享与否的标志位】 -
实现 C 比实现 B 的更进一步在于,字符串和其他信息放置在了一起。
-
- 实现 D:【唯一一个可能不导致动态分配的实现】【唯一一个不共享数据的实现(因为无引用计数)】
-
结构图如下:
-
该
string
对象固定是指针的7
倍。【在使用了默认配置器的前提下】 -
如图,当
Capacity ≤ 15
的时候,Other
的内存可以直接用来存Value
,不需要动态分配内存;当Capacity ≥ 15
的时候,Other
就存取动态分配内存得来的Pointer
。 -
要注意,该实现没有引用计数,即不共享数据。
-
第 16 条:了解如何把 vector 和 string 数据传给旧的 API。
- 通过代码来进行分析:
void doSomething(const int* pInts, size_t numInts) {}// C API,供检验vector void doSomething(const char* pString) {}// C API,供检验string size_t fillArray(double* pArray, size_t arraySize) { return (arraySize - 2); } size_t fillString(char* pArray, size_t arraySize) { return (arraySize - 2); } int test_item_16() { // 第一种场景:vector/string --> C API // vector --> C API 【直接传指针即可,vector和C数组还是有共通之处的】 std::vector<int> v{ 1, 2 }; if (!v.empty()) { doSomething(&v[0], v.size()); doSomething(v.data(), v.size()); // C++11 // doSomething(v.begin(), v.size()); // 错误的用法 doSomething(&*v.begin(), v.size()); // 可以,但不易于理解 } // string --> C API 【用c_str()】 std::string s("xxx"); doSomething(s.c_str()); // string中的数据不一定存储在连续的内存中,并且内部表示不一定是以空字符结尾的 // 使用c_str返回一个指向字符串值的指针,且该指针可用于C // string内部包含空字符没有关系,对于char*的C API会将第一个空字符作为结束符 // string的c_str所产生的指针并不一定指向字符串数据的内部表示; // 可能是指向一个字符串数据的不可修改的拷贝,且 // 该拷贝已经做了适当的格式化,以满足C API的要求。 // 第二种场景:C API 初始化vector/string【最主要还是通过vector作中介】 // C API 初始化vector // 这里说的是用C风格API返回的元素初始化一个vector const int maxNumDoubles = 100; std::vector<double> vd(maxNumDoubles); // 创建大小为maxNumDoubles的vector vd.resize(fillArray(&vd[0], vd.size())); // 使用fillArray向vd中写入数据,然后把vd的大小改为fillArray所写入的元素的个数 // vector和C风格数组的布局有兼容性, // 所以借助C API(fillArray)填充数据,再把vd大小更新为size // C API 初始化string【借助vector的帮助初始化string】 // 这里还是经过vector<char>来进入C API的,因为这个技巧只能工作于vector const int maxNumChars = 100; std::vector<char> vc(maxNumChars); // 创建大小为maxNumChars的vector<char> size_t charsWritten = fillString(&vc[0], vc.size()); // 使用fillString向vc中写入数据 std::string s2(vc.cbegin(), vc.cbegin() + charsWritten); // 通过区间构造函数,把数据从vc拷贝到s2中 // 先让C API把数据写入到一个vector中,然后把数据拷贝到期望最终写入的STL容器中,这一思想总是可行的 // 【借助vector的帮助初始化其他容器】 std::deque<double> d(vd.cbegin(), vd.cend()); // 把数据拷贝到deque中 std::list<double> l(vd.cbegin(), vd.cend()); // 把数据拷贝到list中 std::set<double> s22(vd.cbegin(), vd.cend()); // 把数据拷贝到set中 // 除了vector和string以外的其它STL容器也能把它们的数据传递给C API,你只需要把每个容器的元素拷贝到一个vector中,然后传给该API // 【借助vector的帮助将其他容器的数据传给C API】 std::set<int> intSet; // 存储要传给API的数据的set std::vector<int> v2(intSet.cbegin(), intSet.cend()); // 把set的数据拷贝到vector if (!v2.empty()) doSomething(&v2[0], v2.size()); // 把数据传给API return 0; }
第 17 条:使用 “swap 技巧” 除去多余的容量。
-
vector
和string
都是自动扩容,如果一直使用,容器容量会一直增大;- 如果容器中的元素被大量移除,容器的大小会变小,但是容量不会适应减小。
- 容器的使用率特别低,容器特别臃肿。
-
对
vector
或string
进行 shrink-to-fit 操作时,考虑 swap 技巧。【C++11 中增加了shrink_to_fit()
成员函数】- 代码如下:【swap 技巧的一种变化形式可以用来清除一个容器,并使其容量变为该实现下的最下值】
class Contestant {}; int test_item_17() { // 一、vector除去多余的容量: // 从contestants矢量中除去多余的容量 std::vector<Contestant> contestants; // 让contestants变大,然后删除它的大部分元素 std::vector<Contestant>(contestants).swap(contestants); // shrink-to-fit具体分析如下: // 第一步:vector<Contestant>(contestants)创建一个临时矢量, // vector的拷贝构造函数只为所拷贝的元素分配所需要的内存(这个临时vector没有多余的容量)。 // 第二步:然后把“临时vector中的数据”和“contestants中的数据”作交换, // 临时对象就有了contestants里臃肿的容量, // 到了语句结尾,临时对象就会被析构,从而释放了先前臃肿的内存。 contestants.shrink_to_fit(); // C++11 // 二、string除去多余的容量: std::string s; // 让s变大,然后删除它的大部分字符 std::string(s).swap(s); s.shrink_to_fit(); // C++11 std::vector<Contestant>().swap(contestants); // 清除contestants并把它的容量变为最小 std::string().swap(s); // 清除s并把它的容量变为最小 return 0; }
- 代码如下:【swap 技巧的一种变化形式可以用来清除一个容器,并使其容量变为该实现下的最下值】
-
在做 swap 的时候,不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换(
string
除外)。- 在
swap()
发生后,原先指向某容器中元素的迭代器、指针和引用依然有效,并指向同样的元素;但是,这些元素已经在另一个容器中了。
- 在
第 18 条:避免使用 vector<bool>。
vector<bool>
有两点问题:- 它不是 STL 容器。
- 它并不存储
bool
。
vector<bool>
是一个假容器,它并不真的存储bool
;相反,为了节省空间,它存储的是bool
的紧凑表示。- 在一个典型的实现中,储存在
vector
中的每个所谓的bool
仅占一个二进制位。【位域(bit field)的思想】【一个8
位的字节就可以直接存储8
个所谓的bool
】 - 这个实现的问题在于,一个位域代表的
bool
无法取出来;可以创建一个指向bool
的指针,而指向单个 bit 的指针则是不允许的(引用也是一样)。【书中提及了想要vector<bool>::operator[]
返回指向一个比特的引用可以考虑使用代理技术,但实现上operator[]
返回一个代理类指针,不是bool
指针,所以只是行为上模拟】
- 在一个典型的实现中,储存在
vector<bool>
是失败产物(但它仍在 C++ 标准中),你可以使用替代方案:- 用
deque<bool>
来替代,它是一个 STL 容器,且确实存储bool
。【deque
中元素的内存不是连续的,所以不能把deque<bool>
中的数据传递给一个期望bool
数组的 C API】 - 用
bitset
来替代,bitset
不是 STL 容器,但它是标准 C++ 库的一部分(bitset
容器的大小在编译时确定,所以它不支持插入和删除)。
- 用