More Exceptional C++

来源:互联网 发布:大数据导论 教学大纲 编辑:程序博客网 时间:2024/04/29 11:38

在本书中,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:异常安全与类的设计之二:继承

 

总结:

继承常被过度使用,即使有经验的程序员也是如此。无论何时,都要做到将耦合性降至最低。如果类的关系可以用多种方法表达,请使用关系最弱的那个有效方式。特别是,只有在委托不能独立完成使命情况下,我们才会使用继承。

 

说实话,我在平常中很少使用异常处理,也讨厌用异常,所以,这样都没怎么看!

其实重点不是在看这些语法书,而是平常多看写的好的程序,从中获取经验更值!!!

0 0