第1条:慎重选择容器类型。
标准STL序列容器:vector、string、deque和list。
标准STL关联容器:set、multiset、map和multimap。
非标准序列容器slist和rope。slist是一个单向链表,rope本质上是一“重型”string。
非标准的关联容器hash_set、hase_multiset、hash_map和hash_multimap。
vector
vector作为标准关联容器的替代。(见第23条)
几种标准的非STL容器,包括数组、bitset、valarray、stack、queue和priority_queue。
你是否关心容器中的元素是如何排序的?如果不关心,选择哈希容器.
容器中数据的布局是否需要和C兼容?如果需要兼容,就只能选择vector。(见第16条)
元素的查找速度是否是关键的考虑因素?如果是,就要考虑哈希容器、排序的vector和标准关联容器-或许这就是优先顺序。
对插入和删除操作,你需要事务语义吗?如果是,只能选择list。因为在标准容器中,只有list对多个元素的插入操作提供了事务语义。
deque是唯一的、迭代器可能会变为无效(插入操作仅在容器末尾发生时,deque的迭代器可能会变为无效)而指向数据的指针和引用依然有效的标准STL容器。
第2条:不要试图编写独立于容器类型的代码。
如果你想编写对大多数的容器都适用的代码,你只能使用它们的功能的交集。不同的容器是不同的,它们有非常明显的优缺点。它们并不是被设计用来交换使用的。
你无法编写独立于容器的代码,但是,它们(指客户代码)可能可以。
第3条:确保容器中的对象拷贝正确而高效。
copy
第4条:调用empty而不是检查size()是否为0。
理由很简单:empty对所有的标准容器都是常数时间操作,而对一些list的实现,size耗费线性时间。
第5条:区间成员函数优先于与之对应的单元素成员函数。
区间成员函数写起来更容易,更能清楚地表达你的意图,而且它们表现出了更高的效率。
第6条:当心C++编译器最烦人的分析机制。
把形参加括号是合法的,把整个形参的声明(包括数据类型和形参名字)用括号括起来是非法的。
第7条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉。
STL很智能,但没有智能到知道是否该删除自己所包含的指针所指向的对象的程度。为了避免资源泄漏,你必须在容器被析构前手工删除其中的每个指针,或使用引用计数形式的智能指针(比如Boost的sharedprt)代替指针。
第8条:切勿创建包含auto_ptr的容器对象。
拷贝一个auto_ptr意味着改变它的值。例如对一个包含auto_ptr的vector调用sort排序,结果是vector的几个元素被置为NULL而相应的元素被删除了。
第9条:慎重选择删除元素的方法。
要删除容器中指定值的所有对象:
如果容器是vector、string或deque,则使用erase-remove习惯用法。
SeqContainer
c.erase(remove(c.begin(),c.end(),1963),c.end());
如果容器是list,则使用list::remove。
如果容器是一个标准关联容器,则使用它的erase成员函数。
要删除容器中满足特定条件的所有对象:
如果容器是vector、string或deque,则使用erase-remove_if习惯用法。
如果容器是list,则使用list::remove_if。
如果容器是一个标准关联容器,则使用remove_copy_if和swap,或者写一个循环遍历容器的元素,记住当把迭代器传给erase时,要对它进行后缀递增。
AssocCOntainer
...
AssocContainer
remove_copy_if(c.begin(),
c.swap(goodValues);
或
for(AssocContainer::iterator
if(badValue(*i))
else
}
要在循环内部做某些(除了删除对象之外的)操作:
如果容器是一个标准序列容器,则写一个循环来遍历容器中的元素,记住每次掉用erase时,要用它的返回值更新迭代器。
如果容器是一个标准关联容器,则写一个循环来遍历容器中的元素,记住每次把迭代器传给erase时,要对迭代器做后缀递增。
第10条:了解分配子(allocator)的约定和限制。
第11条:理解自定义分配子的合理用法。
第12条:切勿对STL容器的线程安全性有不切实际的依赖。
对一个STL实现你最多只能期望:
多个线程读是安全的。
多个线程对不同的容器写入操作是安全的。
你不能期望STL库会把你从手工同步控制中解脱出来,而且你不能依赖于任何线程支持。
第13条:vector和string优先于动态分配的数组。
如果用new,意味着你要确保后面进行了delete。
如果你所使用的string是以引用计数来实现的,而你又运行在多线程环境中,并认为string的引用计数实现会影响效率,那么你至少有三种可行的选择,而且,没有一种选择是舍弃STL。首先,检查你的库实现,看看是否可以禁用引用计数,通常是通过改变某个预处理变量的值。其次,寻找或开发一个不使用引用计数的string实现。第三,考虑使用vector而不是string。vector的实现不允许使用引用计数,所以不会发生隐藏的多线程性能问题。
第14条:使用reserve来避免不必要的重新分配。
通常有两种方式来使用reserve以避免不必要的重新分配。第一种方式是,若能确切知道或大致预计容器中最终会有多少个元素,则此时可使用reserve。第二种方式是,先预留足够大的空间,然后,当把所有的数据都加入后,再去除多余的容量。
第15条:注意string实现的多样性。
如果你想有效的使用STL,那么你需要知道string实现的多样性,尤其是当你编写的代码必须要在不同的STL平台上运行而你又面临着严格的性能要求的时候。
第16条:了解如何把vector和string数据传给旧的API。
如果你有个vector
如果想用来自C
第17条:使用“swap技巧”出去多余的容量。
vector(contestants).swap(contestants);
表达式vector(contestants)创建一个临时的矢量,它是contestants的拷贝:这是由
同样的技巧对string也实用:
string
...
string(s).swap(s);
第18条:避免使用vector。
作为STL容器,vector只有两点不对。首先,它不是一个STL容器;其次,它并不存储bool。除此以外,一切正常。因此最好不要使用它,你可以用deque和bitset替代。vector来自一个雄心勃勃的试验,代理对象在C++软件开发中经常会很有用。C++标准委员会的人很清楚这一点,所以他们决定开发vector,以演示STL如果支持
第19条:理解相等(equality)和等价(equivalence)的区别。
标准关联容器总是保持排列顺序的,所以每个容器必须有一个比较函数(默认为less)。等价的定义正是通过该比较函数而确定的。相等一定等价,等价不一定相等。
第20条:为包含指针的关联容器指定比较类型。
每当你创建包含指针的关联容器时,容器将会按照指针的值(就是内存地址)进行排序,绝大多数情况下,这不是你所希望的。
第21条:总是让比较函数在等值情况下返回false。
现在我给你演示一个很酷的现象。创建一个set,用less_equal作为它的比较类型,然后把10插入到该集合中:
set
s.insert(10);
s.insert(10);
对于第二个insert,集合会检查下面的表达式是否为真:
!(10a
结果集合中有两个10!
从技术上讲,用于对关联容器排序的比较函数必须为他们所比较的对象定义个“严格的弱序化”(strict
第22条:切勿直接修改set或multiset中的键。
如果你不关心可移植性,而你想改变set或multiset中元素的值,并且你的STL实现(有的STL实现中,比如set::
如果你重视可移植性,就要确保set和multiset中的元素不能被修改。至少不能未经过强制类型转换(转换到一个引用类型const_cast)就修改。
如果你想以一种总是可行而且安全的方式来许该set、multiset、map和multimap中的元素,则可以分5个简单步骤来进行:
1.
2.
3.
4.
5.
第23条:考虑用排序的vector替代关联容器。
标准关联容器通常被实现为平衡的二叉查找树。也就是说,它所适合的那些应用程序首先做一些插入操作,然后做查找,然后可能又插入一些元素,或许接着删掉一些,随后又做查找,等等。这一系列时间的主要特征是插入、删除和超找混在一起。总的来说,没办法预测出针对这颗树的下一个操作是什么。
很多应用程序使用其数据结构的方式并不这么混乱。他们使用其数据结构的过程可以明显地分为三个阶段,总结如下:
1.
2.
3.
第24条:当效率至关重要时,请在map::operator[]与map::insert之间谨慎作出选择。
假定我们有一个Widget类,它支持默认构造函数,并根据一个double值来构造和赋值:
class
public:
Widget();
Widget(double
Widget&
...
}
map
map
m[1]
语句m[1]
typedef
pair
result.first->second
我们最好把对operator[]的调用换成对insert的直接调用:
m.insert(IntWidgetMap::value_type(1,1.50));
这里的效果和前面的代码相同,只是它通常会节省三个函数调用:一个用于创建默认构造的临时Widget对象,一个用于析构该临时对象,另一个是调用Widget的赋值操作符。
请看一下做更新操作时我们的选择:
m[k]
m.insert(IntWidgetMap::value_type(k,v)).first->second
insert调用需要一个IntWidgetMap::value_type类型的对象,所以当我们调用insert时,我们必须构造和西沟一个该类型的对象。这要付出一个pair构造函数和一个pair析构函数的代价。而这又会导致对Widget的构造和析构动作,因为pair本身又包含了一个Widget对象。而operator[]不使用pair对象,所以它不会构造和析构pair或Widget。
如果要更新一个已有的映射表元素,选择operator[];如果要添加一个新的元素,选择insert。
第25条:熟悉非标准的哈希容器。
标准C++库没有任何哈希容器,每个人认为这是一个遗憾,但是C++标准委员会认为,把它们加入到标准中所需的工作会拖延标准完成的时间。已经有决定要在标准的下一个版本中包含哈希容器。
第4章
第26条:iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator。
减少混用不同类型的迭代器的机会,尽量用iterator代替const_iterator。从const正确性的角度来看,仅仅为了避免一些可能存在的
第27条:使用distance和advance将容器的const_iterator转换成iterator。
下面的代码试图把一个const_iterator强制转换为iterator:
typedef
typedef
typedeef
ConstIter
...
Iter
Iter
包含显式类型转换的代码不能通过编译的原因在于,对于这些容器类型,iterator和const_iterator是完全不同的类,他们之间的关系甚至比string和complex之间的关系还要远。
下面是这种方案的本质。
typedef
typedef
typedeef
IntDeque
ConstIter
...
Iter
advance(i,distance(i,ci));//移动i,使它指向ci所指的位置
这中方法看上去非常简单和直接,也很令人吃惊。为了得到一个与const_iterator指向同一位置的iterator,首先创建一个新的
第28条:正确理解由reverse_iterator的base()成员函数所产生的iterator的用法。
如果要在一个reverse_iterator
如果要在一个reverse_iterator
我们还是有必要来看一看执行这样一个删除操作的实际代码,其中暗藏着惊奇之处:
vector
...
vector::reverse_iterator
v.erase(--ri.base());
对于vector和string,这段代码也许能工作,但对于vector和string的许多实现,它无法通过编译。这是因为在这样的实现中,
既然不能对base()的结果做递减操作,那么只要先递增reverse_iterator,然后再调用base()函数即可!
...
v.erase((++ri).base());
第29条:对于逐个字符的输入请考虑使用istreambuf_iterator。
假如你想把一个文本文件的内容拷贝到一个string对象中,以下的代码看上去是一种合理的解决方案:
ifstream
inputFIle.unsetf(ios::skipws);//istream_iterator使用operator>>函数来完成实际的读操作,而默认情况下operator>>函数会跳过空白字符
string
然而,你可能会发现整个拷贝过程远不及你希望的那般快。istream_iterator内部使用的operator>>实际上执行了格式化的输入,但如果你只是想从输入流中读出下一个字符的话,它就显得有点多余了。
有一种更为有效的途径,那就是使用STL中最为神秘的法宝之一:istreambuf_iterator。
ifstream
string
这次我们用不着清楚输入流的skipws标志,因为istreambuf_iterator不会跳过任何字符。
同样的,对于非格式化的逐个字符输出过程,你也应该考虑使用ostreambuf_iterator。
第30条:确保目标区间足够大。
当程序员希望向容器中添加新的对象,这里有一个例子:
int
vector
vector
transform(values.begin(),values.end(),back_inserter(results),transmogrify);
back_inserter返回的迭代起将使得push_back被调用,所以back_inserter可适用于所有提供了push_back方法的容器。同理,front_inserter仅适用于那些提供了push_front成员函数的容器(如deque和list)。
当是使用reserver提高一个序列插入操作的效率的时候,切记reserve只是增加了容器的容量,而容器的大小并未改变。当一个算法需要向
vector
vector
...
results.reserve(results.size()
transform(values.begin(),
在以上代码中transform欣然接受了在results尾部未初始化的内存中进行复制操作的任务。由于赋值操作重视在两个对象之间而不是在一个对象与一个未初始化的内存块之间进行,所以一般情况下,这段代码在运行时会失败。
假设希望transform覆盖results容器中已有的元素,那么就需要确保results中已有的元素至少和values中的元素一样多。否则,就必须使用resize来保证这一点。
vector
vector
...
if(results.size()
results.resize(values.size());
}
transform(values.begin(),values.end(),results.begin(),transmogrify);
或者,也可以先清空results,然后按通常的方式使用一个插入型迭代起:
...
results.clear();
results.reserve(values.size());
transform(values.begin(),values.end(),back_inserter(results),transmogrify);
第31条:了解各种与排序有关的选择。
sort(stable_sort)、partial_sort和nth_element算法都要求随即访问迭代器,所以这些算法只能被应用于
第32条:如果确实需要删除元素,则需要在remove这一类算法之后调用erase。
1
调用remove(v.begin(),v.end(),99);后变成
1
remove无法从迭代器推知对应的容器类型,所以就无法调用容器的成员函数erase,因此就无法真正删除元素。其他两个算法remove_if和
第33条:对包含指针的容器使用remove这一类算法时要特别小心。
无论你如何处理那些存放动态分配的指针的容器,你总是可以这样来进行:或者调用remove类算法之前先手工删除指针并将它们置为空,或者通过引用计数的智能指针(
下面的代码利用第一种方式:
void
{
if(!pWidget->isCertified())
{
delete
pWidget
}
}
for_each(v.begin(),v.end(),delAndNullifyUndertified
v.erase(vemove(v.begin(),v.end(),static_cast(0)),v.end());
下面的的代码使用第二中方式:
template
class
tpedef
vector
...
v.push_back(RCSPW(new
...
v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());
第34条:了解哪些算法要求使用排序的区间作为参数。
下面的代码要求排序的区间:
binary_search
upper_bound
set_union
set_difference
merge
includes
下面的算法并不一定需要排序的区间:
unique
第35条:通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较。
用mistatch实现:
//此函数判断两个字母是否相同,而忽略它们的大小写
int
{
int
int
if(lc1
if(lc1
return
}
int
{
if(s1.size()
else
}
//如果s1和s2相同,返回0;如果s1比s2短,返回-1;如果s1和s2都是在非结尾处发生不匹配,有开始不匹配的那个字符决定。
int
{
typedef
PSCI
if(p.first
if(p.second
else
}
return
}
用lexicographical_compare实现:
bool
{
return
}
bool
{
return
}
第36条:理解copy_if算法的正确实现。
STL中没有copy_if的算法,下面是一个实现,但是不够完美:
template
OutputIterator
{
return
}
copy_if(widgets.begin(),
因为not1不能被直接应用到一个函数指针上(见41条),函数指针必须先用ptr_fun进行转换。为了调用copy_if的这个实现,你传入的不仅是一个函数对象,而且还应该是一个可配接(adaptable)的函数对象。虽然这很容易做到,但是要想成为STL算法,它不能给客户这样的负担。
下面是copy_if的正确实现:
template
OutputIterator
{
while(begin
if(p(*begin))
++begin;
}
return
}
第37条:使用accumulate或者for_each进行区间统计。
确保accumulate的返回类西和初始值类型相同。for_each返回的是一个函数对象。accumulate不允许副作用而for_each允许。(这是一个深层次的问题,也是一个涉及STL核心的问题,待解)
第38条:遵循按值传递的原则来设计函数子类。
在STL中,函数对象在函数之间来回传递的时候也是像函数指针那样按值传递的。因此,你的函数对象必须尽可能的小,否则拷贝的开销会很大;其次,函数对象必须是单态的,也就是说,它们不得使用虚函数。这是因为,如果参数的类型是基类类型,而实参是派生类对象,那么在传递过程中会产生剥离问题(slicing
试图禁止多态的函数子同样也是不实际的。所以必须找到一种两全其美的办法,既允许函数对象可以很大并且/或保留多态性,又可以与STL所采用的按值传递函数子的习惯保持一致。这个办法就是:将所需要的数据和虚函数从函数子中分离出来,放到一个新的类中,然后在函数子中设一个指针,指向这个新类。
第39条:确保判别式是“纯函数”。
一个判别式(predicate)是一个返回值为bool类型的函数。一个纯函数(pure
因为接受函数子的STL算法可能会先创建函数子对象的拷贝,然后使用这个拷贝,因此这一特性的直接反映就是判别式函数必须是纯函数。
template
FwdIterator
{
begin
if(begin
else{
FwdIterator
return
}
}
第40条:若一个类是函数子,则应使它可配接。
4个标准的函数配接器(not1、not2、bind1st和bind2nd)都要求一些特殊的类型定义。提供了这些必要的类型定义(argument_type、first_argument_type、second_argument_type以及result_type)的函数对象被称为可配接的(adaptable)函数对象,反之,如果函数对象缺少这些类型定义,则称为不可配接的。可配接的函数对象能够与其他STL组件更为默契地协同工作。不过不同种类的函数子类所需要提供的类型定义也不尽相同,除非你要编写自定义的配接器,否则你并不需要知道有关这些类型定义的细节。这是因为,提供这些类型定义最简便的办法是让函数子从特定的基类继承,或者更准确的说,如果函数子类的operator()只有一个形参,那么它应该从
对于unary_function,你必须指定函数子类operator()所带的参数的类型,以及返回类型;对于binary_function,你必须指定三个类型:operator()的第一个和第二个参数的类型,以及operator()的返回类型。以下是两个例子:
template
class
private:
const
public:
MeetsThreshold(const
bool
...
};
struct
public
bool
};
你可能已经注意到MeetsThreshold是一个类,而WidgetNameCompare是一个结构。这是因为MeetsThreshold包含了状态信息(数据成员threshold),而类是封装状态信息的一种逻辑方式;与此相反,WidgetNameCompare并不包含状态信息,因而不需要任何私有成员。如果一个函数子的所有成员都是公有的,那么通常会将其声明为结构而不是类。究竟是选择结构还是类来定义函数子纯属个人编码风格,但是如果你正在改进自己的编码风格,并希望自己的风格更加专业一点的话,你就应该注意到,STL中所有无状态的函数子类(如less、
我们在看一下WidgetNameCompare:
struct
public
bool
};
虽然operator()的参数类型都是const
如果operator()带有指针参数,规则又有不同了。下面是WidgetNameCOmpare函数子的另一个版本,所不同的是,这次以Widget*指针作为参数:
struct
public
bool
};
第41条:理解ptr_fun、mem_fun和mem_fun_ref的来由。
如果有一个函数f和一个对象x,现在希望在x上调用f,而我们在x的成员函数之外,那么为了执行这个调用,C++提供了三种不同的语法:
f(x);
x.f();
p->f();
现在假设有个可用于测试Widget对象的函数:
void
另有一个存放Widget对象的容器:
vector
为了测试vw中的每一个Widget对象,自然可以用如下的方式来调用for_each:
for_each(vw.begin(),
但是,加入test是Widget的成员函数,即Widget支持自测:
class
public:
...
void
....
};
那么在理想情况下,应该也可以用for_each在vw中的每个对象上调用Widget::test成员函数:
for_each(vw.begin(),
实际上,如果真的很理想的话,那么对于一个存放Widget*
list
for_each(lpw.begin(),
这是因为STL中一种和普遍的惯例:函数或函数对象在被调用的时候,总是使用非成员函数的语法形式(即#1)。
现在mem_fun和mem_fun_ref之所以必须存在已经很清楚了--它们被用来调整(一般是#2和#3)成员函数,使之能够通过语法#1被调用。
template
mem_fun_t
mem_fun(R(C::*pmf)
mem_fun带一个指向某个成员函数的指针参数pmf,并且返回一个mem_fun_t类型的对象。mem_fun_t是一个函数子类,它拥有该成员函数的指针,并提供了operator()函数,在operator()中调用了通过参数传递进来的对象上的该成员函数。例如,请看下面一段代码:
list
...
for_each(lpw.begin(),lpw.end(),mem_fun(&Widget::test));//现在可以通过编译了
for_each接受到一个类型为mem_fun_t的对象,该对象中保存了一个指向Widget::test的指针。对于lpw中的每一个
(ptr_fun是多余的吗?)mem_fun是针对成员函数的配接器,mem_fun_ref是针对对象容器的配接器。
第42条:确保less与operator<具有相同的含义。
operator<不仅仅是less的默认实现方式,它也是程序员期望less所做的事情。让less不调用operator<而去坐别的事情,这会无端地违背程序员的意愿,这与“少”带给人惊奇的原则(the
如果你希望以一种特殊的方式来排序对象,那么最好创建一个特殊的函数子类,它的名字不能是less。
第43条:算法调用优于手写的循环。
有三个理由:
效率:算法通常比程序员自己写的循环效率更高。
STL实现者可以针对具体的容器对算法进行优化;几乎所有的STL算法都使用了复杂的计算机科学算法,有些科学算法非常复杂,并非一般的C++程序员所能够到达。
正确性:自己写的循环比使用算法容易出错。
比如迭代器可能会在插入元素后失效。
可维护性:使用算法的代码通常比手写循环的代码更加简介明了。
算法的名称表明了它的功能,而for、while和do却不能,每一位专业的C++程序员都应该知道每一个算法所做的事情,看到一个算法就可以知道这段代码的功能,而对于循环只能继续往下看具体的代码才能懂代码的意图。
第44条:容器的成员函数优先于同名的算法。
第一:成员函数往往速度快;第二,成员函数通常与容器(特别是关联容器)结合得更紧密(相等和等价的差别,比如对于关联容器,count只能使用相等测试)。
第45条:正确区分count、find、binary_search、lower_bound、upper_bound和equal_range。
想知道什么 | 使用算法 | 使用成员函数 | ||
对未排序的区间 | 对排序的区间 | 对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 | count | count |
具有特定值的对象都在哪里 | find(反复调用) | equal_range | equal_range | equal_range |
第46条:考虑使用函数对象而不是函数指针作为STL算法的参数。
函数指针抑制了内联机制,而函数对象可以被编译器优化为内联。
另一个理由是,这样做有助于避免一些微妙的、语言本身的缺陷。在偶然的情况下,有些看似合理的代码会被编译器以一些合法但又含糊不清的理由而拒绝。例如,当一个函数模板的实例化名称并不完全等同于一个函数的名称时,就可能会出现这样的问题。下面是一个例子:
template
FPType
{
return
}
template
void
{
transform(begin1,
ostream_iterator::value_type(s,"\n")>,
average::value_type> //错误?
);
}
许多编译器接受这段代码,但是C++标准却不认同这样的代码。原因在于,理论上存在另一个名为average的函数模板,它也只带一个类型参数。如果这样的话,表达式average::value_type>就会有二义性,因为编译器无法分辨到底应该实例化哪一个模板。换成函数对象就可以了。
第47条:避免产生“直写型”(write-only)的代码。
代码被阅读的次数远远大于它被编写的次数。
第48条:总是包含(#include)正确的头文件。
几乎所有的标准STL容器都被声明在与之同名的头文件中。
除了4个STL算法外,其他所有的算法都被声明在中,这4个算法是accumulate、
特殊类型的迭代器,包括istream_iterator和istreambuf_iterator(见第29条),被声明在中。
标准的函数子(比如less)和函数子配接器(比如not1、bind2nd)被声明在头文件中。
第49条:学会分析与STL相关的编译器诊断信息。
用文本替换(例如用string替换掉basic_string,class
第50条:熟悉STL相关的Web站点。
SGI
STLport站点:http://stlport.org
BOost站点:http://boost.org