概述
术语
● vector、string、deque和list被称为标准序列容器;而标准关联容器是set、multiset、map和multimap。
● 迭代器被分成五个种类。简要地说,输入迭代器是每个迭代位置只能被读一次的只读迭代器;输出迭代器是每个迭代位置只能被写一次的只写迭代器;输入和输出迭代器被塑造为读和写输入和输出流,因此并不奇怪输入和输出迭代器最通常的表现分别是istream_iterator和ostream_iterator;前向迭代器有输入和输出迭代器的能力,同时它们可以反复读或写一个位置,但它们不支持operator--,所以它们可以高效地向前移动任意次数;双向迭代器就像前向迭代器,除了它们的后退可以像前进一样容易,list和标准关联容器都提供双向迭代器;随机访问迭代器可以做双向迭代器做的一切事情,它们也提供“迭代器算术”,即有一步向前或向后跳的能力,vector、string和deque都提供随机访问迭代器。
● 重载了函数调用操作符(即 operator())的任何类叫做仿函数类。从这样的类建立的对象称为函数对象或仿函数,并将operator()定义为const类型。STL中大部分可以使用函数对象的地方也都可以用真函数,所以我经常使用术语“函数对象”来表示C++函数和真的函数对象。
● 函数适配器 针对类成员函数而设计的函数适配器:mem_fun_ref及mem_fun,被mem_fun_ref及mem_fun修饰的成员函数必须是const的;针对一般函数(即非类成员函数)而设计的函数适配器:ptr_fun。
● 仿函数适配器(函数绑定器)是指能够将仿函数和另一个仿函数(或某个值或某个函数)结合在一起的仿函数。比如:not1, not2, bind1st, bind2nd等。
● 迭代器适配器(Iterator Adapters)有三种:Insert iterators ,stream iterators ,Reverse iterators。
● 标准容器适配器 有三种:栈(stack)、队列(queue)和优先队列(priority_queue)。
● string和wstring
关于string的任何东西都可以相等地应用到它的宽字符兄弟 wstring。类似地,任何时候提及string和char或char*之间的关系,对于wstring和wchar_t或wchar_t*之间的关序也是正确的。因为string和wstring都是同一个模板basic_string的实例化。
容器
条款1:仔细选择你的容器
● 标准STL序列容器:vector、string、deque和list。
● 非标准STL序列容器:slist是一个单向链表,rope本质上是一个重型字符串。
● 标准STL关联容器:set、multiset、map和multimap。
● 非标准STL关联容器:hash_set、hash_multiset、hash_map和hash_multimap。
● 几种标准非STL容器,包括数组、bitset、valarray、stack、queue和priority_queue。
标准的连续内存容器在一个或多个(动态分配)的内存块中保存它们的元素,有vector、string和deque及rope。
基于节点的容器指在每个内存块(动态分配)中只保存一个元素,list、slist及所有的标准关联容器都是此类型。
● 你需要“可以在容器的任意位置插入一个新元素”的能力吗?如果是,则你需要序列容器。
● 你关心元素在容器中的顺序吗?如果不,散列容器就是可行的选择;否则,你要避免使用散列容器。
● 必须使用标准C++中的容器吗?如果是,就可以除去散列容器、slist和rope。
● 你需要哪一类迭代器?如果必须是随机访问迭代器,在技术上你就只能限于vector、deque和string。
● 当插入或者删除数据时,是否非常在意容器内现有元素的移动?如果是,你就必须放弃连续内存容器。
● 容器中的数据的内存布局需要兼容C吗?如果是,你就只能用vector。
● 查找速度很重要吗?如果是,你就应该看看散列容器,排序的vector和标准的关联容器——大概是这个顺序。
● 你介意如果容器的底层使用了引用计数吗?如果是,你就得避开string,你可以考虑使用vector<char>。
● 你需要插入和删除的事务性语义吗?即你需要有可靠地回退插入和删除的能力吗?如果是,你就需要使用基于节点的容器。如果你需要多元素插入的事务性语义,你就应该选择list,因为list是唯一提供多元素插入事务性语义的标准容器。
● 你要把迭代器、指针和引用的失效次数减到最少吗?如果是,你就应该使用基于节点的容器,因为在这些容器上进行插入和删除不会使迭代器、指针和引用失效(除非它们指向你删除的元素)。一般来说,在连续内存容器上插入和删除会使所有指向容器的迭代器、指针和引用失效。
● 你需要具有以下特性的序列容器吗:1)可以使用随机访问迭代器;2)只要没有删除而且插入只发生在容器结尾,指针和引用的数据就不会失效。如果你遇到这种情况,deque就是你梦想的容器。
条款2:小心对“容器无关代码”的幻想
不同的容器是不同的,而且它们的优点和缺点有重大不同;它们并不被设计成可互换。
STL是建立在泛化之上的。数组泛化为容器,参数化了所包含的对象的类型;函数泛化为算法,参数化了所用的迭代器的类型;指针泛化为迭代器,参数化了所指向的对象的类型。
条款3:使容器里对象的拷贝操作轻量而正确
容器容纳了对象,但不是你给它们的那个对象。此外,当你从容器中获取一个对象时,你所得到的对象也不是容器里的那个对象。取而代之的是,当你向容器中添加一个对象(比如通过insert或push_back等),进入容器的是你指定的对象的拷贝。拷进去,拷出来,这就是STL的工作方式。是的,STL容器使用了拷贝,但是别忘了一个事实:比起数组它们仍然是一个进步。
条款4:用empty来代替检查size()是否为0
对于判断容器是否为空,你应该首选empty成员函数。而且理由很简单:对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现,size花费了线性时间。
不管发生了什么,如果你用empty()来代替检查是否size() == 0,你都不会出错。
条款5:尽量使用区间成员函数代替它们的单元素兄弟
所谓区间成员函数是指对容器的某一段区间的元素进行操作的函数,而单元素成员函数是指只对容器的某一个元素进行操作的函数。一般来说使用区间成员函数可以输入更少的代码;同时区间成员函数使代码更清晰更直接了当。
注:几乎所有目标区间被插入迭代器指定的copy的使用,都可以调用区间成员函数来替代。
条款6:警惕C++最令人恼怒的解析
假设你有一个存有int型数据的文件,你想要把那些int拷贝到一个list中。这看起来像是一个合理的方式:
ifstream dataFile("ints.dat");
list<int> da
用括号包围一个实参的声明是不合法的,但用括号包围一个函数调用的观点是合法的。语句第一个参数是实参,而第二个参数是一个函数指针。所以我们可以通过为第一个实参增加一对括号,强迫编译器以我们的方式看事情:
list<int> da
一个更好的解决办法是在数据声明中从时髦地使用匿名istream_iterator对象后退一步,仅仅给那些迭代器名字。以下代码到哪里都能工作:
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> da
条款7:当使用new得到指针的容器时,记得在销毁容器前delete掉那些指针
下面代码直接导致一个内存泄漏:
void doSomething()
{
vector<Widget*> vwp;
for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)
vwp.push_back(new Widget);
... // 使用vwp
} // Widgets在这里泄漏了!
幸运的是,我们完全可以使用智能指针避免内存泄露。如利用Boost的shared_ptr,本条款的原始例子可以重写为这样:
void doSomething()
{
typedef boost::shared_ ptr<Widget> SPW; //SPW = "shared_ptr to Widget"
vector<SPW> vwp;
for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)
vwp.push_back(SPW(new Widget)); // 从一个Widget建立SPW, 然后进行一次push_back
... // 使用vwp
} // 这里没有Widget泄漏,甚至不会在上面的代码中抛出异常
● 我们需要记住的所有事情就是STL容器很智能,但它们没有智能到知道是否应该删除它们所包含的指针。当你要删除指针的容器时要避免资源泄漏,你必须用智能引用计数指针对象(比如Boost的shared_ptr)来代替那些指针,或者你必须在容器销毁前手动删除容器中的每个指针。
条款8:永不建立auto_ptr的容器
auto_ptr的容器(COAPs)是禁止的;任何试图使用它们的代码都不能编译。当你拷贝一个auto_ptr时,auto_ptr所指向对象的所有权被转移到拷贝的auto_ptr,而被拷贝的源auto_ptr被设为NULL。即:拷贝一个auto_ptr将改变它的值:
auto_ptr<Widget> pw1(new Widget); // pw1指向一个Widget
auto_ptr<Widget> pw2(pw1); // pw2指向pw1的Widget; pw1被设为NULL(Widget的所有权从pw1转移到pw2)
pw1 = pw2; // pw1现在再次指向Widget;pw2被设为NULL
条款9:在删除容器的选项时仔细选择
我们有下列结论:
● 去除一个容器中有特定值的所有对象:
如果容器是vector、string或deque,使用erase-remove惯用法(参见条款32)。
如果容器是list,使用list::remove。
如果容器是标准关联容器,使用它的erase成员函数。
● 去除一个容器中满足一个特定判定式的所有对象:
如果容器是vector、string或deque,使用erase-remove_if惯用法(用法可参考erase-remove惯用法)。
如果容器是list,使用list::remove_if。
如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。
● 在循环内做某些事情(除了删除对象之外):
如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你的迭代器。
如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。
条款10:注意分配器的协定和约束
条款11:理解自定义分配器的正确用法
条款12:对STL容器线程安全性的期待现实一些
标准C++的世界是相当保守和陈旧的。所有可执行文件都是静态链接;不存在内存映射文件和共享内存;没有窗口系统;没有网络;没有数据库;没有其他进程。当发现标准没有提到任何关于线程的东西时你不该感到惊讶。
● 当涉及到线程安全和STL容器时,你可以确定库实现允许在一个容器上的多读取者和不同容器上的多写入者。但你不能希望库消除对手工并行控制的需要,而且你完全不能依赖于任何线程支持,即你必须自己实现线程安全。
vector和string
vector和string被设计为代替大部分数组的应用。
● 要记住c++标准程序库,basic_string<>被定义为所有字符串类型的基本模板类型,而:
1. string是针对char而预定义的特化版本:namespace std { typedef basic_string<char> string; }
2. wstring是针对wchar_t而预定义的特化版本:namespace std { typedef basic_string<wchar_> wstring; }
● string与char*不同,当不预先给string定义的对象足够的空间时,系统不会在string字符串尾部自动添加'