Effective stl---笔记
1 只有序列容器支持push_front或push_back,
只有关联容器支持count和lower_bound,等等.
2 (条款1解释了deque是唯一一个在迭代器失效的
情况下指针和引用仍然有效的东西) 【不作为特例】
3 迭代器/指针/参考的失效规则
4 typedef代码封装
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
更高等级的封装,限制权限,使用class CustomList含有List
5 nth_element算法 【让第n+1大/小的数据,出现在v[n],且它是数据的大小分水岭,两边数据不保证有序】
6 拷进去,拷出来。这就是STL的方式。是的,拷贝对象是STL的方式。
7 基类对象容器,当然由于继承的存在,拷贝会导致分割。【splice】
8 我们也可以建立一个可以足够包含maxNumWidgets个Widget的空vector,但没有构造Widget:
vector<Widget> vw;
vw.reserve(maxNumWidgets); // reserve的详细信息请参见条款14
【比数组文明多了(数组直接用默认构造函数构造对象了)】
9 尽量调用empty(splice导致不能常数时间知道size是否为0,必须要去计数)【区间插入?】
10 第一,它提供给我一个机会来提醒你assign成员函数的存在,
太多的程序员没注意到这是一个很方便的方法。
它对于所有标准序列容器(vector,string,deque和list)都有效。
【替代数据集】
11 v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(),
back_inserter(v1));
写这些仍然比写assign的调用要做更多的工作。
此外,虽然在这段代码中没有表现出循环,在copy中的确存在一个循环(参见条款43)。
结果,效率损失仍然存在。在这里,我要离题一下来指出几乎所有目标区间是通过
插入迭代器(比如,通过inserter,back_inserter或front_inserter)指定的copy的使用
都可以——应该——通过调用区间成员函数来代替。
比如这里,这个copy的调用可以用一个insert的区间版本代替:
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
12 一般来说使用区间成员函数可以输入更少的代码。
区间成员函数会导致代码更清晰更直接了当。
13 ● 区间构造。所有标准容器都提供这种形式的构造函数:
container::container(InputIterator begin, // 区间的起点
InputIterator end); // 区间的终点
如果传给这个构造函数的迭代器是istream_iterators或istreambuf_iterators(参见条款29),
你可能会遇到C++的最惊异的解析,原因之一是你的编译器可能会因为把这个构造看作一个函数
声明而不是一个新容器对象的定义而中断。条款6告诉你需要知道所有关于解析的东西,包括怎么对付它。
● 区间插入。所有标准序列容器都提供这种形式的insert:
void container::insert(iterator position, // 区间插入的位置
InputIterator begin, // 插入区间的起点
InputIterator end); // 插入区间的终点
关联容器使用它们的比较函数来决定元素要放在哪里,所以它们了省略position参数。
void container::insert(lnputIterator begin, InputIterator
end);
当寻找用区间版本代替单元素插入的方法时,不要忘记有些单元素变量用采用不同的函数名伪装它们自己。
比如,push_front和push_back都把单元素插入容器,即使它们不叫insert。如果你看见一个循环调用push_front或
push_back,或如果你看见一个算法——比如copy——的参数是front_inserter或者back_inserter,你就发现了一个
insert的区间形式应该作为优先策略的地方。
● 区间删除。每个标准容器都提供了一个区间形式的erase,但是序列和关联容器的返回类型不同。
序列容器提供了这个:
iterator
container::erase(iterator begin, iterator end);
而关联容器提供这个:
void
container::erase(iterator begin, iterator end);
为什么不同?解释是如果erase的关联容器版本返回一个迭代器(被删除的那个元素的下一个)会招致一个无法
接受的性能下降。我是众多发现这个徒有其表的解释的人之一,但标准说的就是标准说的,标准说erase的序列
和关联容器版本有不同的返回类型。
这个条款的对insert的性能分析大部分也同样可以用于erase。单元素删除的函数调用次数仍然大于一次调用区间
删除。当使用单元素删除时,每一次元素值仍然必须向它们的目的地移动一位,而区间删除可以在一个单独的
移动中把它们移动到目标位置。
关于vector和string的插入和删除的一个论点是必须做很多重复的分配。(当然对于删除,会发生重复的回收。)
那是因为用于vector和string的内存自动增长来适应于新元素,但当元素的数目减少时它不自动收缩。(条款17描
述了你怎么减少被vector或string持有的不必要的内存。)
一个非常重要的区间erase的表现是erase-remove惯用法。你可以在条款32了解到所有关于它的信息。
● 区间赋值。就像我在这个条款的一开始提到的,所有标准序列容器都提供了区间形式的assign:
void container::assign(InputIterator begin,
InputIterator end);
所以现在我们明白了,尽量使用区间成员函数来代替单元素兄弟的三个可靠的论点。
区间成员函数更容易写,它们更清楚地表达你的意图,而且它们提供了更高的性能。那是很难打败的三驾马车。
14 ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
命名迭代器对象的使用和普通的STL编程风格相反,但是你得判断这种方法对编译器和必须使用编译器的人
都模棱两可的代码是一个值得付出的代价。
【注意stream类似的迭代器,都给予命名】
15 它们能告诉你所容纳的对象类型(通过value_type的typedef)
16 销毁new 使用foreach 和
模板函数对象
struct xx
{
template<typename T>
void operator()(const T *p) const
{
delete p;
//p = NULL;
//置null没意义,且是const,小心多次delete
}
}
如果在foreach之前抛异常,可以使用智能指针boost shared_ptr来避免泄漏。
17 如果你有一个连续内存容器(vector、deque或string——参见条款1),
最好的方法是erase-remove惯用法(参见条款32):
c.erase(remove(c.begin(), c.end(), 1963), // 当c是vector、string
c.end()); // 或deque时,
// erase-remove惯用法
// 是去除特定值的元素
// 的最佳方法
这方法也适合于list,但是,正如条款44解释的,list的成员函数remove更高效:
c.remove(1963); // 当c是list时,
// remove成员函数是去除
// 特定值的元素的最佳方法
对于关联容器,解决问题的适当方法是调用erase:
c.erase(1963); // 当c是标准关联容器时
// erase成员函数是去除
// 特定值的元素的最佳方法
18 因为它为vector、string和deque产生未定义的行为!要记得对于那样的容器,
调用erase不仅使所有指向被删元素的迭代器失效,也使被删元素之后的所有迭代器失效。
19 如果我们观察在本条款中提到的所有东西,我们得出下列结论:
● 去除一个容器中有特定值的所有对象:
如果容器是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时记得后置递增它。
20 当涉及到线程安
全和STL容器时,你可以确定库实现允许在一个容器上的多读取者和不同容器上的多写入者
【使用锁 RAII机制在构造函数里上锁(传容器为参数)】
21 3. 你必须确保只delete一次。如果一个分配被删除了不止一次,结果也会未定义。
【多次delete空不会产生问题】
22 string使用引用计数的话,多线程下不安全。可以用vector<char> 替代。
【对于多线程,我们应该设计尽量避免竞态条件,此是根本,谨慎设计共享变量】
23 使用reserve来避免不必要的重新分配
24 事实上,让C风格API把数据放入一个vector,然后拷到你实际想要的STL容器中的主意总是有效的。
【同理,可以将stl容器通过vector再转回C Api中,vector是桥】
if( !v.empty() )
{
dosomething(&v[0],v.size()); //不要用v.begin()
}
25 vector<Contestant>(contestants).swap(contestants); ???
所以当你想对vector和string进行“收缩到合适”时,就考虑“交换技巧”。
string().swap(s); // 清除s而且最小化它的容量
26 标准库提供了两个替代品,它们能满足几乎所有需要。第一个是deque<bool>。deque提供了几乎所有vector所
提供的(唯一值得注意的是reserve和capacity),而deque<bool>是一个STL容器,它保存真正的bool值。当
然,deque内部内存不是连续的。所以不能传递deque<bool>中的数据给一个希望得到bool数组的C API[1](参
见条款16),但你也不能让vector<bool>做这一点,因为没有可移植的取得vector<bool>中数据的方法。
【vector<bool>不满足STL容器的必要条件,你最好不要使用它。】
【vector<bool>的位域实现。】
如果不在乎没有迭代器和动态改变大小,你也许会发现bitset正合你意。
27 《绿野仙踪》。全世界最有名的童话
片之一,荣获1939年奥斯卡最佳电影歌曲和最佳电影配乐。其中的多色马,关联容器是不同颜色的生物。真
的,它们共享了序列容器的很多特性,但它们在很多基本方法上不同
28 find
对“相同”的定义是相等,基于operator==。set::insert对“相同”的定义是等价,通常基于operator<。
等价一般在每种标准关联容器(比如,set、multiset、map
和multimap)的一部分——排序顺序方面有意义
29 。set<Widget>的默认比较函数是less<Widget>,而默认的less<Widget>简单地对Widget调用
operator<,所以w1和w2关于operator<有等价的值如果下面表达式为真:
!(w1 < w2) // w1 < w2时它非真
&& // 而且
!(w2<w1) // w2 < w1时它非真
在一般情况下,用于关联容器的比较函数不是operator<或甚至less,它是用户定义的判断式。(关于判断式的
更多信息参见条款39。)每个标准关联容器通过它的key_comp成员函数来访问排序判断式,所以如果下式求
值为真,两个对象x和y关于一个关联容器c的排序标准有等价的值:
!c.key_comp()(x, y) && !c.key_comp()(y, x) // 在c的排序顺序中
// 如果x在y之前它非真,
// 同时在c的排序顺序中
// 如果y在x之前它非真
30 if (ciss.find("persephone") != ciss.end())... // 这个测试会成功
但如果我们用非成员的find算法,搜索会失败:
if (find(ciss.begin(), ciss.end(),
"persephone") != ciss.end())... // 这个测试会失败
那是因为“persephone”等价于“Persephone”(关于比较仿函数CIStringCompare),但不等于它(因为string
("persephone") != string("Persephone"))。这个例子演示了为什么你应该跟随条款44的建议优先选择成员函数
(就像set::find)而不是非成员兄弟(就像find)的一个理由。
容器的比较函数对象。算法find用的==
31 通过只使用一个比较函数并使用等价作为两个值“相等”的意义的仲裁者
32 你应该回忆起
set<string*> ssp;
是这个的简写:
set<string*, less<string*> > ssp;
好,为了完全准确,它是
set<string*, less<string*>, allocator<string*> > ssp;
33 令人遗憾的是,stringPtrLess不是一种类型,它是一个
函数。这就是为什么尝试使用stringPtrLess作为set的比较函数不能编译的原因,set不要一个函数,它要的是能在内部用实例化建立函数的一种类型。
【for_each可以使用函数】
struct DereferenceLess {
template <typename PtrType>
bool operator()(PtrType pT1, // 参数是值传递的,
PtrType pT2) const
// 因为我们希望它们
{ // 是(或行为像)指针
return *pT1 < *pT2;
}
};
34 比较函数总应该对相等的值返回false。
35 强制类型转换,产生一个临时对象
if (i != se.end()) { // 同上,
((Employee)(*i)).setTitle("Corporate
Deity"); // 但使用C
} //
强转语法
【类似于】
if (i != se.end()){
Employee tempCopy(*i); // 把*i拷贝到tempCopy
tempCopy.setTitle("Corporate
Deity"); // 修改tempCopy
}
36 如果你要总是可以工作而且总是安全地改变set、multiset、
map或multimap里的元素,按五个简单的步骤去做:
1. 定位你想要改变的容器元素。如果你不确定最好的方法,条款45提供了关于怎样进行适当搜寻的指导。
2. 拷贝一份要被修改的元素。对map或multimap而言,确定不要把副本的第一个元素声明为const。毕竟,你想要改变它!
3. 修改副本,使它有你想要在容器里的值。
4. 从容器里删除元素,通常通过调用erase(参见条款9)。
5. 把新值插入容器。如果新元素在容器的排序顺序中的位置正好相同或相邻于删除的元素,使用insert的“提示”形式把插入的效率从对数时间改进到分摊的常数时间。使用你从第一步获得的迭代器作为提示。
37 EmpIDSet se; // 同前,se是一个以ID号
// 排序的雇员set
Employee selectedID; // 同前,selectedID是一个带有
// 需要ID号的雇员
...
EmpIDSet::iterator i =
se.find(selectedID); // 第一步:找到要改变的元素
if (i!=se.end()){
Employee e(*i); // 第二步:拷贝这个元素
se.erase(i++); // 第三步:删除这个元素;
// 自增这个迭代器以
// 保持它有效(参见条款9)
e.setTitle("Corporate Deity"); // 第四步:修改这个副本
se.insert(i, e); // 第五步:插入新值;提示它的位置
// 和原先元素的一样
}
你将原谅我以这种方法放它,但要记得关键的事情是对于set和multiset,如果你进行任何容器元素的原地修改,你有责任确保容器保持有序
【原地修改不要修改set的key】
38 一旦你写了DataCompare,东西都很好的依序排列了。而一旦位置合适了,只要你的程序按照
101页描述的阶段方式使用数据结构,它们往往比相应的使用真的map的设计运行得更快而且使用更少内存。
如果你的程序不是按照阶段的方式操作数据结构,那么使用有序vector代替标准关联容器几乎可以确定是在
浪费时间。
39 如果你要更新已存在的map元素,operator[]更好,但如果你要增
加一个新元素,insert则有优势。
【人如其名】
40 尽量用iterator代替const_iterator,reverse_iterator和
const_reverse_iterator
41 因为它可能花费线性时间的代价来产生一个和const_iterator等价的iterator,并且因为如果不能访问
const_iterator所属的容器这个操作就无法完成。从这个角度出发,也许你需要重新审视你从const_iterator产生
iterator的设计。事实上那样的考虑帮助激发了条款26,它建议你当处理容器时尽量用iterator代替const和reverse迭代器。
42 reverse_iterator的base成员函数返回一个“对应的”iterator的说法并不准确。对于插入操
作而言,的确如此;但是对于删除操作,并非如此。当需要把reverse_iterator转换成iterator的时候,有一点非
常重要的是你必须知道你准备怎么处理返回的iterator,因为只有这样你才能决定你得到的iterator是否是你需
要的。
【并不是真正对着同一个位置】
43 当你了解它之后,你也应该考虑把ostreambuf_iterator用于相应的无格式一个一个字符输出的作。它们没有了
ostream_iterator的开销(和灵活性),所以它们通常也做得更好。
44
无论何时你使用一个要求指定目的区间的算法,确保目的区间已经足够大或者在算法执行时可以增加大小。
如果你选择增加大小,就使用插入迭代器,
比如ostream_iterators或从back_inserter、front_inserter或inserter返回的迭代器。
这是所有你需要记住的东西。
45
● 如果你需要在vector、string、deque或数组上进行完全排序,你可以使用sort或stable_sort。
● 如果你有一个vector、string、deque或数组,你只需要排序前n个元素,应该用partial_sort。
● 如果你有一个vector、string、deque或数组,你需要鉴别出第n个元素或你需要鉴别出最前的n个元素,
而不用知道它们的顺序,nth_element是你应该注意和调用的。
● 如果你需要把标准序列容器的元素或数组分隔为满足和不满足某个标准,你大概就要找partition或stable_partition。
● 如果你的数据是在list中,你可以直接使用partition和stable_partition,你可以使用list的sort来代替sort和stable_sort。
如果你需要partial_sort或nth_element提供的效果,你就必须间接完成这个任务,但正如我在上面勾画的,会有很多选择。
46 需要更少资源(时间和空间)的算法列在需要更多的前面:
1. Partition
2. stable_partition
3. nth_element
4.
partial_sort
5. sort
6. stable_sort
47 一旦你知道了remove不能“真的”从一个容器中删除东西,和erase联合使用就变成理所当然了。你要记住的
唯一其他的东西是remove不是唯一这种情况的算法。另外有两种“类似remove”的算法:remove_if和unique。
remove和remove_if之间的相似性很直截了当。所以我不会细讲,但unique行为也像remove。它用来从一个区
间删除东西(邻近的重复值)而不用访问持有区间元素的容器。结果,如果你真的要从容器中删除元素,你
也必须成对调用unique和erase,unique在list中也类似于remove。
正像list::remove真的删除东西(而且比eraseremove惯用法高效得多)。
list::unique也真的删除邻近的重复值(也比erase-unique高效)。
【成员函数好,成员函数好】
48 不管你怎么选择处理动态分配指针的容器,通过引用计数智能指针、在调用类似remove的算法前手动删除和
废弃指针或者一些你自己发明的技术,本条款的指导意义依然一样:提防在指针的容器上使用类似remove的算法。
没有注意这个建议的人只能造成资源泄漏
【remove的机制造成】
49 我知道你们中的一部分会用蛮力记忆,所以这里有一个只能操作有序数据的算法的表:
binary_search lower_bound
upper_bound equal_range
set_union set_intersection
set_difference set_symmetric_difference
merge inplace_merge
includes
50 11个名字带“copy”的算法:
copy copy_backward
replace_copy reverse_copy
replace_copy_if unique_copy
remove_copy rotate_copy
remove_copy_if partial_sort_copy
unintialized_copy
51 用accumulate或for_each来统计区间
52 BPFC::operator()的实现例证了BPFC所有的虚函数是怎么实现的:它们调用了在BPFCImpl中它们真的虚函
数。结果是仿函数类(BPFC)是小而单态的,但可以访问大量状态而且行为多态。
我在这里忽略了很多细节,因为我勾勒出的基本技术在C++圈子中已经广为人知了。《Effective C++》的条款34中有。
在Gamma等的《设计模式》[6]中,这叫做“Bridge模式”。Sutter在他的《Exceptional C++》[8]中叫它“Pimpl惯用法”.
从STL的视角看来,要记住的最重要的东西是使用这种技术的仿函数类必须支持合理方式的拷贝。如果你是
上面BPFC的作者,你就必须保证它的拷贝构造函数对指向的BPFCImpl对象做了合理的事情。也许最简单的合理的东西是引用计数,使用 类似Boost的shared_ptr,你可以在条款50中了解它.
实际上,对于本条款的目的,唯一你必须担心的是BPFC的拷贝构造函数的行为,因为当在STL中被传递或从
一个函数返回时,函数对象总是被拷贝——值传递,记得吗?那意味着两件事。让它们小,而且让它们单态。
53 (not1、not2、bind1st和bind2nd)都需要存在某些typedef
54 这是STL里的一个普遍习惯:函数和函数对象总使用用于非成员函数
的语法形式调用
55 了解使用ptr_fun、mem_fun和mem_fun_ref的原因
56 不要通过把less的定义当儿戏来误导那些程序员。如果你使用less(明确或者隐含),保证它表示operator<。
如果你想要使用一些其他标准排序对象,建立一个特殊的不叫做less的仿函数类
57
● 效率:算法通常比程序员产生的循环更高效。
● 正确性:写循环时比调用算法更容易产生错误。
● 可维护性:算法通常使代码比相应的显式循环更干净、更直观。
58
几乎不可能被打败的sort及其同族算法(比如,
stable_sort(),nth_element()等,参见条款31);适用于有序区间的搜索算法(比如,binary_search,
lower_bound等,参见条款34和35)也一样好;就算是很平凡的任务,比如从连续内存容器中除去一些对象,
使用erase-remove惯用法都比绝大多数程序员写的循环更高效
【尽量使用stl提供的算法,人那是计算机科学家写的】
59 在算法调用与手写循环正在进行的较量中,关于代码清晰度的底线是:这完全取决于你想在循环里做的是什
么。如果你要做的是算法已经提供了的,或者非常接近于它提供的,调用泛型算法更清晰。如果循环里要做
的事非常简单,但调用算法时却需要使用绑定和适配器或者需要独立的仿函数类,你恐怕还是写循环比较
好。最后,如果你在循环里做的事相当长或相当复杂,天平再次倾向于算法。因为长的、复杂的通常总应该
封装入独立的函数。只要将循环体一封装入独立函数,你几乎总能找到方法将这个函数传给一个算法(通常
是for_each),以使得最终代码直截了当。
只要能用高层次的术语——如insert、find和for_each,
取代了低层次的词汇——如for、while和do,我们就提升了软件的【【抽象】】层次
60 当面临着STL算法和同名的容器成员函数间进行选择时,你应该尽量使用成员函数。几
乎可以肯定它更高效,而且它看起来也和容器的惯常行为集成得更好。
【私人定制服务】
61 count和find是线性时间的,但有序区间的
搜索算法(binary_search、lower_bound、upper_bound和equal_range)是对数时间的。
62 我会简单地说明count和
find算法都用相等来搜索,
而binary_search、lower_bound、upper_bound和equal_range则用等价
63 lower_bound回答这个问题:“它在吗?如果
是,第一个拷贝在哪里?如果不是,它将在哪里?”
保持比
较函数同步不是火箭发射,但却是另一个要记住的东西,而且我想你已经有很多需要你记的东西了。
64 使用equal_range。equal_range返回一对迭代器,第一个等于lower_bound返回的迭代
器,第二个等于upper_bound返回的(也就是,等价于要搜索值区间的末迭代器的下一个)。因此,
equal_range,返回了一对划分出了和你要搜索的值等价的区间的迭代器。一个名字很好的算法,不是吗?
65 通常我们有一个容器,而不是一个区间。在这种情况下,我们必须区别序列和关联容器。对于标准的序列容器(vector、string、deque和list),你
应该遵循我在本条款提出的建议,使用容器的begin和end迭代器来划分出区间。
这种情况对标准关联容器(set、multiset、map和multimap)来说是不同的,因为它们提供了搜索的成员函
数,它们往往是比用STL算法更好的选择。条款44详细说明了为什么它们是更好的选择,简要地说,是因为
它们更快行为更自然。幸运的是,成员函数通常和相应的算法有同样的名字,所以前面的讨论推荐你使用的
算法count、find、equal_range、lower_bound或upper_bound,在搜索关联容器时你都可以简单的用同名的成员
函数来代替。
调用binary_search的策略不同,因为这个算法没有提供对应的成员函数。要测试在set或map中是否存在某个
值,使用count的惯用方法来对成员进行检测:
。。。。
要测试某个值在multiset或multimap中是否存在,find往往比count好,因为一旦找到等于期望值的单个对象,
find就可以停下了,而count,在最遭的情况下,必须检测容器里的每一个对象。(对于set和map,这不是问
题,因为set不允许重复的值,而map不允许重复的键。)
但是,count给关联容器计数是可靠的。特别,它比调用equal_range然后应用distance到结果迭代器更好。首
先,它更清晰:count 意味着“计数”。第二,它更简单;不用建立一对迭代器然后把它的组成(译注:就
条款45:注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别
是first和second)传给distance。第三,它可能更快一点。
你想知道的
使用的算法使用的成员函数
在无序区间在有序区间在set或map上在multiset或multimap上
期望值是否存在? find binary_search count find
期望值是否存在?
如果有,第一个等
于这个值的对象在
哪里?
find equal_range find find或lower_bound(参见下面)
第一个不在期望值
之前的对象在哪
里?
find_if lower_bound lower_bound lower_bound
第一个在期望值之
后的对象在哪里?
find_if upper_bound upper_bound upper_bound
有多少对象等于期
望值?
count equal_range,然后distance count count
等于期望值的所有
对象在哪里?
find(迭代) equal_range equal_range equal_range
66 把函数对象作为算法的参数所带来的不仅是巨大的效率提升。在让你的代码可以编译方面,它们也更稳健。
当然,真函数很有用,但是当涉及有效的STL编程时,函数对象经常更有用。
【他们可能会内联,而普通函数是函数指针,每次的指针调用】
67 写可读性高的代码
68 在这里,它叫做_Tree,但我知道的其他实现使用__tree或__rb_tree,后者反映出
使用红-黑树——在STL实现中最常使用的平衡树类型。