【C++】Effective STL:50条有效使用STL的经验

第一条:慎重选择容器类型
1、C++容器:先混个眼熟

序列容器:array、vector、string、deque、list、forward_list
有序关联容器:set、map、multiset、multimap
无序关联容器:unordered_set、unordered_map、unordered_multiset、unordered_multimap
容器适配器:stack、queue、priority_queue

2、C++容器:简介
2.1 序列容器

序列容器实现了可以顺序访问的数据结构。

array:静态连续容器;
vector:动态连续容器,超出容器大小会自动分配内存;
deque:双端队列;
list:双链表;
forward_list:单链表;

2.2 有序关联容器

关联容器实现可以快速搜索的排序数据结构(O(log n)复杂度)。

set:只有键,没有值的集合,按键排序,并且键是唯一的;
map:键值对的集合,按键排序,并且键是唯一的;
multiset:只有键,没有值的集合,按键排序,键可以不唯一;
multimap:键值对的集合,按键排序,键可以不唯一;

2.3 无序关联容器

无序关联容器实现了可以快速搜索的未排序(哈希)数据结构(O(1)最好,O(n)最坏情况的复杂性)。

unordered_set:只有键,没有值的集合,按键哈希散列,并且键是唯一的;
unordered_map:键值对的集合,按键哈希散列,并且键是唯一的;
unordered_multiset:只有键,没有值的集合,按键哈希散列,键可以不唯一;
unordered_multimap:键值对的集合,按键哈希散列,键可以不唯一;

2.4容器适配器

容器适配器为顺序容器提供了不同的接口。
stack:先进后出;
queue:先进先出;
priority_queue:优先级队列

3、选择容器

默认序列容器:vector,先考虑vector是否符合要求,如果不符合再选择其它的;
容器大小从始至终不会变,选择array:例如记录一年中每个月的收入;
需要在序列中间做插入和删除操作,选择list;
需要在序列头部、尾部做插入和删除操作时,选择deque;

第二条:不要试图编写独立于容器类型的代码
1、禁止对容器进一步泛化

每一种容器是针对一类场景的泛化,不要试图进一步泛化容器。比如:针对当下场景使用vector容器,写代码时就围绕vector接口来写,不要试图写出能够兼容list、deque的代码。虽然这么做出发点是好的,但是最终总是误入歧途。

2、建议封装容器到类中

要想减少再替换容器类型时所需要修改的代码,可以把容器隐藏到一个类中,并尽量减少与容器相关的外部可见的接口。

第三条:确保容器中的对象拷贝正确而高效
1、使用智能指针

使拷贝动作高效、正确,并防止剥离问题发生的一个简单办法是使容器包含指针而不是对象,最佳选择是使用智能指针。

第四条:调用empty而不是检查size()是否为0

理由很的简单:empty对所有的标准容器都是常数时间操作,而对一些list实现,size耗时线性增长。

第五条:区间成员函数优于与之对应的单元素成员函数
1、区间成员函数

区间成员函数像STL算法一样,使用两个迭代器参数来确定该成员操作所执行的区间。

2、区间成员函数的优点

代码量少、表达清晰直接,易写易懂。

3、何时使用区间成员函数

1)通过利用插入迭代器的方式来限定目标区间的copy调用,几乎都应该被替换为对区间成员函数的调用;
2)所有标准容器都提供了使用区间创建的构造函数;
3)所有标准容器都提供了使用区间插入的insert函数;
4)所有标准容器都提供了使用区间删除的erase函数;
5)所有标准容器都提供了使用区间赋值的assign函数

第六条:当心C++编译器最烦人的分析机制

请使用命名的迭代器对象,这消除二义性,维护代码的人更容易理解。
这里举例一个容易犯错的代码:将变量定义写成了函数声明。

第七条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉

STL容器很智能,但没有智能到知道是否该删除自己所包含的指针所指向的空间。为了避免内存泄漏,建议使用引用计数形式的智能指针对象。

第八条:切勿创建包含auto_ptr的容器对象

不多说了,很多年以前就不再使用auto_ptr了。

第九条:慎重选择删除元素的方法
1、删除特定值

对于连续容器,使用erase-remove:v.erase(remove(v.begin(), v.end(), 1987), c.end());
对于非连续容器list,使用remove函数;
对于关联容器,使用erase;

