代码改变世界

《Effective STL 读书笔记》 第二章 vector和string

2011-08-11 13:43  咆哮的马甲  阅读(857)  评论(0编辑  收藏  举报
作者:咆哮的马甲 
出处:http://www.cnblogs.com/arthurliu/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。 
转载请保持文档的完整性,严禁用于任何商业用途,否则保留追究法律责任的权利。

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

如果使用new来动态分配内存,使用者必须承担以下的责任

  • 确保之后调用delete将内存释放
  • 确保使用的是正确的delete形式,对于单个对象要用delete,对于数组对象需要用delete[]
  • 确保对于一个对象只delete一次
vector、string自动管理其所包含元素的构造与析构,并有一系列的STL算法支持,同时vector也能够保证和老代码的兼容。

使用了引用计数的string可以避免不必要的内存分配和字符串拷贝(COW- copy on write),但是在多线程环境里,对这个string进行线程同步的开销远大于COW的开销。此时,可以考虑使用vector<char>或动态数组。

 

第十四条: 使用reserve来避免不必要的内存分配

对于STL容器而言,当他们的容量不足以放下一个新元素的时候,会自动增长以便容纳新的数据。(只要不超过max_size)
  1. 分配一块儿原内存大小数倍的新内存,对于vector和string而言,通常是两倍。
  2. 将原来容器中的元素拷贝到新内存中
  3. 析构旧内存中的对象
  4. 释放旧内存

reserve以及与resever相关的几个函数
  • size() 容器中现有的元素的个数
  • capacity() 容器在不重新分配内存的情况下可容纳元素的总个数
  • resize(Container::size_type n) 将容器的size强制改变为n  
    • n>size 将现有容器中的元素拷贝到新内存,并将空余部分用默认构造的新函数填满
    • n<size 将尾部的元素全部析构掉
  • reserve(Container::size_type n)将容器的size改变至少为n
    • n>size 将现有容器中的元素拷贝到新内存,多余部分的内存仍然空置
    • n<size 对容器没有影响

通常有两种方式使用reserve避免不必要的内存分配
  1. 预测大致所需的内存,并在构造容器之后就调用reserve预留内存
  2. 先用reserve分配足够大的内存,将所有元素都加入到容器之后再去除多余内存。
     

第十五条: string实现的多样性

实现A  在该实现中,包含默认Allocator的string是一个指针大小的4倍。对于有自定义的Allocator的string,他的大小将更大
实现A

实现B 在使用默认的Allocator的情况下,string对象的大小与指针的大小相等。当使用自定义的Allocator时,string对象将加上对应的自定义Allocator的对象
Other部分用来在多线程条件下进行同步控制,其大小通常为指针大小的6倍。
实现B

实现C string对象的大小总与指针大小相同,没有对单个对象的Allocator的支持。X包含一些与值的可共享性相关的数据
实现C

实现D 对于使用默认Allocator的string,其大小等于指针大小的7倍。不使用引用计数,string内部包含一块内存可容纳15个字符的字符串。
实现D

总结string的多种实现
  • string的值可能会被引用计数(实现A 实现B 实现C)也可能不会(实现D)
  • string对象的大小可能在char*指针的1倍到7倍之间
  • 创建一个新的字符串值可能需要0次(实现D capacity<=15)、1次(实现A、实现C、实现D capacity>15)或2次(实现B)动态的内存分配
  • string对象可能共享(实现B、实现C)也可能不共享(实现A 实现D)其大小和容量信息
  • string可能支持(实现A 实现B 实现D)也可能不支持(实现C)单个对象的分配子
  • 不同的实现对字符内存的最小分配单位有不同的策略

 

第十六条: 了解如何把vector和string数据传给旧的API

将vector传递给接受数组指针的函数,要注意vector为空的情况。迭代器并不等价于指针,所以不要将迭代器传递给参数为指针的函数。
1 void foo(const int* ptr, size_t size);
2
3
4 vector<int> v;
5 ...
6 foo(v.empty() ? NULL : &v[0], v.size());


将string传递给接受字符串指针的函数。该方法还适用于s为空或包含"\0"的情况

1 void foo(const char* ptr);
2
3
4 string s;
5 ...
6 foo(s.c_str());


使用初始化数组的方式初始化vector

1 //向数组中填入数据
2 size_t fillArray(int* ptr, size_t size);
3
4 int maxSize = 10;
5 vector<int> v(maxSize);
6 v.resize(fillArray(&v[0],v.size()));

借助vector与数组内存布局的一致性,我们可以使用vector作为中介,将数组中的内容拷贝到其他STL容器之中或将其他STL容器中的内容拷贝到数组中

 1 //向数组中填入数据
2 size_t fillArray(int* ptr, size_t size);
3
4 vector<int> v(maxSize);
5 v.resize(fillArray(&v[0],v.size()));
6 set<int> s(v.begin(),v.end());
7
8
9 void foo(const int* ptr, size_t size);
10
11
12 list<int> l();
13 ...
14
15 vector<int> v(l.begin(),l.end());
16 foo(v.empty()? NULL : &v[0],v.size());

 

第十七条: 使用swap去除多余容量

1 vector<int>(v).swap(v);

vector<int>(v)使用v创建一个临时变量,v中空余的内存将不会被拷贝到这个临时变量的空间中,再利用swap将这个临时变量与v进行交换,相当于去除掉了v中的多余内存。

由于STL实现的多样行,swap的方式并不能保证去掉所有的多余容量,但它将尽量将空间压缩到其实现的最小程度。

利用swap的交换容器的值的好处在于可以保证容器中元素的迭代器、指针和引用在交换后依然有效。 

1 vector<int> v1;
2 v1.push_back(1);
3 vector<int>::iterator i = v1.begin();
4
5 vector<int> v2(v1);
6 v2.swap(v1);
7 cout<<*i<<endl; //output 1 iterator指向v2的begin


但是在使用基于临时变量的swap要当心iterator失效的情况  

1 vector<int> v1;
2 v1.push_back(1);
3 vector<int>::iterator i = v1.begin();
4
5 vector<int>(v1).swap(v1);
6 cout<<*i<<endl; //crash here

原因在于第5行构造的临时变量在该行结束后就被析构了。



第十八条: 避免使用vector<bool>

vector<bool>并不是一个真正的容器,也并不存储真正的bool类型,为了节省空间,它存储的是bool的紧凑表示,通常是一个bit。
由于指向单个bit的指针或引用都是不被允许的,vector<bool>采用代理对象模拟指针指向单个bit。 
1 vector<bool> v;
2 //...
3
4 bool *pb = &v[0]; // compile error
5
6 vector<bool>::reference *pr = &v[0]; // OK

  

可以考虑两种方式替代vector<bool>

  • deque<bool> 但是要注意deque的内存布局与数组并不一致
  • bitset bitset不是STL容器所以不支持迭代器,其大小在编译器就已经确定,bool也是紧凑的存储在内存中。