Effective STL (一)

来源:互联网 发布:md5加密填充c语言 编辑:程序博客网 时间:2024/05/13 20:00

条款1:仔细选择你的容器

deque是唯一一个“在迭代器失效时不会使它的指针和引用失效”的标准STL容器。

 

条款2:小心对“容器无关代码”的幻想

既要和序列容器又要和关联容器一起工作的代码并没有什么意义。很多成员函数只存在于其中一类容器中,比如,只有序列容器支持push_front或push_back,只有关联容器支持count和lower_bound。 在不同的类中,相同的操作名称,但在意义上是天差地别的。

 

条款3:使容器里对象的拷贝操作轻量而正确

拷贝对象是STL的方式。即,比如向容器中添加对象,都是通过值来传递的,即都要对对象进行拷贝。

还有其它的操作,如排序算法,删除元素等,都要进行大量的拷贝动作。(通过容器里面的对象的拷贝构造函数和拷贝赋值操作符来完成的)

如果你以基类对象建立一个容器,而你试图插入派生类对象,那么当对象(通过基类的拷贝构造函数)拷入容器的时候对象的派生部分会被删除。

一个使拷贝更高效、正确而且对分割问题免疫的简单的方式是建立指针的容器而不是对象的容器。

(但指针的容器有它们自己STL相关的头疼问题。智能指针的容器是一个吸引人的选择)

 

条款4:用empty来代替检查size()是否为0

对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现,size花费线性时间。

 

条款5:尽量使用区间成员函数代替它们的单元素兄弟

因为区间成员函数一般针对特定的容器进行了优化,要比“通用”版本的操作效率高。

无论何时你必须完全代替一个容器的内容,你就应该想到赋值。

几乎所有目标区间被插入迭代器指定的copy的使用都可以用调用的区间成员函数的来代替。(尽量用成员函数来代替copy)

● 一般来说使用区间成员函数可以输入更少的代码。

● 区间成员函数会导致代码更清晰更直接了当。

 

条款6:警惕C++最令人恼怒的解析

ifstream dataFile("ints.dat");

list<int> data(istream_iterator<int>(dataFile), // 警告!这完成的并不

istream_iterator<int>()); // 是像你想象的那样

编译器可能会将它解析为一个函数的声明。

改用如下代码来代替:(即在第一个参数外面加上括号)

list<int> data((istream_iterator<int>(dataFile)), 

istream_iterator<int>()); 

不过这种方法可能并不是所有编译器都支持的。

所以改成下面的方法可以保证所有编译器支持:

istream_iterator<int> dataBegin(dataFile);

istream_iterator<int> dataEnd;

list<int> data(dataBegin, dataEnd);

 

条款7:当使用new得指针的容器时,记得在销毁容器前delete那些指针

当你要删除指针的容器时要避免资源泄漏,你必须用智能引用计数指针对象(比如Boost的shared_ptr)来代替指针,或者你必须在容器销毁前手动删除容器中的每个指针。

 

条款8:永不建立auto_ptr的容器

auto_ptr的容器(COAPs)是禁止的。试图使用它们的代码都不能编译。

主要是因为auto_ptr会传递所有权,所以再对容器操作的时候,很可能产生一些非预期的行为。

 

条款9:在删除选项中仔细选择

● 去除一个容器中有特定值的所有对象:

如果容器是vector、string或deque,使用erase-remove惯用法。

如果容器是list,使用list::remove。

如果容器是标准关联容器,使用它的erase成员函数。

● 去除一个容器中满足一个特定判定式的所有对象:

如果容器是vector、string或deque,使用erase-remove_if惯用法。

如果容器是list,使用list::remove_if。

如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

● 在循环内做某些事情(除了删除对象之外):

如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你的迭代器。

如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

 

条款10:注意分配器的协定和约束

 

条款12:对STL容器线程安全性的期待现实一些

你能从已有的实现里确定的最多是下列内容:

● 多个读取者是安全的。多线程可能同时读取一个容器的内容,这将正确地执行。当然,在读取时不能有任何写入者操作这个容器。

● 对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。

所以要实现线程安全,必须自己来处理代码,将要加锁部分的代码lock住。

 

条款13:尽量使用vector和string来代替动态分配的数组

无论何时,你发现你自己准备动态分配一个数组(也就是,企图写“new T[...]”),你应该首先考虑使用一个vector或一个string。(这样就可以避免管理资源,省去了new及delete所可能造成的资源泄漏)

如果你在多线程环境中使用引用计数字符串,就应该注意线程安全性支持所带来的的性能下降问题。

如果你用的string实现是引用计数的,而且要在多线程环境中使用,可以用如下的方法尝试:

一,看看库是否可以关闭引用计数。

二,寻找或开发一个不使用引用计数的string实现。

三,考虑使用vector<char>来代替string。

 

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

因为vector和string空间不足时(且小于max_size),每次会以2为因数增长。而且每次都会申请新内存,将旧数据拷贝到新内存,销毁旧内存的对象,释放旧内存。所以消耗很大。

用reserve来保存足够多的容量。如果确切的知道有多少元素,就可以使用reserve,或者保留你可能需要的最大空间。将数据全部添加完后,再修整掉多余的内容。

 

条款15:小心string实现的多样性