2、删除满足特定条件的对象

对于连续容器,使用erase-remove_if;
对于非连续容器,使用list::remove_if
对于关联容器,使用remove_copy_if和swap或者遍历使用erase,注意参数使用后缀递增;

第十条:了解分配子(allocator)的约定和限制

一个类型为T的对象,它的默认分配子allocator的两个类型定义分别是allocator::pointer和allocator::reference。
其他待完善,没有理解分配子的作用。

第十一条:理解自定义分配子的合理用法
第十二条:切勿对STL容器的线程安全性有不切实际的依赖

对容器成员函数的每次调用,都锁住容器直到调用结束;
在容器返回的每个迭代器的生存期结束前,都锁住容器;

第十三条:vector和string优先于动态分配的数组

不解释了,直接照做就是了。

第十四条:使用reserver来避免不必要的重新分配

reserver成员函数能使你把重新分配的次数减少到最低限度。
size:容器中有多少元素;注意:不等于容器容纳多少元素;
capacity:容器已经分配的内存可以容纳多少元素;
resize:强迫容器改变到包含n个元素的状态。
reserve:强迫容器把它的容器变为至少是n,如果n不小于当前的大小,会重新分配;如果小于,什么也不做。

第十五条:注意string实现的多样性

不同的string实现以不同的方式来组织下列信息:字符串的大小、容量capacity、字符串的值、对值的引用计数。

第十六条:了解如果把vector和string数据传给旧的API

vector返回C API:

vector<int> v;
if(!v.empty()){
	doSomething(&v[0], v.size());
}

注意:不要用v.begin()代替&v[0]

string返回C API:s.c_str();

第十七条:使用“swap技巧”除去多余的容量
class Contestant{... ...};
vector<Contestant> contestants;
... ...
vector<Contestant>(contestants).swap(contestants);
string s;
... ...
string(s).swap(s);
第十八条:避免使用vector

首先,它不是一个STL容器;
其次,它并不存储bool

代替方法:

deque<bool>
或者使用bitset
第十九条:理解相等(equality)和等价(equivalence)的区别

相等:operator==
等价:!(x<y) && !(y<x),对于两个对象x和y,如果按照关联容器c的排列顺序,每个都不在另一个的前面,那么称这两个对象按照c的排列顺序是等价的。

第二十条:为包含指针的关联容器指定比较类型

如果什么也不做,默认是对指针的地址做排序。必须自己编写比较函数子类。
比较函数模板如下:

struct DereferenceLess {
	tumplate<typename PtrType>
	bool operator()(PtrType pT1, PtyType pT2) const
	{
		return *pT1 < *pT2;
	}
}
第二十一条:总是让比较函数在等值情况下返回false

如果比较函数在等值情况下返回true,两个相等的值,可能不等价。
比如使用less_equal(operator<=)作为比较函数,x=y=10时,!(x<=y) && !(y<=x)的结果为false,即最终会得出:x!=y

第二十二条:切勿直接修改set或multiset的值

所有的标准关联容器是按照一定顺序来存放的,如果修改了会打破容器的有序性。

第二十三条:考虑用排序的vector替代关联容器

如果元素少并且几乎不会有插入删除操作,可以考虑使用排序的vector替代关联容器,无论是空间还是时间都是最优的。

第二十四条:当效率至关重要时,请在map::operator[]和map::insert之间谨慎做出选择

map::operator[]的设计目的是为了提供“添加和更新”的功能;
在作为“添加”操作时,insert比operator[]效率高;
当作为“更新”操作时,优先使用operator[]

第二十五条:熟悉非标准的散列表

非标准的散列表:hast_set、hast_multiset、hash_map、hash_multimap
注意这里说的是旧版本的STL

第二十六条:iterator优先于const_iterator、reverse_iterator及const_reverse_iterator。

原因:
1、容器中的instert和crase函数的形参只接受iterator类型,不接受const_iterator、reverse_iterator及const_reverse_iterator。
2、从iterator到const_iterator,或者从reverse_iterator到const_reverse_iterator之间都存在隐式转换,但是反过来技术上可以实现,但不推荐,效率不能保证。
3、在同一个表达式中混用iterator和const_iterator,比如比较操作,会发生隐式转换,有时会出现问题。

