都是一些简单的总结,可以帮助回忆当时看书的知识点~
##第1条:慎重选择容器类型
STL容器的分类远比我想像中的多。别人意外的是stack,queue等不是STL容器,但这不是这章的重点。
- 你是否需要在容器的任意位置插入新元素?如果是,就选择序列容器。‘
- 你是否关心容器中的元素是如何排序的?如是不关心,哈希容器是一个可能的选择;否则,你要避免哈希容器。
- 你需要哪种类型的迭代器?如果必须是随机访问迭代器,就只能限定的vector、deque和string。如果要求使用双向迭代器,就不能使用slist。
- 当发生元素的插入或删除操作时,避免移动容器中的原来的元素是否很重要?如果是,就要避免连续内存的容器。
- 容器中的数据布局是否需要和C兼容?如果是,只能选择vector。
- 元素的查找速度是否是关键考虑因素?如果是,就要考虑哈希容器、排序的vector(二分查找)和标准关联容器。
- 你是否介意容器内使用引用计数?如果是,就要避免使用string和rope,可以考虑vector替代。
- 对插入和删除操作,你需要事务语义(可以回滚)吗?如果需要,list将是你的最好选择。
- 你需要使迭代器、指针和引用变为无效的次数最少吗?如果是,请使用基于节点的容器。
- 如果序列容器的迭代器是随机访问类型,而且只要没有删除操作发生,且插入操作只发生在容器的末尾,则指向数据的指针和引用就不会变为无效,这样的容器是否对你有帮助?这是很特殊的情况,如果真的是这样的,deque是绝佳选择。
##第2条:不要试图编写独立于容器类型的代码
这是现在很流行的泛化(generalization)编程,但作者强调不要在这里使用。原因在于,每个容器所支持的操作不一样,就连erase、insert这样的操作在STL中也不统一。除非你的代码是给自己用的,否则不要使用,天知道那个程序员用的是什么容器!
##第3条:确保容器中的对象拷贝正确而高效
这一条是告诉我们,STL操作中,拷贝发生的频率是相当大的。指针是一个好的想法,但内存泄漏很麻烦。
##第4条:调用empty而不是检查size()是否为0
empty的存在不仅仅是为了少敲几个字符。
##第5条:区间成员函数优先于与之对应的单元素成员函数
如第3条所说,STL操作中,拷贝发生的频率是相当大的。如果用单元素一个一个地操作,这种性能上的损耗将会更多。下面是书中给出的一般区间操作。
//区间创建
container::container(Inputiterator begin, Inputiterator end);
//区间插入
void container::insert(iterator position, Inputiterator begin, Inputiterator end);
//关联容器不需要position
//区间删除
iterator container::eraser(iterator begin, iterator end);
//关联容器有所不同
void container::eraser(iterator begin, iterator end);
//区间赋值
void container::assign(Inputiterator begin, inputiterator end);
##第6条:当心C++编译器最烦人的分析机制
int g(double ());编译器认为是,g是一个函数,返回值是int,参数是一个函数指针,返回值为double,该参数为空。int g(double d) = int g(double (d)) = int g(double),之所以STL这里要提到,是因为在声明时常常被误解。比如:
ifstream dataFile(“ints.dat”);
list data(istream_iterator(dataFile), istream_iterator());
在VS中,测试中得到警告,说data被声明库函数,可能这个函数不能被调用。一个解决方法是加上括号以区分 list data((istream_iterator(dataFile)), istream_iterator());
更好的方式是不使用匿名对象。C++的一条普遍规律是,尽可能地解释为函数声明。这真是一个让人恼火的问题。
##第7条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
这个是常识。
不过每次都要写for循环,实在很麻烦。书中提出用for_each代替。
##第8条:切勿创建包含auto_ptr的容器对象
这是很危险的,以至于C++标准都禁止它。
##第9条:慎重选择删除元素的方法
没有通用的删除方法。
//如果你有一个连续的容器(vector,deque,string),最好的方法是erase-remove习惯用法
c.erase(remove(c.begin(), c.end(), 1963), c.end()); //remove返回删除后的新c.end(),在后面还有一些数据是没有清除掉的(事实上remove没有删除元素,它把找到的元素用后面的填补上)
//对于list,remove更加有效
c.remove(1963);
//对于关联容器,正确方法调用erase
c.erase(1963);
//如果找到删除的值没这么简单,remove_if配合erase会派上用场。
bool badValue(int );
c.erase(remove_if(c.begin(), c.end(), badValue), c.end()); //连续内存型容器
c.remove_if(badValue); // list
//对于关联容器,直接写for循环会比较好,但很容易写出错误的代码。比如
container<int>::iterator it = ...
c.erase(it); // 执行后it变库无效,很多程序员把这个it用到for循环中的++,立马出错!如果c是序列容器,它会返回下一个元素的迭代器,关联容器就没这么好运了。
for(AssocCaotainer<int>::iterator it = c.begin(); it != c.end(); ) {
if(badValue(*it)) {
c.erase(i++);
}
else ++i;
}
//注意下面的
for(SeqCaotainer<int>::iterator it = c.begin(); it != c.end(); ) {
if(badValue(*it)) {
i = c.erase(i);//这里绝对不要用c.erase(i++),因为像vector这类连续内存容器在插入或删除后,原来的指针或引用会无效!!!
}
else ++i;
}
##第10条:了解分配子(allocaltor)的约定和限定
##第11条:理解自定义分配子的合理用法
##第12条:切勿对STL容器的线程安全性有不切实际的依赖
一般来说,STL不是线程安全的。
##第13条:vector和string优先于动态分配的数组
很明显,在各个方面,vector和string都表现得很优秀。但是可能是受到C的影响,至今还末用到这条,现在决定,以后凡是简单的动态数组,全用vector;如果是字符串,全用string。
##第14条:使用reserve来避免不必要的重新分配
相信这一点是很容易理解的。下面几个函数是vector和string特有的,会常常用到
capacity() 返回已经分配的内存可以容纳的元素
resize(Container::size_type n) 强迫容器改变到包含n个元素状态,如果n小于capacity(),capacity()的值是不会减少。它的特点就是,执行后,调用size()就是返回n。
reserve(Container::size_type n) 强迫容器把它的容量变为至少是n。如果n小于capcity(),可能不会做什么事。
##第15条:注意string实现的多样性
string的实现中有引用计数,这一点恐怕你已经清楚了。但它的实现不同版本都不一样,直接的表现是sizeof(string)的值不同,即使相同它们的内存方案也可能不一样。通常来说,对程序员这些没什么影响。
##第16条:了解如何把vector和string的数据传给旧的API
因为它们是内存连续的容器,所以这点很好实现。
对于vector,简单采用 &v[0] 就可以了;对于string,s.c_str()是唯一方法,不要用&s[0]的方法,因为string的实现多样性,而且很有可能string内部不保证空字符结尾。如果你介意c_str()带来的性能损失,用vector代替string。
##第17条:使用“swap技巧”除去多余的容量
14条中,只有reserve可能有这个能力除去多余容量,但仅仅是可能。比如
vector(v).swap(v);
vector(v)声明一个隐式对象,有v初始化,然后再与v交换。请放心,原来的指针和引用都能工作。string也类似。string(s).swap(s);
##第18条:避免使用vector
你也许在怀疑,bool是内置类型,为什么不行?STL的实现者原先对bool做了压缩处理,每个元素只占2个bit,标准的是点8个bit,这样指针指向就出错了。总之C++标准委员会声明vector是一个失败 的产物,你就不要使用了。取而代之deque或bitset。
##第19条:理解相等(equality)和等价(equivalence)的区别
相等的概念是基于operator==;等价关系是以“的已排序的区间中对象值的相对顺序”为基础。在关联容器中,需要指定一个排序类(注意,不是函数),默认的比较函数是less。对于w1和w2,等价的关系表示库 !(w1 < w2)&&!(w2 < w1)。如果人为故意,等价和相等不一样。
STL中关联函数的比较函数只有一个,当然,一个足够了。
##第20条:为包含指针的关联容器指定比较类型
上面一条提到比较函数,通常有一个默认的比较函数。考虑set < string*> s,如果你向里面插入数据后,你会发现没有按照顺序输出。原因在于,s是这样声明的set < string*, less < string*> s,比较的是string*,结果输出是按指针地址的值来排序。所以,对于指针关联容器,我们要指定自己的排序类型
struct StringPtrLess : pblic binary_function < const string*, const string*, bool>
{
bool operator()(const string* ps1, const string* ps2) const
{
return *ps1 < *ps2;
}
};
set < string*, StringPtrLess> s;
对于map,用法类似。
##第21条:总是让比较函数在等值的情况下返回false
如果把它代入到等价的关系式,这就明白了。
##第22条:切勿直接修改set或multiset中的键
键 = key。key是排序的依据,随意修改会打乱容器结构。
##第23条:考虑用排序vector替代关联容器
连续内存窗口有它的优势,调用一些排序算法和查找算法可以达到set和map的效果,不过大量元素插入和删除的代价太高了,只能用来查找。
##第24条:当效率至关重要时,请在map::operator[]与map::insert之间谨慎选择
产生这个误会的原因是,operator[]也可以达到insert的效果,但它的代价确实偏高,却常常不被查觉。本条的要点概括为:如果要更新一个已有的映射表元素,优先使用operator[];如果是添加一个新元素(也许你会觉得insert有些麻烦
typedef map < int, double> MyMap;
Map m;
m[1] = 1.0; // m.insert(Map::value_type(1, 1,0);
m[2] = 1.1; // m.insert(Map::value_type(1, 1,1);
… …)
##第25条:熟透非标准的哈希容器
哈希容器没包括在C++标准库中,据说是因为标准委员会为了赶时间。
##第26条:iterator优于const_iterator、reverse_iterator以及const_reverse_iterator
iterator可以直接转换到其它的iterator,反过来则不行。
##第27条:使用distance和advance将容器的const_iterator转换成iterator
代码上看去像
const_itrator ci = …
iterator i(c.begin());
advance(i, distance(i ,ci));
如果能避免const_iterator,就没有这些事了。
##第28条:正确理解由reverse_iterator的base()成员函数所生成的iterator用法
base()返回的iterator指向的位置不是原来的位置,而是后面一个元素,这一点很迷惑人。
##第29条:对于逐个字符的输入请考虑使用istreambuf_iterator
如果纯粹读文件中的内容,不用格式化(如读出一个整数,路过空格等),首选istreambuf_iterator代替istream_iterator。当然,与之对应的还有ostreambuf_iterator替代ostream_iterator.
##第30条:确保目标区间足够大
如果你通过STL算法来向容器中增加元素,那么这是一个值得重视的问题。虽然STL能自动管理内存,但我们常常高估了它的能力。常见的例子是:
int trans(int x);
vector<int> v1, v2;
... ...
transform(v1.begin(), v1.end(), v2.end(), trans); //将v1的值经过trans变换加到v2.end()上
在STL算法中,迭代的样子是 v2.end()++。这时你就会明白,v2.end()后面没有可能内存,会访问非法地址。这时你在前面用 v2.reserve(v2.size()+v1.size()),很抱歉,又错了,原因在于后面的空间没有初始化,结果将未知。正确的方法是
transform(v1.begin, v1.end(), back_insert(v2), trans);
这样返回迭代器前v2.push_back()将被调用,这样插入就正确了。类似的还有front_insert, inserter等
##第31条:了解各种与排序有关的选择
STL提供的排序算法要比你自己写的好得多,何必自寻烦恼呢?共有6种可用的排序算法(按消耗资源由少至多排列),partition, stable_partition, nth_element, partial_sort, sort, stable_sort。并不是说,消耗少的就一定好,是因为它们实现的功能有限。在不同的场合请选择合适的算法。
##第32条:如果确实要删除元素,则需要在remove这一类算法之后调用erase
相信你已经知道vector经典的remove-erase组合,所有的内存连续容器都适用。remove算法没有删除元素的能力,因为它不可册通过一个迭代器判断出能否调用erase,即使标准的STL容器erase使用方式也不尽相同,因此,这个工作就交给用户完成。
##第33条:对包含指针的容器使用remove这一类算法时要特别小心
对于这类容器就不应该使用remove算法,手工写for循环将是更好的选择。如果你参照remove的实现,或许你可以把for循环加以改进。
##第34条:了解哪些算法要求使用排序的区间作为参数
需要的有 binary_search, lower_bound, upper_bound, equal_range, set_union, set_intersection, set_difference, set_symmetric_difference, merge, inplace_merge, includes. unique和unique_copy虽然没有强制使用,但它们确实需要工作的排序的容器中。另一个要注意的是,上面这些算法通常都需要一个比较函数,而且要与提供给排序的比较函数相一致。
##第35条:通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较
这种程序,说难也难,说简单也简单。
##第36条:理解copy_if算法的正确正确实现
首先,标准STL中没有copy_if,但各大厂商的“好事者”几乎者实现了它。本书作者也加入到这个行列中了。
##第37条:使用accumulate或者for_each进行区间统计
accumulate绝不仅仅是将几个数加在一起求合那么简单,可以自定义运算子。相比之下,for_each稍显繁琐但更灵活。原因在于 accumulate返回的是size_type,而for_each返回Function。
##第38条:遵循按值传递的原则来设计函数子类
不知道为什么,我看到很多operator()按引用传递值。
##第39条:确保判别式是“纯函数”
有几个名词可以来解释一下
判别式(predicate)返回bool类型的函数
纯函数(pure fuction)指返回值仅与输入有关
判别式类 一个函数子类,它的operator()函数是一个判别式
问题根源在于:在一个算法中,你无法确定函数子被调用多少次。像for_each这类简单的算法调用次数就是迭代器的个数,但其它的较复杂的就不一定了,比如find_if就不一样。设计函数功能单一是一个重要的原则。
##第40条:若一个类是函数子,则应使它可配接
总共有4个配接器not1, not2, bind1st, bind2nd。如果你要使用它们,你必须确保函数子类从unary_function, binary_function等继承。或者使用ptr_fun, mem_fun等(这个用起来不太方便)。对于前一种方法,有一个使用特点
1、对于无状态函数子类(指没有私有成员变量的),通常定义为struct;这是STL的风格,当然用class也完全一样。
2、一般情况下,传递给unary_function或binary_function的非指针类型需要去掉const和引用(&)部分。
struct Cmp: public std::binary_function < int, int, bool> {
bool operator()(const int& i, const int& j);
}
如果是指针,就要写成一致
struct Cmp: public std::binary_function < const int*, const int*, bool> {
bool operator()(const int* i, const int* j);
}
其实写函数子也不是很麻烦
##第41条:理解ptr_fun、mem_fun和mem_fun_ref的由来
用一句话来说就是“函数调用方式的多样性,使STL必须规定一种方式来约束”(哦,估计下次我自己看到也无法理解)。在第40条中,我们看到函数子的声明是从其它类中继承过来的,如果不继承或者是内部成员函数,可能需要这几个适配器的支持,因此,在编译无法通过的时候,查查它们的用法也许有意想不到的收获。
##第42条:确保less与operator<具有相同的语义
在排序容器中,有一个默认的比较函数子less < T>,见第20条。确保比较函数子可行性是很重要的,同时,为了避免出现混乱,less < T> (如果需要自己实现,就不能用less这个名字)与operator < 不同有歧义。
##第43条:算法调用优先于手写循环
诚然,不论是简洁,速度,正确性,算法都比手写循环好,不过现在情况好多了,函数子,算法,适配器都不是难事了。
##第44条:窗口的成员函数优先于同名算法
比如说查找,对于set,可以使用成员函数find,也可以用算法find。但是很明显,成员函数对list知道得更多技术细节,就如同手写循环和算法一样。
##第45条:正确区分count, find, binary_search, lower_bound, upper_bound和equal_range
确切地说,它们是不同的算法,作用也不同,关键在于,有时候我们可以在它们中间任选一个来解决问题,我是说在某些方面,它们的共同点。现在我还记得它们的特点,也许以后还有印象。
##第46条:考虑使用函数对象而不是函数作为STL算法的参数
STL中的sort算法比qsort快,这一点确实让人吃惊。这样你就会喜欢函数对象了。
##第47条:避免产生“直写型”(write-only)的代码
直写型 —— 直接写所有函数。这是编程风格问题,意思是请不要所STL的算法写得太复杂了,我还达不到这个境界。
##第48条:总是包含(#include)正确的头文件
不同的平台可能需要不同的头文件。
##第49条:学会分析STL相关的编译器诊断信息 stlsplit是可以选用的工具。
##第50条:boost