Effective STL 第二部分

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

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

  • 确保之后调用delete将内存释放
  • 确保使用的是正确的delete形式,对于单个对象要用delete,对于数组对象需要用delete[]
  • 确保对于一个对象只delete一次

使 用vector和string代替动态分配的数组是个很明智的选择,它们不仅能够自动管理内存(主要是自动释放内,自动增加内存),还提供了很多可用的函 数和类型:既有像begin、end和size这样的成员函数,也有内嵌的像iterator、 reverse_iterator或value_type的typedef。

vector、string自动管理其所包含元素的构造与析构,并有一系列的STL算法支持,同时vector也能够保证和老代码的兼容。

对于string实现,可能使用了引用计数器,这是一种那个消 除了不必要的内存分配和字符拷贝的策略,但是在多线程环境里,对这个string进行线程同步的开销远大于COW的开销。

解决方法:1、关闭引用计数;2、寻找或开发一个不使用引用计数的String实现替代品  3、考虑使用vector<char>或动态数组。

条款14:使用reserve避免不必要的重新分配

对于STL容器而言,当他们容量不足以放下一个新元素的时候,会自动增长以便容纳新的数据。

  • 分配一块原内存大小的数倍的新内存,对于vector和 string 而言,通常是两倍;
  • 将原来容器中元素拷贝到新内存中;
  • 析构旧内存中的对象;
  • 释放就内存;

reserve以及与resever相关的几个函数

  • size() 容器中现有的元素的个数
  • capacity() 容器在不重新分配内存的情况下可容纳元素的总个数。如果要知道vector或者string中有多少没有被占用的内存,可以让capacity()减去size();如果size 和 capacity返回同样的值,容器中就没有剩余空间;而下一次插入元素时,就会产生新的内存分配;
  • resize(Container::size_type n) 将容器的size强制改变为可容纳n个元素。调用resize之后,size将返回n。
    • n>size 将现有容器中的元素拷贝到新内存,并将空余部分用默认构造的新函数填满
    • n<size 将尾部的元素全部析构掉
  • reserve(Container::size_type n)将容器的size改变至少为n
    • n>size 将现有容器中的元素拷贝到新内存,多余部分的内存仍然空置
    • n<size 对容器没有影响

通常有两种方式使用reserve避免不必要的内存分配

  1. 预测大致所需的内存,并在构造容器之后就调用reserve预留内存
  2. 先用reserve分配足够大的内存,将所有元素都加入到容器之后再去除多余内存。
条款15:string实现的多样性

每个String实现都容纳了下面的信息:

  • 字符串的大小,也就是它包含的字符的数目。
  • 容纳字符串字符的内存容量。(字符串大小和容量之间差别的回顾,参见 条款14 。)
  • 这个字符串的值,也就是,构成这个字符串的字符。

另外,一个string可能容纳

  • 它的配置器的拷贝。对于为什么这个域是可选的解释,转向 条款10 并阅读关于这个古怪的管理分配器的规则。

依赖引用计数的string实现也包含了

  • 这个值的引用计数。

总结:

  • string对象的大小可能在char*指针的1倍到7倍之间
  • 新字符串值的建立可能需要0、1或2次动态分配。
  • string对象可能是或可能不共享字符串的大小和容量信息。
  • string可能是或可能不支持每对象配置器
  • 不同实现对于最小化字符缓冲区的配置器有不同策略。
条款16: 如何将vector和string的数据传给遗留的API
【1】用C风格API返回的元素初始化一个vector,可以利用vector和数组潜在的内存分布兼容性将存储vecotr的元素的空间传给API函数:
// C API:此函数需要一个指向数组的指针,数组最多有arraySize个double
// 而且会对数组写入数据。它返回写入的double数,不会大于arraySize
size_t fillArray(double *pArray, size_t arraySize);
vector<double> vd(maxNumDoubles); // 建立一个vector,它的大小是maxNumDoubles
vd.resize(fillArray(&vd[0], vd.size()));
// 让fillArray把数据写入vd,然后调整vd的大小 为fillArray写入的元素个数

【2】 用C风格API的数据初始化string对象,也很简单。只要让API将数据放入一个vector<char>,然后从vector中将数据拷到string:

// C API:此函数需要一个指向数组的指针,数组最多有arraySize个char
// 而且会对数组写入数据。它返回写入的char数,不会大于arraySize
size_t fillString(char *pArray, size_t arraySize);
vector<char> vc(maxNumChars); // 建立一个vector, 它的大小是maxNumChars
size_t charsWritten = fillString(&vc[0], vc.size()); 
// 让fillString把数据写入vc string s(vc.begin(), vc.begin()+charsWritten);
// 从vc通过范围构造函数拷贝数据到s