第二十七条:使用distance和advance将容器的const_iterator转换成iterator

distance:用于取得两个迭代器之间的距离;
advance:用于将一个迭代器移动指定的距离。

typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;

IntDeque d;
ConstIter ci;
...
Iter i(d.begin()); /迭代器 i 指向容器 d 起始位置
advance(i, distance<ConstIter>(i, ci)); /注意这里指明distance所使用的类型为ConstIter

效率问题:对于随机访问的迭代器(如vector、string、deque产生的迭代器)而言,执行时间是常数时间;对于其他标准容器以及散列表的迭代器而言,执行时间是一个线性时间,因此推荐二十六条,尽量用iterator代替const_iterator

第二十八条:正确理解由reverse_iterator的base()成员函数所产生的iterator的用法

1、插入操作:如果要在一个迭代器reverse_iterator ri指定的位置上插入一个新元素,则只需在ri.base()位置处插入元素即可。
2、删除操作:如果要在一个迭代器reverse_iterator ri指定的位置上删除一个元素,要先递增ri,然后在调用base()函数,例如:v.erase((++ri).base());

第二十九条:对于逐个字符的输入请考虑使用istreambuf_iterator

对比istream_iterator,它在默认请看下会跳过空白字符,并且效率低。
读取一个文本文件的内容到一个string对象中,推荐的方案如下:

ifstream inputFile("test.txt");
string fileData((istreambuf_iterator<char>(inputFile)), 
				 istreambuf_iterator<char>());
第三十条:确保目标区间足够大

对于vector、string、deque、list容器在尾部插入对象时,需要使用back_inserter函数;
其中deque和list容器还可以使用front_inserter在头部插入对象。
为了提供插入操作的性能,对于vector和string容器可以使用reserve来提前分配好内存。

第三十一条:了解各种与排序有关的选择

按照性能由高到底排序:
1、partition:把满足特定条件的元素放到前面;
2、stable_partition:把满足特定条件的元素放到前面,并且是稳定的排序(在遇到相等的对象时,还会按照出场顺序排序);
3、nth_element:找出部分最优的对象,可以不按照顺序,比如列出前十个,而且这十个可以不用排序;
4、partial_sort:部分排序;
5、sort:全部排序
6、stable_sort:稳定版本的全部排序,不仅全部排好序,而且在遇到相等的对象时,还会按照出场顺序排序。
注意:list::sort是稳定排序。

第三十二条:如果确实需要删除元素,则需要在remove这一类算法之后调用erase

注意:remove不能删除容器中元素。
因为从容器中删除元素的唯一方法是调用该容器的成员函数,而remove并不知道它操作的元素所在的容器,所以remove不可能从容器中输出删除元素。
remove实际功能:把不用被删除的元素放到容器前部,把需要被删除的元素放到容器尾部。
如果需要真正删除元素,需要使用erase和remove配合

vector<int> v;
...
v.erase(remove(v.begin(), v.end(), 99), v.end());

同理unique也需要和erase配合使用。

注意:list::remove会真正删除元素,并且比使用erase-remove配合使用更高效,list::unique也会真正删除元素。

第三十三条:对包含指针的容器使用remove这一类算法时要特别小心

对指针容器使用erase-remove组合来删除时,很可能造成内存泄漏。
其实在执行remove后,还没有执行erase之前已经发生内存泄漏。因为remove在将不需要删除的指针元素移动到容器头部时,会覆盖掉需要删除的指针元素,造成内存泄漏;
再执行erase时,也会造成内存泄漏,因为还没释放内存,就删除了指针。
同理:remove_if和unique也会造成内存泄漏。

解决方法有两种:
1、使用带有引用计数的智能指针
2、使用partition代替remove

第三十四条:了解哪些算法要求使用排序的区间作为参数

需要排序后才能使用的算法:

// 使用二分法查找的算法,需要先排序
binary_search
lower_bound
upper_bound
equal_range

// 集合操作,为了提高效率(为了保证线性时间效率),需要先排序
set_union
set_intersection
set_defference
set_symmetric_defference

// 合并操作,为了提高效率
merge
inplace_merge

