Effective STL~2 vector和string(条款13~18)
第13条:vector和string优先于动态分配的数组
使用new来动态分配内存,意味着使用者将承担责任:
- 必须确保有人会用delete删除所分配内存。如果没有,new将导致内存泄漏;
- 必须确保使用正确delete形式。单个对象删除用delete,数组删除用delete[];
- 必须确保只delete一次。如果new一次,delete多次,结果未定义;
而使用STL顺序容器vector和string,其包含的元素会自动构建、析构,无需额外处理。因此,优先使用vector和string替换动态分配的数组。
通常,只有一种情况下,用动态分配的数组(new [])取代vector和string是合理的,且只对string适用:在多线程环境中使用了引用计数的string,而避免内存分配和字符拷贝节省的时间还比不上同步控制上的时间。
如何确认string实现使用了引用计数方式?
1)查阅库文档;
2)检查string源码实现。注意string是basic_string
对于使用引用计数的string在多线程花费较大时间,可以由3种选择:
1)检查库实现,看是否可能禁用引用计数;
2)寻找或开发另一个不使用引用计数的string实现;
3)考虑使用vector
[======]
第14条:使用reserve来避免不必要的重新分配
vector和string的内存增长过程,调用类似于realloc的操作:
1)分配一块大小为当前容量的某个倍数新内存,作为初始内存。大多数实现中,vector、string容量以2倍数增长;
2)把容器的所有元素从旧内存拷贝到新内存;
3)析构就内存中的对象;
4)释放旧内存;
频繁的扩容,非常耗时,会影响程序效率。如果知道元素的大致数量,从而使用reserve成员函数,强迫容器容量变成指定值,可以有效避免频繁扩容。
vector<int> vec;
vec.reserve(1000);
// 循环过程vec不会发生扩容
for (size_t i = 0; i < 1000; i++)
{
vec.push_back(i);
}
[======]
第15条:注意string实现的多样性
一个string对象大小是多少?即sizeof(string)值是多少?
有的string实现是4,有的是28。
cout << sizeof(string) << endl;
为什么?
我们先看string实现。几乎每个string都包含如下信息:
- 字符串大小(size),即所包含的字符个数 ;
- 用于存该字符串中字符的内存的容量(capacity);
- 字符串的值(value),即构成该字符串的字符;
- 它的分配子的一份拷贝(可选,见条款10);
如果是建立在引用计数基础上的string实现可能还包括:
- 对值的引用计数;
4种不同string实现
1)A实现 sizeof(string) = 28
默认分配子allocator为指针的4倍空间,自定义分配子可能更大。RefCnt是引用计数。
2)B实现 sizeof(string) = 4
“其他”中,包含了一些与多线程环境下同步控制相关的额外数据,实现同步控制的数据大小是指针6倍。RefCnt是引用计数。
3)C实现 sizeof(string) = 4
C实现没有对单个对象的分配子支持。X表示不考虑共享问题(一个引用计数值)。
4)D实现 sizeof(string) = 28
分两种情况,对应2种数据结构:容量<=15,容量>15。
对应以下代码:
string s("Perse");
实现D不会导致任何动态分配,实现A、C将导致一次,B会导致2次。实现B中包含对多线程环境的同步支持。实现C不支持针对单个对象的分配子,意味着只有它可以共享分配子。实现D没有使用引用计数,所有string都不共享数据。
[======]
第16条:了解如何把vector和string数据传给旧的API
对于需要旧式C风格字符串的地方,可以使用c_str成员函数将string传递给const char*
// C API
void doSomething(const char* pString);
// 调用
string s;
...
doSomething(s.c_str());
对于需要旧式C风格T*(数组指针)和size_t(数组大小)的地方,可以使用vector元素的取地址运算符&和vector的size成员函数转换。
// C API 向pArray[0..arraySize-1]写入, 返回已被写入元素个数
size_t fillArray(double* pArray, size_t arraySize);
int maxNumDoubles = 10;
vector<double> vd(maxNumDoubles);
vd.resize(fillArray(&vd[0], vd.size()));
这项技术只对vector有效,因为只有vector才保住和数组有同样的内存布局。不过,string对象初始化时,可以用vector
将除vector和string以为的STL容器把数据传递给C API,可以先把容器元素拷贝到vector,然后再传给C API
// C API
void doSomething(const int* pInts, size_t numInts);
set<int> intSet;
...
vector<int> v(intSet.begin(), intSet.end()); // 把set数据拷贝到vector
if (!v.empty()) doSomething(&v[0], v.size()); // 把数据从vector传给API
[======]
第17条:使用“swap技巧”除去多余的容量
shrink to fit(压缩至适当大小)问题
假设我们向contestants矢量添加一些元素后,又想删除。如果使用条款5介绍的remove+erase成员函数的方法,能从vector删除元素,但无法改变其容量。如果想把它的容量从以前的最大值缩减到当前需要的数量时,该怎么办?这种容量的缩减通常称为“shrink to fit”。
使用swap实现“shrink to fit”,从contestants矢量中除去多余的容量:
vector<int> contestants = { 1, 2, 3, 4, 5, 6, 7, 8 };
// size: 8 capacity: 8
cout << "size: " << contestants.size() << " capacity: " << contestants.capacity() << endl;
// 使用remove + erase成员函数并不能改变vector容量
contestants.erase(remove(contestants.begin(), contestants.end(), 2));
// size: 7 capacity: 8
cout << "size: " << contestants.size() << " capacity: " << contestants.capacity() << endl;
// 使用swap从contestants矢量中除去多余的容量
vector<int>(contestants).swap(contestants);
// size: 7 capacity: 7
cout << "size: " << contestants.size() << " capacity: " << contestants.capacity() << endl;
vector
然后,用swap操作交换临时矢量和contestants矢量。
使用swap清除容器
vector<double> v;
string s;
... // 往v、s中添加、删除数据
vector<double>().swap(v); // 清除v并把容量变为最小
string().swap(s); // 清除s并把容量变为最小
注意:string、vector的大小不一定是0,取决于库容器实现。
[======]
第18条:避免使用vector<bool>
vector
为什么说vector
因为一个对象要成为STL容器,就必须满足条款23列出的所有条件,其中一个是:如果c是包含对象T的容器,而且支持operator[],那么下面代码必须能被编译:
T* p = &c[0]; // 用operator[]返回变量的地址初始化一个T*变量
如果你用operator[] 取得了Container
因此,如果vector
vector<bool> v;
bool* pb = &v[0]; // 实际上无法通过编译,因为&v[0]的类型是reference*,而非bool*
但它不能被编译,因为vector
那如何解决vector
可以使用代理对象(proxy object):
template<typename Allocator>
vector<bool, Allocator> {
public:
class reference { ... }; // 用来为指向某个bit的引用而产生的代理类
reference operator[](size_type n); // operator[]返回一个代理
...
};
既然vector
标准库提供了2个选择:
1)deque
deque
2)bitset
bitset不是标准C++库的一部分。大小(元素个数)编译时确定,因此不支持插入和删除元素。
标准库为什么会保留vector
因为最初C++标准委员会打算,用vector
[======]