【3】让C风格API把数据放入一个vector,然后拷到你实际想要的STL容器中的主意总是有效的:

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()); // 拷贝数据到deque
list<double> l(vd.begin(), vd.end()); // 拷贝数据到list
set<double> s(vd.begin(), vd.end()); // 拷贝数据到set

【4】如何将vector和string以外的STL容器中的数据传给C风格API?只要将容器的每个数据拷到vector,然后将它们传给API:

void doSomething(const int* pints, size_t numInts); // C API (同上)
set<int> intSet; // 保存要传递给API数据的set
...
vector<int> v(intSet.begin(), intSet.end()); // 拷贝set数据到vector
if (!v.empty()) doSomething(&v[0], v.size()); // 传递数据到API
条款17:使用swap去除多余容量

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

vector<int>(v)使用v创建一个临时变量,v中空余的内存将不会被拷贝到这个临时变量的空间中。但是,vector的拷贝构造函数只分配拷贝的元素需要的内存,所以这个临 时vector没有多余的容量。再利用swap将这个临时变量与v进行交换,相当于去除掉了v中的多余内存。
由于STL实现的多样行,swap的方式并不能保证去掉所有的多余容量,但它将尽量将空间压缩到其实现的最小程度。
利用swap的交换容器的值的好处在于可以保证容器中元素的迭代器、指针和引用在交换后依然有效。
vector<int> v1;
v1.push_back(1);
vector<int>::iterator i = v1.begin();

vector<int> v2(v1);
v2.swap(v1);
cout<<*i<<endl;  //output 1  iterator指向v2的begin

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

vector<int> v1;
v1.push_back(1);
vector<int>::iterator i = v1.begin();

vector<int>(v1).swap(v1);
cout<<*i<<endl;  //crash here  *i本来应该是匿名对象的.begin所指向的元素,但是由于
匿名对象已经被析构,所以失效

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

条款18:避免使用vector<bool>

作为一个STL容器,vector<bool>有两个问题。第一,它不是一个STL容器。第二,它并不容纳bool,因而永远不要使用vector<bool>。

标 准库提供了两个替代品,它们能满足几乎所有需要。第一个是deque<bool>。deque提供了几乎所有vector所提供的(唯一值得 注意的是reserve和capacity),而deque<bool>是一个STL容器,它保存真正的bool值。当然,deque内部内 存不是连续的。所以不能传递deque<bool>中的数据给一个希望得到bool数组的C API。第二个vector<bool>的替代品是bitset。bitset不是一个STL容器,但它是C++标准库的一部分。与STL容 器不同,它的大小(元素数量)在编译期固定,因此它不支持插入和删除元素,所以bool也是紧凑的存储在内存中。。此外,因为它不是一个STL容器,它也不支持iterator

为了节省空间,它存储的是bool的紧凑表示,通常是一个bit。 由于指向单个bit的指针或引用都是不被允许的,vector<bool>采用代理对象模拟指针指向单个bit。

vector<bool> v;
//...

bool *pb = &v[0]; // compile error

vector<bool>::reference *pr = &v[0];  // OK
条款19:理解相等(equality)和等价(equivalence)的区别 
  • 相等的概念是基于operator==的,也就是取决于operator==的实现
  • 等价关系是基于元素在容器中的排列顺序的,如果两个元素谁也不能排列在另一个的前面,那么这两个元素是等价的。
标准关联容器需要保证内部元素的有序排列,所以标准关联容器的实现是基于等价的。标准关联容器的使用者要为所使用的容器指定一个比较函数(默认为less),用来决定元素的排列顺序。
例如:set中的find函数采用的是等价准则,而find算法采用的是相等准则。
非成员的函数(通常为STL算法)大部分是基于相等的。下列代码可能会返回不同的结果
struct CIStringCompare:
    public binary_function<string, string, bool> {
    bool operator()(const string& lhs,
                    const string& rhs) const
    {
        int i = stricmp(lhs.c_str(),rhs.c_str());
        if(i < 0)
            return true;
        else
            return false;
    }
};


set<string,CIStringCompare> s; //set的第二个参数是类型而不是函数
s.insert("A");

if(s.find("a") != s.end())  //true
{
    cout<<"a";
}

if(find(s.begin(),s.end(),"a") != s.end())   //false
{
    cout<<"a";
}
条款20:为指针的关联容器指定比较类型

当关联容器中保存的是对象指针时,需要自己定义比较器(不是一个函数,而是一个仿函数模板),不然关联容器会按照指针大小进行排序,而不是指针指向的内容。