// 判断一个区间中的所有的对象是否都在另一个区间中,为了提高效率
includes
第三十五条:通过mismatchlexicographical_compace实现简单的忽略大小写的字符串比较

第三十六条:理解copy_if算法的正确实现

STL中包含copy的算法

copy
copy_backward
replace_copy
replace_copy_if
reverse_copy
unique_copy
remove_copy
remove_copy_if
rotate_copy
partial_sort_copy
uninitialized_copy

但是就是没有copy_if算法,需要自己实现

template<typename InputIterator,
		typename OutputIterator
		typename Predicate>
OutputIterator copy_if(InputIterator begin,
					   InputIterator end,
					   OutputIterator destBegin,
					   Predicate p)
{
	while (begin != end) {
		if (p(*begin))
			*destBegin++ = *begin;
		++begin;
	}
	return destBegin;
}
第三十七条:使用accumulate或者for_each进行区间统计

count:统计一个区间中有多少个元素;
count_if:统计满足某个条件的元素个数;
min_element:获取区间中最小值;
max_element:获取区间中最大值;

accumulate:对区间进行自定义的操作,比如计算一个容器中字符串长度的总和。

/计算和
list<double> ld;
...
double sum = accumulate(ld.begin(), ld.end(), 0.0);

/ 计算容器中字符串长度的总和
string::size_type
stringLengthSum(string::size_type sumSoFar ,
				const string& s)
{
	return sumSoFar + s.size();
}

set<string> ss;
...//插入一些字符串
string::size_type lengthSum = 
	accumulate(ss.begin(), ss.end(), 
		static_cast<string::size_type>(0),
		stringLengthSum);
};
第三十八:遵循按值传递的原则来设计函数子类

1、在C和C++的标准库函数中,如果需要函数作为参数,需要使用函数指针,并且函数指针是按值传递的(被复制);
2、在STL中,函数对象在函数之间来回传递也是按值传递(被复制);
3、因为函数对象是按值传递,即需要来回复制,因此函数对象要尽可能的小;
4、函数对象必须是单态的(不能是多态),也就是说,它们不能有虚函数,因此在传递过程中,会出现剥离问题(slicing problem):在对象复制过程中,派生部分可能会被去掉,而仅保留了基类部分。

第三十九:确保判别式是“纯函数”。

1、判别式函数(predicate function):是一个返回值为bool类型(或者可以隐式地转换为bool类型)的函数。
2、判别式类(predicate class):是一个函数类,它的operator()函数是一个判别式。
3、纯函数(pure function):是指返回值仅仅依赖于其参数的函数。
4、在STL中容器使用的比较函数都是判别式,比如:find_if以及各种与排序相关的算法,这些算法要求每执行一次都要保证结果是唯一,不会受其他参数的干扰,比如全局变量、静态变量等。

第四十条:若一个类是函数类,则应使它可配接

1、STL函数配接器not1、not2、bind1st、bind2nd等都要求一些特殊的类型定义,提供这些必要的类型定义的函数对象被称为可配接的(adaptable)函数对象。
2、特殊的类型定义:argument_type、first_argument_type、second_argument_type、result_type
3、继承 std::unary_function、std::binary_function来完成上述的特殊的类型定义

第四十一条:理解ptr_fun、mem_fun和mem_fun_ref的来由

STL算法只支持非成员函数,不支持成员函数(无法通过编译),如果要使用成员函数需要使用ptr_fun、mem_fun或者mem_fun_ref来将成员函数封装到一个函数类中

第四十二条:确保less与operator<具有相同的语义

1、不要特化一个位于std名字空间中的模板
2、operator<是std::less默认实现方式,不要修改std::less的行为,因为这样做很可能会误导其它的程序员。

第四十三条:算法调用优先于手写的循环

1、先看一个手写循环和使用算法的例子
一个支持重画的类Widget

class Widget{
public:
	...
	void redraw() const;
	...
}

使用手写循环来重画容器list中的所有的Widget:

list<Widget> lw;
...
for (list<Widget>::iterator i=iw.begin(); i!=iw.end(); ++i){
	i->redraw();
}

使用for_each循环算法来完成

for_each(lw.begin(), lw.end()),
	mem_fun_ref(&Widget::redray)); 

mem_fun_ref:封装成员函数,使它可以用在算法中,因为算法只能使用非成员函数