因为string的各实现不同,可能造成string特性的一些差异,如sizeof(string)就可能大小不一致。

新字符串值的建立可能需要0、1或2次动态分配。

string对象可能是或可能不共享字符串的大小和容量信息。

string可能是或可能不支持每对象配置器。

不同实现对于最小化字符缓冲区的配置器有不同策略。

 

条款16: 如何将vector和string的数据传给遗留的API

如果你有一个vector对象v,而你需要得到一个指向v中数据的指针,以使得它可以被当作一个数组,只要使用&v[0]就可以了。

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

 

条款17:使用“交换技巧”来修整过剩容量

当vector擦除了很多元素之后,想要把它的大小缩减,以节省空间。

reserve和resize都没法减少vector的占用空间。只能用swap来做。

如下:

vector<Contestant>(contestants).swap(contestants);

即,用目前contestants的有效元素来初始化一个临时vector,然后再将两个vector的内容互换。并且当这个临时对象消失的时候,那个vector的所有空间都被释放了。

string(s).swap(s); // 在s上进行“收缩到合适”

vector<Contestant>().swap(v); // 清除v而且最小化它的容量

string().swap(s); // 清除s而且最小化它的容量

 

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

第一,它不是一个STL容器。

第二,它并不容纳bool。

(虽然vector<bool>满足大部分STL容器的必要条件,但仍然不能完全满足。)

可以用deque<bool>来代替(deque<bool>的存储并不连续)。或者用bitset来代替。(bitset是标准库的一部分,但不是STL容器。)

bitset在编译期间固定大小,所以不支持插入和删除元素。也不支持iterator,使用一个压缩的表示法,使得它包含的每个值只占用一比特。它提供vector<bool>特有的flip成员函数及其它位集操作函数。

 

条款19:了解相等和等价的区别

a==b表示a和b相等。

!(a<b) && !(b<a) 即,a<b为假,且 b<a 也为假,则a和b等价。

只所以在关联容器中使用等价,没有使用相等的原因是:

如果关联容器使用相等来决定两个对象是否有相同的值,但因为关联容器要决定元素间的顺序,所以还是要有用来比较元素大小的运算符,这样多个运算符,在关联容器中容易造成混乱。

通过只使用一个比较函数并使用等价作为两个值“相等”的意义的仲裁者,标准关联容器避开了很多会由允许两个比较函数而引发的困难。

 

条款20:为指针的关联容器指定比较类型

在对关联容器进行默认排序时,会默认采用指针的值来做为排序对象。所以此时要自定义排序规则。

 

条款21: 永远让比较函数对相等的值返回false

如果对相等的值返回true的时候,则在关联容器里面用operator <= 来比较两个元素的时候,则会将两个不相等的元素判断为不相等。(因为!(a<b) && !(b<a)为true才相等)

而且对multiset及multimap等,使用equal_range来得到等价的值的范围,a==b,但并不能得到a与b等价,所以它们两个不可能都在equal_range得到的范围内。

从技术上说,用于排序关联容器的比较函数必须在它们所比较的对象上定义一个“严格的弱序化(strict weak ordering)”。任何一个定义了严格的弱序化的函数都必须在传入相同的值的两个拷贝时返回false。

 

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

set和multiset保持它们的元素有序,这些容器的正确行为依赖于它们保持有序。

如果要修改的话,将原有元素拷贝出来,然后修改这个拷贝值。删除容器中的原元素,再将新的拷贝元素插入到容器里面。

 

条款23:考虑用有序vector代替关联容器

有序的vector与关联容器相比,从算法上来看,查找速度可能不会快,但会节省空间,因为关联容器要保存一些指针。

但实际上,因为关联容器存储元素的时候,元素是分散的,就可能存储在多个内存页面上,或者是存储在虚拟内存中,所以用到元素的时候,会经常发生缺页错误,从而导致页面更频繁的换入换出,影响查找速度。

用有序vector来模拟map或multimap时,map<const K, Val> 中的K不能为const,因为要对vector进行排序的时候,要改变K的值,来达到排序的效果。(模拟map的话,因为vector存储的是pair,所以要自定义排序函数)

 

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

map::operator[]被设计为简化“添加或更新”功能,与vecotr、string等的operator[]无关,也与内建数组没有联系。

map<int, Widget> m;

m[1] = 1.50;  //使用operator[]

m.insert(map<int, Widget>::value_type(1, 1.50));  //使用insert

使用:m[1] = 1.50; 时,如果1还未在map中出现,就会插入这个元素。

当插入元素的过程中(即1还未在map中出现),第一种方法会比第二种方法多出如下的三步:

建立临时的默认构造Widget对象。

对Widget的赋值操作。

销毁这个临时Widget对象。

当1已经在map中出现,就会更新这个值。

这个时候,第一种方法反而会更高效。原因如下:

在insert方法中出现的map<int, Widget>是一个pair对象,在这个地方就会构造和析构pair,而且还会构造Widget,所以此时效率就比operator[]低。

不过我们自己可以实现一个算法,针对不同的情况调用不同的函数来提高效率,使其总是高效的。

 

条款25:熟悉非标准散列容器

散列容器是关联容器。

事实上的标准名字:hash_set、hash_multiset、hash_map和hash_multimap。(目前还未标准化,但下个版本会加入标准库。不同的厂家实现原理、细节可能会不同)