set<string*> s;
s.insert(new string("A"));
s.insert(new string("C"));
s.insert(new string("B"));

for(set<string*>::iterator i = s.begin(); i != s.end(); i++)
{
    cout<<**i;  //输出一定会是ABC么?
}

因为set中存储的是指针类型,而它也仅仅会对指针所处的位置大小进行排序,与指针所指向的内容无关。

当关联容器中存储指针或者迭代器类型的时候,往往需要用户自定义一个比较函数来替换默认的比较函数。

struct CustomedStringCompare:
    public binary_function<string*, string*, bool> {
    bool operator()(const string* lhs,
                    const string* rhs) const
    {
        return *lhs < *rhs;
    }
};


set<string*,CustomedStringCompare> s;
s.insert(new string("A"));
s.insert(new string("C"));
s.insert(new string("B"));

for(set<string*, CustomedStringCompare>::iterator i = s.begin(); 
i != s.end(); i++) { cout<<**i; //ABC }

可以更进一步的实现一个通用的解引用比较类型

struct DerefenceLess{
    template<typename PtrType>
    bool operator()(PtrType ptr1, PtrType ptr2) const
    {
        return *ptr1 < *ptr2;
    }
};

set<string*,DerefenceLess> s;
条款21: 永远让比较函数对“相等的值”返回false

在关联容器中,用户自定义比较类型时,当两个元素相等时,应该返回false;

如果用less_equal来实现关联容器中的比较函数,那么对于连续插入两个相等的元素则有

1 set<int,less_equal<int>> s;
2 s.insert(1);
3 s.insert(1);

关联容器对”相同“的定义是等价,因此set测试10B是否等价于10A。当执行这个测试时,它自然是使用set的比较函数。在这一例子里,是operator<=,因为我们指定的set的比较函数为less_equal, 而less_equal意思就是operator<=。 于是,set将计算这个表达式是否为真:

因为关联容器是依据等价来实现的,所以判断两个1是否等价!

 !(1<=1) && !(1<=1) // false 不等价 

显然,该表达式返回false,于是两个10都会插入这个set,结果是set以拥有了两个为10的值的拷贝而告终,也就是说它不再是一个set了。通过使用less_equal作为我们的比较类型,我们破坏了容器!

比较函数的返回值表明元素按照该函数定义的顺序排列,一个值是否在另一个之前。相等的值不会有前后顺序,所以,对于相等的值,比较函数应该返回false。

对于multiset又如何呢?multiset应该可以存储两个相等的元素吧? 答案也是否定的。对于下面的操作:

multiset<int,less_equal> s;
s.insert(1);
s.insert(1);

pair<multiset<int,less_equal>::iterator,multiset<int,less_equal>::iterator> 
ret = s.equal_range(1);

返回的结果并不是所期望的两个1。因为equal_range的实现(lower_bound:第一个不小于参数值的元素(基于比较函数的小于), upper_bound:第一个大于参数值的元素)是基于等价的,而这两个1基于less_equal是不等价的,所以返回值中比不存在1。

事实上,上面的代码在执行时会产生错误。VC9编译器Debug环境会在第3行出错,Release环境会在之后用到ret的地方发生难以预测的错误。

条款22:避免原地修改set和multiset的键

set、multiset、map、multimap都会按照一定的顺序存储其中的元素,但如果修改了其中用于排序的键值,则将会破坏容器的有序性。

对于map和multimap而言,其存储元素的类型为pair<const key, value>,修改map中的key值将不能通过编译(除非使用const_cast)。

对于set和multiset,其存储的键值并不是const的,在修改其中元素的时候,要小心不要修改到键值。

class Employee
{
public:
    int id;
    string title;
};

struct compare:
    public binary_function<Employee&, Employee&, bool> {
    bool operator()(const Employee& lhs,
                    const Employee& rhs) const
    {
        return lhs.id < rhs.id;
    }
};


set<Employee,compare> s;

Employee e1,e2;

e1.id = 2;
e1.title = "QA";

e2.id = 1;
e2.title = "Developer";

s.insert(e1);
s.insert(e2);

set<Employee,compare>::iterator i = s.begin();
i->title = "Manager"; //OK to update non-key value
i->id = 3; // 破坏了有序性

有些STL的实现将set<T>::iterator的operator*返回一个const T&,用来保护容器中的值不被修改,在这种情况下,如果希望修改非键值,必须通过const_case。

set<Employee,compare>::iterator i = s.begin();
const_cast<Employee&>(*i).title = "Manager"; //OK
const_cast<Employee*>(&*i).title = "Arch"; //OK
const_cast<Employee>(*i).title = "Director"; 
// Bad 仅仅就修改了临时变量的值 set中的值没有发生改变

对于map和multimap而言,尽量不要修改键值,即使是通过const_cast的方式,因为STL的实现可能将键值放在只读的内存区域当中。

相对安全(而低效)的方式来修改关联容器中的元素

  1. 找到希望修改的元素。
  2. 将要被修改的元素做一份拷贝。(注意拷贝的Map的key值不要声明为const)
  3. 修改拷贝的值。
  4. 从容器中删除元素。(erase 见第九条)
  5. 插入拷贝的那个元素。如果位置不变或邻近,可以使用hint方式的insert从而将插入的效率从对数时间提高到常数时间。
 1 set<Employee,compare> s;
 2 
 3 Employee e1,e2;
 4 
 5 e1.id = 2;
 6 e1.title = "QA";
 7 
 8 e2.id = 1;
 9 e2.title = "Developer";
10 
11 s.insert(e1);
12 s.insert(e2);
13 
14 set<Employee,compare>::iterator i = s.begin();
15 Employee e(*i);
16 e.title = "Manager";
17 
18 s.erase(i++);
19 s.insert(i,e);
条款23:考虑使用排序的vector替代关联容器  

在你的应用中,如果查找的频繁程度逼插入和删除的高很多,推荐使用有序的vector代替关联容器,这主要是从内存引用失效频率考虑的。

因为相对于vector,关联容器需要更大的存储空间。在排序的vector中存储数据比在关联容器中存储数据消耗更少的内存,考虑到页面错误的因素,通过二分搜索进行查找,排序的vector效率更高一些。

如果使用排序的vector替换map,需要实现一个自定义的排序类型,该排序类型依照键值进行排序。

条款24:当关乎效率时应该在map::operator[]和map-insert之间仔细选择

STL map的operator[]被设计为简化“添加或更新”功能,但事实上,当“增加”被执行时,insert比operator[]更高效。当进行更新 时,情形正好相反,也就是,当一个等价的键已经在map里时,operator[]更高效。理由如下:当进行“增加”操作时,operator[]会有三 个函数调用:构造临时对象,撤销临时对象和对对象复制,而insert不会有;而对于更新操作,insert需要构造和析构对象,而operator[] 采用的对象引用,不会有这样的效率损耗。

从效率方面的考虑,当向map中添加元素时,应该使用insert,当需要修改一个元素的值的时候,需要使用operator[]

如果使用operator[]添加元素

1 class Widget{
2 };
3 
4 
5 map<int,Widget> m;
6 Widget w;
7 
8 m[0] = w;
9 //Widget构造函数被调用两次

对于第8行,如果m[0]没有对应的值,则会通过默认的构造函数生成一个widget对象,然后再用operator=将w的值赋给这个widget对象。 使用insert可以避免创建这个中间对象。

1 map<int,Widget> m;
2 Widget w; 
3 
4 m.insert(map<int,Widget>::value_type(0,w));  //没有调用构造函数

如果使用insert修改元素的值(当然,不会有人这样做)

 1 map<int,Widget> m;
 2 Widget w(1); 
 3 m.insert(map<int,Widget>::value_type(0,w)); 
 4 
 5 Widget w2(2);
 6 
 7 m.insert(map<int,Widget>::value_type(0,w2)).first->second = w2; 
//构造了一个pair对象 8 9 // 上面这段代码比较晦涩 10 // map::insert(const value_type& x)的返回值为pair<iterator,bool> 11 // 当insert的值已经存在时,iterator指向这个已经存在的值,bool值为false。 12 // 反之,指向新插入的值,bool值为true。

使用operator[]则轻便且高效的多

map<int,Widget> m;
Widget w(1); 
m.insert(map<int,Widget>::value_type(0,w));

Widget w2(2);

m[0] = w2;

一个通用的添加和修改map中元素的方法

template<typename MapType,
         typename KeyType,
         typename ValueType>
typename MapType::iterator InsertOrUpdate(MapType& map,const KeyType& k, 
const ValueType& v) // 注意typename的用法 从属类型前一定要使用typename { typename MapType::iterator i = map.lower_bound(k);
// 如果i!=map.end(),则i->first不小于k if(i!=map.end() && !map.key_comp()(k,i->first)) // k不小于i->first 等价! { i->second = v; return i; } else { return map.insert(i,pair<const KeyType, ValueType>(k,v)); } }; map<int,Widget> m; Widget w(1); map<int,Widget>::iterator i = InsertOrUpdate<map<int,Widget>,int,Widget>(m,0,w);

 

 

posted @ 2017-08-08 11:56  繁星的夜空2012  阅读(107)  评论(0编辑  收藏  举报