2、使用算法的优点
效率高、可维护性好。
理由:略,一定要用起来

第四十四条:容器的成员函数优先于同名的算法

1、列举出这些函数
关联容器:count、find、lower_bound、upper_bound、equal_range
list容器:remove、remove_if、unique、sort、merge、reverse

2、理由
速度更快、成员函数通常与容器结合的更加紧密

第四十五条:正确区分count、find、binary_search、lower_bound、upper_bound和equal_range

1、如果容器的区间是排序好的使用:binary_search、lower_bound、upper_bound和equal_range,
这些算法是对数时间的效率。
2、如果容器的区间不是排序好的使用:count、count_if、find、find_if,
这些算法是线性时间的效率。

count:区间中是否存在某个特定的值?如果存在的话,有多少个拷贝?
find:区间中有这样的值吗?如果有,它在哪里?
两者还有一个区别:find找到后就返回,效率高;count要遍历一遍容器的区间

binary_search:测试一个排序的容器区间中是否存在某一个特定的值,返回true或者false;
equal_range:查找一个值在容器区间中的位置;
lower_bound:查找一个值在容器区间中第一次出现的位置,如果没有,则返回适合插入该值的位置;

第四十六条:考虑使用函数对象而不是函数作为STL算法的参数

使用函数对象比直接使用函数作为STL算法的参数,更高效。
原因是:编译器可以对函数对象做内敛优化,而函数作为参数,其实是指针,编译器不会对指针做优化。

第四十七条:避免产生“直写型”(wirte-only)的代码

1、不要写过于复杂的嵌套函数调用
2、注意添加代码的注释

第四十八条:总是包含(#include)正确的头文件

1、有些标准头文件可以省略,也可以通过编译,但是考虑移植性,强烈建议不要省略;
2、标准的STL容器都被声明在与之同名的头文件中:vector、list等,特殊的:multiset和set都在<set>中,map和multimap都在<map>中
3、算法被声明在<algorithm>中,除了下面四个
4、accumulate、inner_product、adjacent_difference、partial_sum被声明在<numeric>中
5、特殊类型的迭代器被声明在<iterator>中
6、标准的函数类和函数配接器被声明在<functional>中:如not1、bind2nd

第四十九条:学会分析与STL相关的编译器诊断信息

1、std::basic_string<… …>很长的一个信息,翻译成string就好;
2、vector和string的迭代器通常就是指针,所以错误的使用iterator错误信息中会有:double *
3、如果错误消息源于某一个STL算法的内部实现,那么也许在调用算法的时候,使用了错误的类型。

第五十条:熟悉与STL相关的web站点

SGI站点:www.sgi.com/tech/stl/
STLport站点:www.stlport.org
Boost:www.boost.org

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Content Containers...................................................................................................................1 Item 1. Choose your containers with care...........................................................1 Item 2. Beware the illusion of container-independent code................................4 Item 3. Make copying cheap and correct for objects in containers.....................9 Item 4. Call empty instead of checking size() against zero..............................11 Item 5. Prefer range member functions to their single-element counterparts...12 Item 6. Be alert for C++'s most vexing parse...................................................20 Item 7. When using containers of newed pointers, remember to delete the pointers before the container is destroyed............................................................22 Item 8. Never create containers of auto_ptrs....................................................27 Item 9. Choose carefully among erasing options..............................................29 Item 10. Be aware of allocator conventions and restrictions..........................34 Item 11. Understand the legitimate uses of custom allocators........................40 Item 12. Have realistic expectations about the thread safety of STL containers. 43 容器 款1: 仔细选择你要的容器 款2: 小心对“容器无关代码”的幻想 款3: 使容器里对象的拷贝操作轻量而正确 款4: 用empty来代替检查size是否为0 款5: 尽量使用范围成员函数代替他们的单元素兄弟 款6: 警惕C++的及其令人恼怒的分析 款7: 当使用new得指针的容器时,切记在容器销毁前delete那些指针 款8: 千万不要把auto_ptr放入容器中 款9: 小心选择删除选项 款10: 当心allocator的协定和约束 款11: 了解自定义allocator的正统使用款12: 对STL容器的线程安全性的期待现实一些
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郭老二

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值