Effective STL学习笔记.docx
- 文档编号:4404050
- 上传时间:2022-12-01
- 格式:DOCX
- 页数:33
- 大小:506.62KB
Effective STL学习笔记.docx
《Effective STL学习笔记.docx》由会员分享,可在线阅读,更多相关《Effective STL学习笔记.docx(33页珍藏版)》请在冰豆网上搜索。
EffectiveSTL学习笔记
EMIAL:
liuhaibo256@
QQ:
95343674
msn:
liuhaibo256@
欢迎讨论
EffectiveSTL学习笔记
前言
边学习变总结,下面的内容包括从原文中摘取的我认为重要的地方和自己的理解。
没有什么章节,比较乱。
希望对stl入门学习的同学有帮助。
有一点就是我感觉原文的例子有些并不贴切,遇到不好理解的地方建议到网上找相关知识点的介绍参考学习。
请耐心看吧。
序列容器支持push_front或push_back,但关联容器不支持。
关联容器提供对数时间复杂度的lower_bound、upper_bound和equal_range成员函数,但序列容器却没有。
在一个序列容器上用一个迭代器作为参数调用erase,会返回一个新迭代器,但在关联容器上什么都不返回。
假设,然后,你希望写一段可以用在所有常用的序列容器上——vector,deque和list——的代码。
很显然,你必须使用它们能力的交集来编写,这意味着不能使用reserve或capacity(参见条款14),因为deque和list不支持它们。
由于list的存在意味着你得放弃operator[],而且你必须受限于双向迭代器的性能。
这意味着你不能使用需要随机访问迭代器的算法,包括sort,stable_sort,partial_sort和nth_element.用vector和deque都会使splice和成员函数方式的sort失败?
这里的罪魁祸首是不同的序列容器所对应的不同的迭代器、指针和引用的失效规则。
稍后你可能发现从列表的中部插入和删除客户并不像你想象的那么频繁,但你真的需要快速确定客户列表顶部的20%——一个为nth_element算法量身定做的任务.如果你使用了任何排序算法(参见条款31):
next_permutation或者previous_permutation;remove、unique或它们的同类(参见条款32);rotate或reverse等,对象会移动(拷贝)。
是的,拷贝对象是STL的方式。
第一,它提供给我一个机会来提醒你assign成员函数的存在,太多的程序员没注意到这是一个很方便的方法。
它对于所有标准序列容器(vector,string,deque和list)都有效。
当寻找用区间版本代替单元素插入的方法时,不要忘记有些单元素变量用采用不同的函数名伪装它们自己。
比如,push_front和push_back都把单元素插入容器,即使它们不叫insert。
如果你看见一个循环调用push_front或push_back,或如果你看见一个算法——比如copy——的参数是front_inserter或者back_inserter,你就发现了一个insert的区间形式应该作为优先策略的地方。
还有区间赋值的assign和区间删除的饿erase,注意对于erase序列容器和关联容器的返回值是不同的。
所以现在我们明白了,尽量使用区间成员函数来代替单元素兄弟的三个可靠的论点。
区间成员函数更容易写,它们更清楚地表达你的意图,而且它们提供了更高的性能。
那是很难打败的三驾马车。
在使用区间形式的构造函数时,如果传递的参数是istream_iterator或者istreambuf_iterator,编译器会解析为函数声明而不是对象定义。
解决的办法就是把参数单独定义,然后带入构造函数。
我们需要记住的所有事情就是STL容器很智能,但它们没有智能到知道是否应该删除它们所包含的指针。
当你要删除指针的容器时要避免资源泄漏,你必须用智能引用计数指针对象(比如Boost的shared_ptr)来代替指针,或者你必须在容器销毁前手动删除容器中的每个指针。
所有东西都是public的仿函数类的作者经常把它们声明为struct而不是class,也许只因为可以避免在基类和operator()函数前面输入“public”。
把这样的仿函数声明为class还是struct纯粹是一个个人风格问题。
一般来说,传给unary_function或binary_function的非指针类型都去掉了const和引用,但是如果是指针的话const和*是不能省略的。
不要忘记所有使用这些unary_function和binary_function基类基本理由的冗繁的文字。
这些类提供函数对象适配器需要的typedef,所以从那些类继承产生可适配的函数对象。
可适配性是重要的,每次你写仿函数类时都应该努力促进它。
如果想删除容器:
如果你有一个连续内存容器(vector、deque或string——参见条款1),最好的方法是erase-remove惯用法:
container
c.remove(1982);如果c是标准关联容器,成员函数remove是最高效的方法,只需要花费对数时间。
而在map和mutimap上调用remove将是不能编译通过的,在set和mutiset上调用remove可能是无法编译通过的。
因为remove算法可能覆盖容器值(参见条款32),潜在地破坏容器。
标准关联容器的erase的返回类型是void[1]。
对于那些容器,你必须使用“后置递增你要传给erase的迭代器”技术。
我们必须对vector、string和deque采用不同的战略。
特别是,我们必须利用erase的返回值。
那个返回值正是我们需要的:
一旦删除完成,它就是指向紧接在被删元素之后的元素的有效迭代器。
如果我们观察在本条款中提到的所有东西,我们得出下列结论:
∙去除一个容器中有特定值的所有对象:
如果容器是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时记得后置递增它。
如你所见,与仅仅调用erase相比,有效地删除容器元素有更多的东西。
解决问题的最好方法取决于你是怎样鉴别出哪个对象是要被去掉的,储存它们的容器的类型,和当你删除它们的时候你还想要做什么(如果有的话)。
只要你小心而且注意了本条款的建议,你将毫不费力。
如果你不小心,你将冒着产生不必要低效的代码或未定义行为的危险。
因此,如果你想要写自定义分配器,让我们总结你需要记得的事情。
∙把你的分配器做成一个模板,带有模板参数T,代表你要分配内存的对象类型。
∙提供pointer和reference的typedef,但是总是让pointer是T*,reference是T&。
∙决不要给你的分配器每对象状态。
通常,分配器不能有非静态的数据成员。
∙记得应该传给分配器的allocate成员函数需要分配的对象个数而不是字节数。
也应该记得这些函数返回T*指针(通过pointertypedef),即使还没有T对象被构造。
∙一定要提供标准容器依赖的内嵌rebind模板。
vector和string是羽翼丰满的序列容器,所以它们让你支配可以作用于这样的容器的整个STL算法军火库。
虽然数组也可以用于STL算法,但没有提供像begin、end和size这样的成员函数,也没有内嵌像iterator、reverse_iterator或value_type那样的typedef。
而且char*指针当然不能和提供了专用成员函数的string竞争。
STL用的越多,越会歧视内建的数组。
所以,避免重新分配的关键是使用reserve尽快把容器的容量设置为足够大,最好在容器被构造之后立刻进行。
vector
vector的拷贝构造函数做了这个工作。
但是,vector的拷贝构造函数只分配拷贝的元素需要的内存,所以这个临时vector没有多余的容量。
然后我们让临时vector和contestants交换数据,这时我们完成了,contestants只有临时变量的修整过的容量,而这个临时变量则持有了曾经在contestants中的发胀的容量。
交换技巧的变体可以用于清除容器和减少它的容量到你的实现提供的最小值。
String.swap(s);清除s,而且将它的容量最小话。
C++老手立即发现这有问题,因为在C++里没有办法捏造引用。
这样做要求有能力重载operator.(“点操作符”),而那是不允许的。
另外,建立行为像引用的对象是使用代理对象的例子,而代理对象会导致很多问题。
(一个这样的问题产生了条款18。
对代理对象的综合讨论,转向《MoreEffectiveC++》的条款30,你能知道什么时候它们工作什么时候不。
)
确保一个给定类型的所有分配器都等价是你的责任。
如果你违反这个限制,不要期待编译器发出警告。
大多数标准容器从未调用它们例示的分配器,这些容器包括list和所以的标准关联容器,那是因为这些都是基于节点的容器,每个节点还要包括除了对象T之后的其他信息,如串接节点需要的指针。
我们要的不是T的内存,我们要的是包含了一个T的ListNode的内存。
所以,listnode对应的分配器为,allocator:
:
rebind
:
other结果,list
:
rebind
:
other从它用于T对象的分配器(叫做Allocator)获取对应的ListNode对象分配器。
哈利路亚!
我们最后完成了对分配器特质的检查。
因此,如果你想要写自定义分配器,让我们总结你需要记得的事情。
∙把你的分配器做成一个模板,带有模板参数T,代表你要分配内存的对象类型。
∙提供pointer和reference的typedef,但是总是让pointer是T*,reference是T&。
∙决不要给你的分配器每对象状态。
通常,分配器不能有非静态的数据成员。
∙记得应该传给分配器的allocate成员函数需要分配的对象个数而不是字节数。
也应该记得这些函数返回T*指针(通过pointertypedef),即使还没有T对象被构造。
∙一定要提供标准容器依赖的内嵌rebind模板。
写你自己的分配器时你必须做的大部分事情是重现大量样板代码,然后修补一些成员函数,特别是allocate和deallocate。
我建议你从Josuttis的样例allocator网页[23]或Austern的文章《WhatAreAllocatorsGoodFor?
》[24]的代码开始,而不是从头开始写样板。
向C风格的函数传递参数。
对于vector:
if(!
v.empty()){
doSomething(&v[0],v.size());
//这个函数可以修改v的某些元素,但是绝对不能修改v的daxiao,这样会使//其内部状态不一致,从而size()无法返回正确的大小。
}
对于string:
doSomething(s.c_str());//即使字符串的长度是0它都可以工作
应该明白上面这段代码是什么意思。
有序vector经常可以作为关联容器的替代品,但对这些vector而言,保持顺序非常重要。
如果你将一个有序vector传给一个可能修改其数据的API函数,你需要重视vector在调用返回后不再保持顺序的情况。
实际上让C风格的函数把数据放进vector,然后再把数据转储到其他容器的方法总是有效的。
Swap技巧休整容器的容量,可以用于vector和string
Vector
…//putsomedataintocompent
Vector
操作过程:
Vector
然后和原来的compent交换,完成空间的紧缩。
同样的技巧可以应用于string:
strings;
...//使s变大,然后删除所有
//它的字符
string(s).swap(s);//在s上进行“收缩到合适”
需要避免使用vector
为了保存bool值有两个替代方案。
一是deque
Deque的内部内存是不连续的,所以不能吧deque
二是biset类bitset不是一个STL容器,但它是C++标准库的一部分。
与STL容器不同,它的大小(元素数量)在编译期固定,因此它不支持插入和删除元素。
此外,因为它不是一个STL容器,它也不支持iterator。
但就像vector
它提供vector
如果不在乎没有迭代器和动态改变大小,你也许会发现bitset正合你意。
find算法和set的insert成员函数是很多必须判断两个值是否相同的函数的代表。
但它们以不同的方式完成,find对“相同”的定义是相等,基于operator==。
set:
:
insert对“相同”的定义是等价,通常基于operator<。
在一般情况下,用于关联容器的比较函数不是operator<或甚至less,它是用户定义的判断式。
(关于判断式的更多信息参见条款39。
)每个标准关联容器通过它的key_comp成员函数来访问排序判断式,所以如果下式求值为真,两个对象x和y关于一个关联容器c的排序标准有等价的值。
下面的这段看起来很别扭:
标准关联容器保持有序,所以每个容器必须有一个定义了怎么保持东西有序的比较函数(默认是less)。
等价是根据这个比较函数定义的,所以标准关联容器的用户只需要为他们要使用的任意容器指定一个比较函数(决定排序顺序的那个)。
如果关联容器使用相等来决定两个对象是否有相同的值,那么每个关联容器就需要,除了它用于排序的比较函数,还需要一个用于判断两个值是否相等的比较函数。
(默认的,这个比较函数大概应该是equal_to,但有趣的是equal_to从没有在STL中用做默认比较函数。
当在STL中需要相等时,习惯是简单地直接调用operator==。
比如,这是非成员find算法所作的。
)
如果你想要string*指针以字符串值确定顺序被储存在set中,你不能使用默认比较仿函数类less
你必须改为写你自己的比较仿函数类,它的对象带有string*指针并按照指向的字符串值来进行排序。
就像这样:
structStringPtrLess:
publicbinary_function conststring*,//的理由参见条款40 bool>{ booloperator()(conststring*ps1,conststring*ps2)const { return*ps1<*ps2; } };//这个仿函数类只对string有效 然后你可以使用StringPtrLess作为ssp的比较类型: typedefset StringPtrSetssp;//建立字符串的集合, 下面这个是比较的仿函数模板: 鉴于这种情况,你手头最好也能有一个用于那种比较的仿函数模板。 像这样: structDereferenceLess{ template booloperator()(PtrTypepT1,//参数是值传递的, PtrTypepT2)const//因为我们希望它们 {//是(或行为像)指针 return*pT1<*pT2; } };//这模板对于所以指针关联容器都是可用的。 这样的模板消除了写像StringPtrLess那样的类的需要,因为我们可以改为使用DereferenceLess: set 此外,如果需要一个只能指针或者迭代器的容器,也需要为它指定比较类型。 关联容器对相同的定义是等价,默认使用less 除非你的比较函数总是为相等的值返回false,你将打破所有的标准关联型容器,不管他们是否允许有副本。 标准关联容器的典型实现是平衡二叉查找树。 一个平衡二叉查找树是一个对插入、删除和查找的混合操作优化的数据结构。 但是对于大多数的应用来说,使用数据结构并没有那么混乱,它们对数据结构的使用可以总结为这样的三个截然不同的阶段: 建立-查找-重组。 对于这样使用数据结构的应用来说,有序的vector无论在空间还是时间上都能比关联容器提供更好的性能。 概要: 在有序vector中存储数据很有可能比在标准关联容器中保存相同的数据消耗更少的内存;当页面错误值得重视的时候,在有序vector中通过二分法查找可能比在一个标准关联容器中查找更快。 使用vetor的最大的缺点是必须保持有序,当插入一个新的元素是,大于这个新元素的所有东西都必须向上移动一位。 它和听起来一样昂贵,如果vector恰好需要重新分配内存,则会更昂贵,因为所有的vector都必须拷贝。 同样的,如果一个元素从vector中被删除,所有大于它的元素都要下移。 总之,vector的插入和删除都很昂贵。 但是,在关联容器中插入和删除却很轻量。 所以,只有在使用数据结构时几乎不和插入和删除混合使用时使用有序的vector代替关联容器才有意义。 当使用vector来模拟map 因为对vetor中元素排序时,需要通过赋值进行移动,那就意味着pair的两个组件都必须是可以赋值的。 Map和multimap以顺序的方式保存他们的元素,但用于排序目的时它们只作用于元素的key部分(pair的第一个组件),所以当使用有序的vector代替map时,需要为pair写一个自定义的比较函数,因为pair的<作用于pair的两个组件。 不是所有算法可以用于任意区间。 比如,remove(参见条款32和33)需要前向迭代器和可以通过这些迭代器赋值的能力。 所以,它不能应用于由输入迭代器划分的区间,也不能是map或multimap,也不能是set和multiset的一些实现(参见条款22)。 同样,很多排序算法(参见条款31)需要随机访问迭代器,所以不可能在一个list的元素上调用这些算法。 Map: : operator[]被设计为添加或者更新功能。 与vector/deque/string/普通数组都无关。 它的操作过程是: 如果发现map中没有参数指定的键,则使用默认构造函数构造一个对象与key关联成一个pair,插入到map中,然后把右值赋给pair.second.这样必然会造成性能上的下降。 如果需要插入,我们可以直截了当的进行: Typedefmap m.insert(IntDoubleMap: : value_type(1,1.26)); 每个标准容器都提供了value_type的typedef。 需要记住的是: 对于map和multimap(以及非标准容器的hash_map和hash_multimap),容器元素的类型总是某种pair。 下面的函数高效且优美的实现了插入和更新。 出于对效率的考虑,当给map添加一个元素时,我们断定insert比operator[]好; 出于对效率和美学的考虑,当更新已经在map里面的元素值时operator[]更好。 首先可供查找的算法大致有count,find,binary_search,lower_bound,upper_bound,equal_range。 带有判别式的如count_if,find_if或者binary_search的派别式版本,其用法大致相同,不影响选择,所以不作考虑。 注意这些查找算法需要序列式容器,或者数组。 关联容器有相应的同名成员函数exceptbinary_search。 首先,选择查找算法时,区间是否排序是一个至关重要的因素。 可以按是否需要排序区间分为两组: A.count,find B.binary_search,lower_bound,upper_bound,equal_range A组不需排序区间,B组需要排序区间。 当一个区间被排序,优先选择B组,因为他们提供对数时间的效率。 而A则是线性时间。 另外A组B组所依赖的查找判断法则不同,A使用相等性法则(查找对象需要定义operator==),B使用等价性法则(查找对象需要定义operator<,必须在相等时返回false)。 A组的区别 count: 计算对象区间中的数目。 find: 返回第一个对象的位置。 查找成功的话,find会立即返回,count不会立即返回(直到查找完整个区间),此时find效率较高。 因此除非是要计算对象的数目,否则不考虑count。 B组的区别{1,3,4,5,6} binary_search: 判断是否存在某个对象 lower_bound: 返回>=对象的第一个位置,lower_bound (2)=3,lower_bound(3)=3 目标对象存在即为目标对象的位置,不存在则为后一个位置. upper_bound: 返回>对象的第一个位置,upper_bound (2)=3,upper_bound(3)=4 无论是否存在都为后一个位置. equal_bound: 返回由lower_bound和upper_bound返回值构成的pair,也就是所有等价元素区间。 equal_bound有两个需要注意的地方: 1.如果返回的两个迭代器相同,说明查找区间为空,没有这样的值 2.返回迭代器间的距离与迭代器中对象数目是相等的,对于排序区间,他完成了count和find的双重任务 详细的建议可参考EffectiveSTL-169 下面针对排序区间给出一些例子 1.查找元素及数
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Effective STL学习笔记 STL 学习 笔记