Effective STL~2 vector和string(条款13~18)

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

使用new来动态分配内存,意味着使用者将承担责任:

  1. 必须确保有人会用delete删除所分配内存。如果没有,new将导致内存泄漏;
  2. 必须确保使用正确delete形式。单个对象删除用delete,数组删除用delete[];
  3. 必须确保只delete一次。如果new一次,delete多次,结果未定义;

而使用STL顺序容器vector和string,其包含的元素会自动构建、析构,无需额外处理。因此,优先使用vector和string替换动态分配的数组。

通常,只有一种情况下,用动态分配的数组(new [])取代vector和string是合理的,且只对string适用:在多线程环境中使用了引用计数的string,而避免内存分配和字符拷贝节省的时间还比不上同步控制上的时间。

如何确认string实现使用了引用计数方式?
1)查阅库文档;
2)检查string源码实现。注意string是basic_string的类型定义,wstring是basic_string<wchar_t>的类型定义。

对于使用引用计数的string在多线程花费较大时间,可以由3种选择:
1)检查库实现,看是否可能禁用引用计数;
2)寻找或开发另一个不使用引用计数的string实现;
3)考虑使用vector而不是string;

[======]

第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(contestants)创建一个临时矢量,是contestants的拷贝:由vector的拷贝构造函数来完成。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不是一个STL容器,也不存储bool。

为什么说vector不是一个STL容器?
因为一个对象要成为STL容器,就必须满足条款23列出的所有条件,其中一个是:如果c是包含对象T的容器,而且支持operator[],那么下面代码必须能被编译:

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

如果你用operator[] 取得了Container中的一个T对象,那么你可以通过它的地址得到一个指向该对象的指针(如上面的&c[0])。
因此,如果vector是一个容器,那么下面代码可以被编译:

vector<bool> v;
bool* pb = &v[0]; // 实际上无法通过编译,因为&v[0]的类型是reference*,而非bool*

但它不能被编译,因为vector并不是一个真的容器,不真正存储bool。为了节省空间,它存储的是bool的紧凑表示:每个bool仅占用1bit空间。而指向单个bit的指针或引用,是被禁止的。

那如何解决vector::operator[]返回值是T&的问题呢?
可以使用代理对象(proxy object):

template<typename Allocator>
vector<bool, Allocator> {
public:
    class reference { ... }; // 用来为指向某个bit的引用而产生的代理类
    reference operator[](size_type n); // operator[]返回一个代理
    ...
};

既然vector应当避免,那么需要vector时,应该使用什么?
标准库提供了2个选择:
1)deque
deque是一个STL容器,而且存储bool。但元素内存不是连续的,因此不能传递给一个期望bool数组的C API。

2)bitset
bitset不是标准C++库的一部分。大小(元素个数)编译时确定,因此不支持插入和删除元素。

标准库为什么会保留vector,而不删除呢?
因为最初C++标准委员会打算,用vector演示STL如何支持“通过代理来存储其元素的容器”。人们在实现自己的基于代理的容器时,就有现成的参考。不过,要创建一个代理的容器,同时又要满足STL容器的所有要求是不可能的。由于各种原因,他们的尝试被遗留在标准中。

[======]

posted @ 2021-12-19 13:52  明明1109  阅读(102)  评论(0编辑  收藏  举报