在本书中,Herb Sutter采用了独具匠心的“提问/解答”的方式来指导你学习C++的语言特性;在本书的每个专题中,HerbSutter合理地设想出你的疑问和困惑,又有如神助地猜到了你的(可能是错误的)解答,然后给你以指点并呈现出最佳方案,最后,还提炼解决类似问题的一般性原则。
本书适合的读者对象是中高级程序员,尽管如此,只要具备基本的C++功底和一定的程序设计经验,你完全可以理解和消化本书的所有内容。
序
怎样才能成为专家?
1.掌握基础知识
2.将相同的内容再学习一遍,但这一次,请将你的注意力集中在细节上-这些细节的重要性,你头一次可能并没有意识到。
但应该怎样挑选合适的细节呢,本书提供了最佳答案。
一旦透彻理解了这些细节问题,你在编程时就不必劳神于细节;你就尽可以将注意力集中在真正需要尽力解决的问题上。
前言
《Exceptional C++》和《More Exceptional C++》在结构和主题而非内容上有重叠之处。
和前者相比,本书更强调泛型编程技术以及如何有效地使用C++标准库,并涉及了如trait和predicate这样的重要技术,有几个条款还深入分析了使用标准容器和算法时应该牢记的要点。
泛型程序设计与C++标准程序库
条款1:流 难度:2
在动态地使用不同的输入输出流-包括标准控制台流(console stream)和文件时,最佳使用方式是什么?
这个条款完全没搞懂,对于流这部分内容,我是一点都不熟,看来需要专门学习一下?看看有没有什么好使的东西!
设计准则
尽量提高可读性,避免撰写精简代码(即,简洁但难以理解和维护),避免晦涩。
在C++中,有四种方法获得多态行为:虚函数、模板、重载、转换。
设计准则
尽量提高可扩充性。
避免写出的代码只能解决当前问题,几乎任何时候,若能写出可扩充性的方案,那将是更佳选择-当然,只要我们不太过分。
均衡的判断力是有经验程序员所具有的一个特征。尤其是,在“编写专用代码,只解决当前问题”(短视,难以扩充)和“编写一个宏大的通用框架去解决本来应该很简单的问题”(追求过度设计)之间,有经验的程序员懂得如何去获取最佳的平衡。
所以,如果存在两个选择,它们在设计和实现中需要的工作量相同,而且具有大致相当的清晰度和可维护性,那么,请尽量考虑可扩充性。
设计准则
尽量提高封装性,将关系分离。
只要可能,一段代码-函数或类-应该只知道并且只负责一件事。
条款2:Predicates之一:remove()删除了什么?
1.std::remove()算法完成什么功能?
在《Generic Programming and the STL》已经有说明了,略。
2.写一段代码,用来删除std::vector<int>中值等于3的所有元素?
v.erase(remove(v.begin(),v.end(), 3), v.end);
3.删除一个容器中的第n个元素?
template<typename FwdIter>
FwndIter remove_nth(FwdIterfirst, FwdIter last, size_t n)
{
assert(distance(first,last)>= n);
advance(first,n);
if(first!= last)
{
FwdIterdest = first;
returncopy(++first,last, dest);
}
returnlast;
}
解释:刚开始还说怎么不用erase而用copy那么麻烦,后面才发现erase是容器的成员函数,不是算法,当然不能用了,呵呵。
条款3:Predicates之二:状态带来的问题 难度7
Predicates(谓词)是一个函数指针或一个函数对象(一个提供了函数调用运算符operator()的对象)。
当函数对象没有成员的时候,函数对象较函数好像没有什么好处。当每调用一次Predicates后都需要维护一些状态信息时,如果使用函数,就必须得借助静态变量之类的东西。而如果用函数对象的话,通过成员变量就可以解决了。
但是要让状态性Predicates正常工作,对算法必须有要求,得保证算法:
a.算法绝不能对predicate做复制(即,自始自终只能使用同一给定对象)
b.算法必须“以某个已知的顺序”将predicate作用到区间里的元素上。
可惜的是,C++标准没有要求标准算法提供以上两个保证。(但我看实际上标准算法都是按要求实现的,所以,我们尽可放心使用)
对于b,你必须完全依赖算法使用predicate的次序,我们无法逃避这一点。
对于a,我们可以通过智能指针保证函数对象被拷贝后,但其内部的状态维护实现实体只有一份,又最后一个被销毁的函数对象拷贝负责清除状态维护实现实体。
具体代码见书,智能指针的实现可能和auto_ptr差不多。
条款4:可扩充的模板:使用继承还是traits? 难度7
如何检测某个模板参数是否具有某个函数或者继承自某个类?
几个方法的本质就是采用转换,比如说在析构函数中转换函数指针或类指针。(因为这一块我觉得用得不多,仅仅是让自己的代码检查更严格而已,故不用太仔细研究)。
知道模板的参数类型T派生于某个其它类型,这对模板来说有什么用处呢?知道这种派生关系能带来某种好处吗?而且,这种好处在没有继承关系的情况下就无法获得吗?
对于一个模板来说,就算知道它的一个模板参数从某个给定的基类继承,这也不能让它获得“使用traits无法获得”的额外好处。使用traits仅有的一个真正的缺点是,在一个庞大的继承体系中,为了处理大量的类,需要写大量的特殊化代码;不过,运用某些技术可以减轻或者消除这一缺点。
本条款的主要目的在于说明:与某些人的想法相反,“为了处理模板中的分类而使用继承”不足以成为使用继承的理由。traits提供了更通用的机制;当用一个新类型-例如来自第三方程序库中的某个类型-来实例化一个现有模板的时候,此类型可能很难从某个预先确定的基类派生,此时,traits体现了更强的可扩充性。
条款5:typename 难度7
C++标准:
如果一个名称被使用在模板声明或定义中并且依赖于模板参数,则这个名称不被认为是一个类型的名称,除非名称查找到了一个合适的类型名称,或者这个名称用关键字typename修饰。
template<typename T>
class X_base
{
public:
typedef T instantiated_type;
};
template<typename A, typename B>
class X : public X_base<B>
{
public:
bool operator() (instantiated_type& i)const
{
return i != instantiated_type();
}
};
在上面的代码中,因为instantiated_type依赖模板参数T,所以它不被认为是一个typedef后的类型名,故不能用它来定义变量i等类型具有的行为。编译不过,达不到子类可以使用父类typedef名称的目的!
除非
template<typename A, typename B>
class X : public X_base<int>
{
public:
bool operator() (instantiated_type& i)const
{
return i != instantiated_type();
}
};
这样就能查找到一个合适的类型名称int,子类就可以用父类typedef生成的名称了,可以编译通过。
或者
通过关键字显式地告诉编译器X_base<B>::instantiated_type是一个名称,那么它就可以当名称用了。
template<typename A, typename B>
class X : public X_base<B>
{
public:
typedef typename X_base<B>::instantiated_type
instantiated_type;
bool operator() (instantiated_type& i)const
{
return i != instantiated_type();
}
};
书上说,如果不加typename,编译器就不知道X_base<B>::instantiated_type是什么东西,除了是名称还能是其他的吗?书上说有可能,我就没想到在什么时候有可能?
注意使用类模板时,无论什么时候在类名后面都要跟上模板参数,比如子类去继承时、X_base<B>::instantiated_type时,即模板类的完整类名是类名加上传递给模板参数的值。
条款6:容器、指针和“不是容器的容器”
char* p = &v[0]; //指针
vector<char>::iterator i =v.begin(); //迭代器
通常,当你想指向一个容器内部的对象时,一条不错的准则是:尽量使用迭代器而不使用指针。但是,迭代器和指针往往是在同样的情况下以同样的方式失效。迭代器存在的一个理由是,它提供了一种方式,用以“指向”一个被包含对象。如果可以选择,尽量使用迭代器来指向容器内部。
不幸的是,使用指向容器内部的指针得到的效果,并不总能通过迭代器来得到。使用迭代器有两个潜在的缺陷,只要其中一个落到你头上,你就要继续使用指针。
(1)能使用指针的地方,不一定总能方便地使用迭代器。
(2)如果迭代器是一个对象而不是一个普通指针,使用迭代器会招致空间和性能上的额外开销。
从满足标准容器和迭代器条件的意义上来说,vector<bool>不是一个容器。
例如,它有[]操作,但是不能取地址,别人也不能引用它内部的对象
int _tmain(int argc, _TCHAR* argv[])
{
vector<bool> v;
v.push_back(false);
//bool& a = v[0]; //编译不通过
//bool* b = &v[0]; //编译不通过
bool c = v[0];
return 0;
}
也就是说,vector<bool>为了节省空间,将每个bool存储到一个bit上,返回一个元素时,首先取到对应位,再转换成对应bool返回,造成速度上的损失。
结论:或许可以说,std::vector<bool>是符合标准的,但它不是个容器。(它也不是个很好的存储bit值的vector,因为它丧失了一些针对bit的操作功能,而提供这些操作是很合理的,std::bitset就提供了这些功能)
在一定程度上,vector<bool>可以作为一个例子,来演示如何写被代理容器。
谨防过早优化
如果你阅读过Exceptional C++,对于我反对“过早优化”的老生长谈,你应该见怪不怪了。那些规则可以总结为:(1)不要太早优化。(2)除非知道确实必要,否则不要使用优化。(3)即便那样,除非已经知道了要优化什么、哪里需要优化、否则也不要使用优化。
一般来说,对于自己所写的代码在空间和时间性能上的实际瓶颈,程序员(包括你我)的猜测糟糕的一塌糊涂。如果没有性能分析或其它时延数据指导你,你会很轻易地花掉几天的时间去优化一些不需要优化的东西。
很多情况下,vector<bool>和vector<int>这样的东西相比,如果它们之间存在性能上的差异,其差异也很可能微乎其微。
如果你正在为vector<bool>的“非容器性”所困,或者如果在你的环境中所测量出来的性能差异并非微不足道,而且这种差异对你的应用程序影响巨大,那么请不要使用vector<bool>。你可能首先会想到用vector<char>或vector<int>来代替它,并且,在对容器中的只进行设定时使用类型转换。但这样做很麻烦。更好的解决之道是使用deque<bool>;这是一个更简单的方案。
vector<bool>的名称有点让人误解,因为其内部元素根本不是标准的bool。
条款7:使用vector和deque 难度3
我们强烈建议C++程序员使用标准vector模板,而不要使用C风格的数组。(但是在程序中我们还是大量用数组,其实vector也是连续存储,通过reserve同样可以一次分配内存,并且取元素的个数比数组容易,以后可以尝试一下)
在默认情况下,请尽量在程序中使用vector,除非你需要在容器的头部执行有效的增加或删除操作,并且不需要底层对象连续存储。
如何将vector缩小至占用最小的内存?
如果想缩小一个已有的vector或deque,请运用“和一个临时容器交换”的手法。
例:
vector<Customer> c(1000);
//现在,c.capacity() >= 1000
//删除前10个元素之外的所有元素
c.erase(c.begin()+10, c.end());
//下面一行代码真的将c的内部缓冲区缩小至合适大小
vector<Customer>(c).swap(c); //用c构造一个临时vector,将其和c交换,然后临时对象被销毁
//现在,c.capacity() == c.size(),或者稍稍大于c.size()
//下面一行代码使得c真的为空
vector<Customer>().swap(c);
//现在,c.capacity()==0,除非vector实现强制让空vector包含某一最小容量。
条款8:使用map和set
主要说明了不能修改直接修改key这个事,我才不会那么做呢,没什么看头
条款9:主要是讲诉++操作要放在单独的一个语句中,不要在函数参数中++,没什么看头
设计准则
避免使用宏,宏往往使得代码更难以理解,从而更难维护。
设计准则
始终要为被重载的运算符保留正常语义。
l.erase(i++); //没问题,递增一个有效的迭代器
l.erase(i);
i++; //错误,i不是一个有效的迭代器
条款10:模板特殊化与重载
模板重载指模板函数名相同,但参数不同的模板函数
记住:非模板函数只有在完全匹配的时候才会被优先调用。
书上经常搞template<>,但是我在VS2005上根本编译通不过。
template<> void fun<int>(int);
优化与性能
条款12:内联
以前对内联的了解已经比较多了,这个地方只捡总结。
最后请记住,如果你想用什么方式提高效率,总是先借助你的算法和数据结构。它们会给你的程序带来数量级的整体改善,而内联之类的过程优化通常收效甚微。
设计准则
在性能分析证明确实必要之前,避免内联或详细优化。
条款13: 缓式优化之一:一个普通的旧式String(暂时未用到引用计数)
这个条款比较重要的是string的缓冲区分配策略,这个对以后的类似问题有借鉴意义。
(a)精确增长
不浪费空间,性能差
(b)固定增量增长
比如每次增加64个字节
空间浪费少,性能一般
(c)指数增长(通常最佳增长因子为1.5)
比如指数是1.5,则在向一个已经满载的100字节字符串添加一个字符时,将分配一个长度为150字节的缓冲区。
性能上佳,有些浪费空间
条款14:缓式优化之二:引入缓式优化
即让两个字符串对象在底层共享一个缓冲区,暂时避免拷贝操作;只是在确实知道需要拷贝的时候才进行拷贝。
条款15:缓式优化之三:迭代器与引用
条款16:缓式优化之四:多线程环境
上面都是讲字符串的,其实只要悉心研究一下CString和sting实现方式就行了,再说了解太多的原理也没用,只要会使用字符串类就行。
异常安全议题及技术
条款17:构造函数失败之一:对象生命周期
从构造函数中抛出异常意味着什么?
答:这意味着构造已经失败,对象从没有存在过,她的生命周期从没有开始过。确实,报告构造函数失败-也就是说,无法正确构造出某种类型的有效对象-的唯一方法是抛出一个异常。
顺便说一句,如果构造函数不成功,析构函数就永远不会被调用,其原因正在于此-没有东西可以摧毁,它无法死亡,因为它从来就没有存在过。请注意,这样一来,“一个对象的构造函数抛出异常”这句话实际上具有矛盾性。这样一种东西甚至不能被称为一个前对象(ex-object),它从没有生存过,从没有加入过对象家族。它是一个非对象(non-object)。
条款18:构造函数失败之二:吸收异常
这再次强化了那句格言:任何情况下,绝对不允许析构函数产生异常,写一个可以产生异常的析构函数是个不折不扣的错误。析构和异常水火不容。
来自C++标准第15.3款第10段:“在一个对象的构造函数或析构函数的function try block处理程序中,引用对象的任何非静态成员或基类将导致不可预测的行为。”
条款19:未捕获的异常
标准函数uncaught_exception()有什么用?何时该使用它?
条款20:未管理指针存在的问题之一:参数求值
未看
条款21:未管理指针存在的问题之二:使用auto_ptr?
我一直没用起来,什么时候用用
条款22:异常安全与类的设计之一:拷贝赋值
未看
条款23:异常安全与类的设计之二:继承
总结:
继承常被过度使用,即使有经验的程序员也是如此。无论何时,都要做到将耦合性降至最低。如果类的关系可以用多种方法表达,请使用关系最弱的那个有效方式。特别是,只有在委托不能独立完成使命情况下,我们才会使用继承。
说实话,我在平常中很少使用异常处理,也讨厌用异常,所以,这样都没怎么看!
其实重点不是在看这些语法书,而是平常多看写的好的程序,从中获取经验更值!!!