《C++ Strategies and Statics》读书笔记

来源:互联网 发布:app数据统计模板 编辑:程序博客网 时间:2024/05/01 13:27

C++Strategies and Statics(C++战略战术)

 

它是一本每个专业C++程序员都应该读的书之一。---ACCU主席FrancisGlassBorow

 

awealth of 很多的

 

告诉读者如何在使用C++的过程中作出正确的选择

 

如果你曾经阅读过<<C++沉思录>>,那么这本书的程度和那一本在一定程度上是一致的,因为它们都不是关于语言介绍的,而都是关于如何使用C++的

 

阅读建议:每一章都是独立介绍一个主题,所以不用循序渐进,可以选择性阅读

 

第一章抽象和第九章 Resuability是最重要的,好好关注!!

 

大部分C++程序员的成功都是来自于工作实践中的惯用法和技巧,而这本书就是介绍这个的,除此之外还介绍了我们经常接触到的C++缺陷。(作者8年工作中学到的并被认为是最重要的战略战术)

 

第0章简介

C++是一门规模庞大的编程语言。只了解C++规则的程序员就和只知道棋子如何移动的棋手一样失败。为了获得成功,还需要学习一些相关的战略战术。

 

在本书中,我们就避免多谈理论,而尽量为读者给出例子及实用的建议。

 

第一章抽象

 

只要讲抽象,太难了,放到最后再看。

 

第二章类

在类的设计中,最重要的一部分就是对该类所表达的抽象模型有着一个清晰的理解:它和谁交互,它能做什么?我们在上一章中就关注于这个话题。一旦我们得到了这样的一个抽象模型,那么下一步我们要做的就是进行详细设计和类的实现了。在本章中,我们将围绕在这个阶段中的一些最常见的惯例以及bug进行讲述。

2.1构造函数

在本节中,我们将关注一些和构造函数相关的常见问题,这些问题中的部分会导致程序的执行速度变慢,另外一些则可能会导致程序中bug的产生。

2.1.1编译器提供的拷贝构造函数是否符合我们的要求?

编译器提供的拷贝构造函数是简单地进行位拷贝,大多数情况下,我们期望这样,这时我们不用自己提供拷贝构造函数。但是在某些情况下,这种位拷贝并不是我们想要的。比如String类和要实现统计一个类总共产生了多少个实例时等,具体例子代码这儿就不列出了。

对于缺省赋值构造函数是否能够工作这个问题,我们并没有一个通用的规则。一种从经验中得到的方法就是:对哪些包含指针的类要另眼相待当然,有时没包含指针也需要自己写拷贝构造函数(比如统计一个类总共产生了多少个实例),什么东西都没有绝对,要根据实际情况而定。

2.1.2 拷贝构造函数不可忽略

因为我们不能保证客户按我们的期望产生对象,所以,当我们确实因为某些原因使得为类实现拷贝构造函数变得非常困难,那么请把它声明为私有的,并且不要为它提供任何的定义。这儿体现出一种思想,那就是无论在什么情况下,保证正确性必须放在首位,其他的任何东西都可以牺牲。

2.1.3 类成员的初始化

当类中的某个数据成员本身也是一个类对象时,我们应该避免用赋值操作来为该成员进行初始化。因为在构造函数的函数体被执行前,对象中的所有对象都已经被它们的默认构造函数所初始化了。我们可以用构造函数初始化列表来一次性完成初始化,而不必调用多余的默认构造函数(据说可以提高30%的效率)。对于内建类型,因为没有默认构造函数,所以在初始化列表初始化不能得到效率的提升,不过提高代码可读性。当我们在编写构造函数的定义时,请在写完正式的参数列表后停下来一会,想一想有多少成员可以使用构造函数的初始化语法。通常我们都可以发现所有的成员都可以用这种方法来进行初始化,而当我们写构造函数的函数体时,将会发现其实我们什么也不需要做!

成员初始化的顺序

C++中规定,一个类中成员的初始化顺序和它们在类中被声明的顺序(而不是构造函数初始化列表中的顺序)必须是一致的。

如果一个类的成员是一个引用的话,必须在初始化列表中完成初始化。

其实本质上一个引用成员和一个指针成员是一致的,例

class Test

{

public:

    const int&ref;

    int i;

public:

    Test();

};

sizeof(Test)= 8

也就是说,引用和指针本质是一样的,只有在某些情况下,引用变量本身可能不占内存(被编译优化掉了),而不是以前理解的引用变量本身在任何情况下不占内存。

 

2.2赋值(=)

一般情况下,如果编译器提供的默认拷贝构造函数不合要求,那么默认赋值操作符函数几乎也可以被确定为错误的(反之亦然)。

写赋值操作符函数要注意一个问题,小心参数是自己,这种情况可能需要特别处理。例:

constString& String::operator=(cosnt String& s)

{

if(&s!=this)

    {

       delete []data;

       data = new char[strlen(s.data)+1];

       strcpy(data, s.data);

    }

}

请注意上面的赋值操作符。其中对于&s和this的比较是为了防范如下的代码:

Strings;

s= s;

一旦我们确信正在处理两个不同的对象,那么与复制构造函数不同的是,赋值操作符必须将原有值中所控制的资源释放掉。(这也就是我们代码中delete的作用)

2.2.1operator=返回值

赋值操作符应该返回一个被赋值对象的常量引用。这使得用户可以写出如下的代码:

Complexx,y;

x=y=Complex(0,0);

通过返回一个常量引用,我们就可以阻止将赋值结果作为左值的做法:

(a=b)=c;

 

2.3公有数据成员

避免使用公有数据成员,以免在实现变化的时候对客户代码代码产生影响。另一个原因就是公有数据还会使得我们的类难以用来保证不变量。比如一个类中有两个成员,一个分子,一个分母,如果设置成公有成员,我们没法保证客户将分母设置为0。但是如果写成一个赋值函数,我们可以对客户的值进行检查,如果设置的分母值是0,我们可以采取相应的措施。

2.4隐式类型转换

谨慎使用

 

2.5操作符重载:成员或非成员?

本质就是定义函数,根据含义而定

 

2.6重载、缺省值以及省略符

简单

 

2.7const

简单

 

2.8返回值为引用

和返回指针差不多,即不能返回一个自动变量的引用

 

2.9静态对象的构造

C++编译系统确保:所有的静态对象在它们被使用之前都会被初始化。

对于在同一个编译单元中出现的静态对象,它们的初始化顺序和它们在代码中出现的顺序是一致的。

在不同文件中的初始化顺序操作的顺序则是未定义的。

例:

externString default_name;

Stringdefault_src_file(default_name + ".c");

我们无法保证,default_name会早于default_src_file被创建。

对于上面的那个问题,我们没有一个简单的解决办法。如果我们有着一个依赖于定义在其他文件中的其他静态对象的静态对象(如default_src_file),我们就应该试着将这两个对象放在同一个文件中(这样我们就可以得到它们的初始化的确切顺序)。如果我们做不到这点,我们就不得不将这些初始化操作延迟到main函数被执行时才执行它们。

 

我发觉只要理解了小结的东西就OK了。(每一条都必须完全理解,为什么是这样?)

2.10小结

1.判断缺省的复制构造函数和赋值操作符的行为是否符合我们的期望,必要时重新实现它们。

2.避免使用赋值操作来初始化成员;使用构造函数初始化列表语法来完成初始化操作。

3.当编写复制操作符时,请检测s=s这种情况。

4.避免出现公有数据。

5.尽可能少地声明和使用隐式类型转换。避免在同一个类中出现两个(或多个)转换操作符。

6.一元操作符、复制操作符、()、[]、以及->应该被定义成成员函数,其他操作符应该被定义为非成员函数。

7.除非被调用函数需要它自己的对象拷贝,否则请使用传递常量引用的方式来调用函数。

8.在可能的情况下,将指针参数声明为指向常量的指针,将引用参数声明为指向常量的引用。(const char* p="123"; void foo(const String& s);)

9.在可能的情况下,将成员函数声明为const。

10.不同文件中的静态数据的初始化顺序没有被明确定义。

上面的10项中,我很少用5、6项,故不太熟,其他的应该没多大问题。

 

第3章句柄

数据抽象给我们带来的一个主要好处就是:对象的物理结构没有必要反映出它的抽象结构来。我们展示给用户看的模型具体的某些物理属性也不一定需要以模型中的形式出现在实现中。只要我们的对象可以和抽象模型相匹配,就可以以我们喜欢的方式来实现它。

尤其是当抽象模型表示的是一个对象“包含”另一个对象时,我们的实现也不需要将被包含的对象作为一个数据成员放置到包含它的对象中去,而可以让包含对象拥有一个用来表示被包含对象的句柄(handle)。(我们通常把这个用来实现被包含对象的对象称为rep。)从概念上来说,句柄是一个指向某种C++对象的指针,它通常也就是一个普通的C++指针。我们使用术语“句柄”的原因是:句柄本身也可以是某种C++对象,它可以用来提供和指针一致的功能。句柄可以给我们带来如下好处:(重点)

1.我们可以在实现中用尺寸大小固定的对象来表示尺寸大小不定的值。(比如一个String存放各种长度的字符串,但是sizeof(String)确是固定的)

2.我们可以在实现中用运行时绑定而不是编译时绑定的方式来处理对象。(有什么好处?)

3.对于实现的改变通常只会引起一次重新链接,而不是重新编译。(为什么?)

4.我们可以对他人隐藏对象的实现。

不过有利必有弊,句柄的实现需要我们编写更多的代码,并且它还会带来运行时的开销。

 

所有包含了以指针为实现的句柄的类都应该至少包含如下的函数:

构造函数和析构函数、拷贝构造函数、赋值操作符

 

3.2使用计数器来避免多份拷贝

3.2.3Copy on Write

可以参考MFC中的CString(在这儿是String),道理是一样的。

告诫:

使用了计数器的类要比没有使用计数器但功能相同的类要复杂得多,并且所有围绕着该计数器的操作都将导致程序执行时间的显著增加。如果用来复制值的时间足够短(这可能是由于该值占据的空间很小,导致复制它的成本很低;也可能是因为我们很少去复制它们),将类改为使用计数器的方式会导致程序的运行变慢。我们应该经常做一些性能上的度量,以确信这种优化的结果不会让我们太失望。

对于哪些哦拿过来管理计数器的代码,我们应该仔细地对之进行审察。那些牵涉到计数器使用的bug通常都是很难找的。

 

3.7综述

句柄是C++中的一种普通使用的实现技巧。它们赋予了类设计者足够的弹性来处理不定大小的实现,减少编译期的依赖,阻止他人获得类的实现细节,以及为同一个抽象数据类型提供不同的实现。所有的这些方法都会导致一些运行时的开销(至少是对每个操作有一个额外的内存引用),但通常情况下,与获得的好处相比,这种开销都是值得的。

 

3.8小结

1.使用句柄来用常量大小的对象表示不定大小的值。

2.rep类要么是一个嵌套类(rep的成员可以为公有),要么是一个所有成员都为私有、但“真正”的类是它的友元类的类。(一句话,rep类只为目的类而存在,管你用嵌套还是友元实现这个目的

3.使用计数器可以避免额外的赋值操作,但我们还是需要对性能进行检测以确信使用它真的可以带来性能上的提高。(参考CString的实现

4.句柄可以用来确保对实现(而不是接口)的改变只会要求用户进行重新连接而不是重新编译。它们同样也可以对他人隐藏实现的细节。

5.句柄可以允许构造函数基于参数从一系列的实现中挑选合适的实现。

6.句柄不一定是一个普通的指针,它们也可以是“智能指针”对象。

 

第四章继承

许多对于继承的讨论都始于对语言规则的解释。当我们需要通过了解规则来使用这种特性时,我们应该首先确信我们对在设计的何处使用继承有着充分的了解。有着不恰当继承的程序也可以通过编译,但它们总是非常难以被理解和维护。

4.1is-a关系(是一个关系)

继承应该用于“新类(派生类)描述的对象集基类描述的对象集的一个子集”这种情况。

4.1.1has-a(有一个关系)

当基类是派生类的对象的一部分时,我们就不应该再使用继承了,而应该使用组合。

4.1.2在派生类中删除某些操作

所有在基类中的操作应该能够应用到派生类中去。虽然派生类可以通过重写基类的成员函数来为实现自己的行为,但是派生类不能通过将基类的操作定义为私有的来删除在基类中合法的操作。

任何试图在派生类中对操作进行限制的行为通常都意味着继承架构设计上的失误。

4.1.3私有、保护以及公有继承

在C++中有着三种不同的继承方式:公有、保护和私有。在所有的继承方式中,派生类的成员都可以访问基类中的公有的和保护的成员。(差点记混了)这三种不同的继承方式之间的区别在于派生类向其用户提供的接口,以及用户何时才可以隐式地将一个指向派生类的指针转化为一个指向基类的指针。

使用什么样的继承,基类的公有成员和保护成员在派生类中就是什么样的成员。

当使用了公有继承后,基类中的公有成员也就同时成为了派生类中的公有成员,它里面的保护型成员也成为了派生类中的保护型成员。(也就是使用派生类的成员时,不要区分这个成员它继承而来的还是自己的,此时你的眼中应该只存在派生类,所有的东西都是派生类固有的)。一个指向派生类的指针可以被隐式地转化为一个指向公有基类的指针。(因为此时仍然保持通过基类指针可以做的事情通过子类指针然后可以做特性)

当私有继承被使用时,基类中的公有成员以及保护型成员都变成派生类中的私有成员。一个指向派生类的指针不能被隐式地转化为一个指向私有基类的指针。(因为通过派生类指针不能做的事情通过基类指针却可以做,显然不符合概念。实际上,我们需要慎重考虑使用私有继承,因为这样触犯了在派生类中删除某些基类操作条款,正常情况下是基类提供的接口在派生类也应该提供)。

如果我们使用的是保护继承,基类中的保护型和公有成员都将成为派生类中的保护型成员。并且不能隐式地将一个指向派生类的指针转化为一个指向保护型基类的指针(书中错了)。(道理同私有继承)

 

4.2公有继承

当继承是接口的一部分时,我们就应该使用公有继承。也就是说,我们希望向用户讲解X和Y(X派生自Y)之间的is-a关系。和接口的其他部分一样,我们(在某种程度上说)也就是保证了永远不会对基类的这部分进行修改!这是因为用户可能会编写依赖于“从指向Derived的指针到指向Base的指针之间的隐式转换”的代码。

4.3私有继承

当继承不是接口的部分(而是实现的细节)时(也就是说,我们继承基类不是为了继承基类的接口,而是用基类来实现派生类的某些接口),我们应该使用私有继承。用户不可能写出依赖于这种继承方式的代码,即用户不能编写依赖于“从指向Derived的指针到指向Base的指针之间的隐式转换”的代码。

实际上私有继承体现的关系已经不是is-a关系了,而仅仅是一种使用关系(或者说has-a)关系,所以,我们已经没有多少使用它的必要性了。由于组合体现的是一种has-a关系,我们为什么不使用组合来代替呢?与从一个基类继承相比,我们可以将一个基类的对象声明为派生类的成员。这样做不会带来任何时间和空间上的开销,并且得到的类也容易让人理解(阅读代码的人们不一定会去记忆哪个成员函数是从它的私有基类中继承而来的这个问题)

在大多数情况下,没有基类的类总是要比和它功能相同但却使用了私有继承的类要容易理解和扩展。(在AVS项目的通信底层,就出现了这种情况,将线程类和套接字类作为组合类使用,应该好理解得多)。如果我们使用了组合这种方式,那么当以后我们希望给这个类增加一个新的基类(成员)时,我们只会得到一个简单的继承架构,而不会得到复杂的多重继承架构。在很多的架构中,使用多重继承的代码总是比使用简单继承的代码要慢,占用的空间也要大一些,并且还会使我们对它的理解过程变得更难。

在如下情况下,我们将会得到使用私有继承的一个特例:我们在派生类中需要重写基类中的虚函数,但又不希望对基类的使用暴露于公有接口中。如果被重写的函数是析构函数时,最简单(也是唯一)的做法就是使用私有继承。(这种情况我看我很难遇到)

4.4保护型继承

保护型继承和私有继承类似,大部分情况下都可以用组合代替。我从来都没有使用过保护型继承,也没有听说有人在哪个项目中使用过它。我们并不是说保护型继承没有用--如果派生类中的成员需要重写(保护型)基类中的虚函数,我们就可以使用保护型继承了。但如果可以使用组合,我们就应该使用组合:因为使用语言中的晦涩特性(如保护型继承)会增大理解程序的难度。

4.5与基类抽象的一致性

当我们既想通过抽象方式操作具体对象,又想根据具体对象完成对应的具体操作时,虚函数是一个值得考虑的解决方案。

4.6纯虚函数

提供一个公用接口,具体类必须实现它。

4.7有关继承的细节和陷阱

4.7.1没有被继承的东西

当我们使用继承时,我们必须时刻牢记,下面的这些东西没有从基类被继承:

1.构造函数(派生类中某种类型的构造函数默认调用基类对应类型的构造函数)

2.析构函数(派生类中某种类型的析构函数默认调用基类对应类型的析构函数,如果基类的析构函数是虚函数,子类的析构函数也是虚函数)

3.赋值操作符(默认调用基类的赋值操作符)

4.被隐藏的成员函数。如果在基类中存在的成员函数在派生类中没有被重写,并且在派生类中还声明了一个和该函数有着相同名字但参数列表不同的成员函数,那么在基类中的那个成员函数就将被隐藏。(以前还没注意过这个问题)

4.7.2在派生类中指定virtual关键字

当我们重写一个虚函数时,在派生类中virtual不是必须的。但我们最好还是把它加上,这会使那些阅读我们代码的人更容易了解我们的代码。

4.7.3在构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数时实际调用的都是构造函数和析构函数所在类的那个虚成员函数。这是由构造函数和析构函数依次将虚表修改所属自己类而引起的。(只要理解了构造函数和析构函数的隐藏行为就好理解了:构造函数会暗中首先调用基类的构造函数,然后是初始化本类的虚表,然后才是执行我们所见的函数体。而析构函数的执行情况刚好相反,首先执行我们所见的函数体,然后暗中将虚表更改为基类的虚表,最后才是调用基类的析构函数)。

 

4.8小结

这章在前面的理解比较具体,故小结条目在此暂不列出。

 

第五章多重继承

我们将侧重于为什么要使用多重继承来进行设计。

5.1作为交集的多重继承

如果一个有着同样名字的成员存在于两个以上的基类中,并且派生类中没有定义该成员,那么对这个成员使用未限定形式的访问将会产生歧义(编译错误)。通过在调用函数时为该函数指定基类名的方式,我们可以去掉这种歧义:

t.Computer::reset();//OK

另外一个更通用的用来消除歧义的方法就是在派生类定义一个成员函数,用它来完成我们想要做的事情:

voidSmart_telephone::reset

{

    Computer::reset();

    Telephone::reset();

}

 

classD:public B1,public B2{

表示D既是B1,也是B2。

 

5.2虚基类

当使用多重继承来建模is-a关系时,如果派生类中可能包含同一基类的多份拷贝,我们就应该使用虚基类。在两个类中存在多份的is-a关系没有任何意义:X和Y之间的is-a关系只能是存在或否。说两次“X是Y”毫无意义。

例:

classA{

public:

    int i

}

classB: public A{};

classC: public A{};

classD: public B, public C{};

在上面的代码中,一个D对象中同时存在两个不同的A子对象。在很多的设计中,同时具有两个A子对象并不是一件正确的事情;相反,我们应该让B和C这两个基类同时共享一份A子对象。我们可以通过把A声明为一个虚基类来做到这一点。

classB: public virtual A{};

classC: public virtual A{};

如果在一个对象中同时包括了同一基类的虚拟和非虚拟版本,那么只有虚基类才会被共享。(

例:

class A

{

public:

    int d;

};

class B:public virtual A{};

class C:public A{};

class D:public A{};

class E:public B,public C, public D{};

那么sizeof(E)= B(4+4)+C(4)+D(4)=16

当class C:public virtual A{};之后:

sizeof(E)= B(4+4)+C(4+4)+D(4)-A(4)=16

注意,虽然都是16,但是算法是不一样的。

 

虚基类通常都是由最外层派生类来构造的。在我们的例子中,用来创建A子对象的责任落到了D的构造函数上,在B和C的构造函数中,用于初始化基类A的那部分代码则不会被执行。

 

当多个基类中都包含同样名字的成员,并且我们在派生类中使用该成员时,编译器会从多个名字中挑选出最合适的那个名字(就近原则)。但上面的B,C,D之间是平等的(选择名字时是平等的,但被构造的顺序是和出现的先后顺序一致的),当编译器无法确定最合适的那个时,就报编译错误!

 

虚基类会导致事情变得更加复杂。只有在必要的情况下,我们才应该使用它们。但对于我们的这个设计来说,当两个彼此相交的集合同时都是另外一个集合的子集时,使用虚基类来描述这种is-a关系就是最恰当的做法。

 

5.3一些有关多重继承的细节问题

5.3.1名字歧义

当多个基类声明(或继承)了有着同样名字的成员函数时,就很可能产生名字歧义。名字歧义经常(但不是通常)都暗示出我们的设计不是那么尽善尽美。

5.3.2基类间的初始化

在类声明中有关基类出现的顺序决定了如下三件事情:

1.派生类中的基类部分的构造顺序和它们在声明中出现的顺序相一致。(不过虚基类是个特例)。

2.派生类中的基类部分将会以它们被构造的顺序相反的顺序被析构。

3.该顺序可以对存储控件的布局细节产生影响,但它不应该影响到程序的作用。

5.3.3有关虚基类的初始化

如果虚基类中不存在缺省的构造函数,那么它就必须在每个派生类中都被初始化。

class A

{

public:

    A(int i);

    int d;

};

A::A(int i)

{

}

class B:public virtual A

{

public:

    B(int i);

};

B::B(int i)

:A(i)

{

}

class E:public B

{

public:

    E(int i);

};

E::E(int i)

:A(i),B(i)

{

 

}

也就是说,在E中必须显式地构造A,B不会再担负构造A的责任。因为B这种角色可能有好多个,我们不应依赖B和其同类的出现顺序决定A的最终状态。把A交给E去构造就可以消除这种依赖关系,何乐而不为呢?(印证了:虚基类通常都是由最外层派生类来构造的。在我们的例子中,用来创建A子对象的责任落到了D的构造函数上,在B和C的构造函数中,用于初始化基类A的那部分代码则不会被执行。)

5.3.4指定存取类型

当声明一个派生类时,我们应该为每个基类都执行相应的存取类型。如果我们没有为它们指定相应的存取类型,那么对于声明为class的派生类来说,缺省的存取类型就是私有的;对于struct来说,缺省存取类型为公有的。(但最好指定,别依赖缺省)

 

5.4小结

1.只有在派生类包括多个基类,并且这多个基类间没有继承关系时(容易忽略这点),我们才应该使用多重继承。

2.使用虚基类来避免在同一对象中出现多份基类子对象。

3.基类对象的构造顺序和它们在类声明中的顺序一致。

4.为每个基类明确地指定存取类型。

 

除了虚基类的内存布局:

基类1(不包含虚基类)

基类2(不包含虚基类)

...

虚基类1

虚基类2

...

但是在虚基类指针存放的什么没搞懂

 

这章没有其它什么问题了。

 

第6章考虑继承的设计

类向外提供两种接口:一种面向对象;另外一种则面向它的派生类.

6.1被保护的接口

依照C++的语言规则来说,类的面向用户的接口包括它的公有成员,面向派生类的接口则同时包括它的公有和保护型成员。但是我们在第一章中学到的知识在此同样适用:由类所提供的抽象模型要比它成员的类型签名和存取类型都重要。一个不能提供让人容易理解并详细记录的抽象模型的类的用途不会太大。

一个被设计为不能被继承的类同样也可以向其用户提供一个很好的抽象模型,但它向其派生类所展示的抽象模型则完全是随机构造的!这个随机接口的可用性都是很小的。

应该强调的是,我们在此讨论的是如何设计一个将要被其他拿去当作基类使用的类。很显然,如果我们是要被卖出去的类库的所有者,那么我们就可以通过修改相应的基类来修正我们可能碰到的所有问题。在这种情况下,对不恰当的继承的担心在此没有什么意义。问题只出在用户对于学要被修改的类没有所有权的时候。

我们先来看看导致不恰当的继承的几种原因。

6.1.1基类中的成员函数不是虚函数

当派生类需要重写的函数在基类中并没有被声明为虚拟时,我们就很容易得到不恰当的继承。(如果我们希望其他人来继承我们的类,请确保该类的析构函数是虚函数,即使这个析构函数不本来不需要被声明也应该这样做)。原因是很多时候我们都希望用基类的指针来操作子类,并且希望调用的是子类的函数。

6.1.2基类中的私有成员应该被声明为保护成员

派生类有时也可能需要访问基类中的私有成员。

6.1.3非虚基类

另外一个问题发生在“不是为继承所设计的类却被用于多重继承”的时候。

        Vehicle

       /     \

Land_vehicle   Sea_vhicle

      \      /

   Amphibious_vehicle

这个问题确实是难以预料的,在设计Land_vehicle和Sea_vehicle的时候谁会想到还有两栖交通工具要那样继承,妈的!!!

6.1.4基类中的假设

在基类的实现中可能存在着一些假设:它们对于基类来说是正确的,但对于派身类来说则不一定。例如,我们在基类中可以假设某些信息是非必须的,或者在基类中有着某些不变量(它们并不是被记录的抽象模型中的部分)。

 

6.2我们的设计是否应该考虑到继承?

是不是所有的类在设计时都必须考虑到继承呢?或者进一步说,我们是否应该把所有的成员函数和基类都声明为虚拟的呢?在此我们应该从如下两个方面来考虑:正确性和性能。

6.2.1正确性

我们将成员函数声明为虚函数是否会带来错误的结果呢?也就是说,假设我们在派生类中重写了该函数,我们是否还会期望在通过指向基类的指针调用该函数时调用到的是基类中的那个函数呢?(显然不是)。继承的所有观念都来自于只有对象(而不是调用者)才了解特定操作的实现细节。(也就是说,不管我们以什么方式来操作对象,都应该调用的是实际对象所拥有的操作,而不是根据调用者来确定)。总结:将函数声明为虚拟的不会导致问题的出现。

那么对于虚基类来说情况又如何呢?将一个公有基类声明为虚基类在概念上来说不会有什么错误。但是由于当存在虚基类时,所有的虚基类都会在最外层的派生类中被构造,故这时我们依赖于上层的类初始化虚基类将不会如愿所尝,解决办法是在派生类的每个构造函数中对虚基类进行初始化。

如同我们所见到的,将一个基类改为虚基类的变化还是很大的。它会对所有继承自它的派生类都产生一个强制要求---在构造函数中明确初始化它。

6.2.2性能

设计时考虑到继承会对性能带来什么样的影响呢?

将一个函数声明为虚函数时,实际上,检测到底该调用哪个版本的函数只会占用很小部分的开销,更大的开销来自于虚函数不能扩展为内联函数。

对于小函数来说,这两方面的开销考虑都很重要。对于大函数来说,这些开销和函数的执行时间比起来是微不足道的。所以,将一个小函数声明为虚函数可能会带来很大的性能影响,而将一个大函数改为虚函数则不会对程序的执行效率带来可测到的影响。如果我们对此还有什么疑问的话,那么对于中等和大规模的函数来说,请不要犹豫,将它们改为虚函数就是了。从另一方面讲,对性能的冲击并没有达到让我们一定要修改设计以避免使用虚函数和虚基类的地步,如果使用虚函数和虚基类可以让我们的设计更清晰,代码更容易被理解和维护,那么这些性能上的损失也是值得的。

6.2.3设计时就考虑继承会有一定的开销

我们从上面的讨论中可以发现:设计时就考虑继承会带来一些可观的性能开销,同时也会带来一些显而易见的好处。将函数声明为虚函数带来的运行时开销根据函数的大小和硬件架构的不一样也会变动得很大。将成员函数改为保护型而不是私有的会让我们难以(或不可能)在不破坏现有的派生类的情况下对实现进行修改。虚基类会对每个派生类的每个构造函数都提出需求。如果不需要使用到上述特性,我们也就可以避免这些开销。

6.3一些为继承所做的设计的例子

假设我们希望我们的类可以被其他人所继承。我们就应该在开始时就理解和记录下派生类所表示的抽象模型。(也就是考虑好派生类怎样使用我们的基类)。和类必须向其用户提供一个特殊的接口一样,被用来继承的类也必须向其派生类提供一个特殊的抽象接口(这个接口可能会不同于面向用户的那个接口)。派身类的职责必须被很好地记录下来并被人所理解。

两次重申,我们在此讨论的是:如何设计可能会被其他人继承的类。如果我们拥有所有的派生类,那么使用保护型数据将是一种很完美的解决方法。但如果我们设计的是要给其他人从中派生的类,保护型数据就不再适合了。

当我们在设计将要被其他类继承的类时,我们至少应该在类中声明了定义一个虚析构函数。

6.3.1抽象基类

抽象类不需要向外提供什么功能,它们只是被用来指定一个接口。实现该抽象模型的职责被交到派生类中去实现了。

6.3.2为派生类提供服务的类

另外一个为继承所做的设计就是用来为派生类提供服务的基类。考虑如下用来管理计数器的基类:

例子略

让基类来为派生类提供服务只是“一个类为另一个类提供服务”中的一个特例而已。除此之外,“一个类为另一个类提供服务”还包括:

1.has-a关系

2.use-a关系

3.基类可以调用在派生类中被重写的虚函数

4.基类中可以声明operatornew和operator delete成员函数,当派生类对象被创建和删除时我们会调用到它们。

5.一个指向基类的指针经常被用来“向下类型转换”为一个指向派身类的指针。(如果基类是虚基类宏阔这在派生类中存在两个基类子对象时,这种做法不值得信赖。)

下面给出了一些应该有基类提供的服务的例子:

1.对对象进行序列化

2.当对象被创建或被销毁时向外产生一条调试信息。

3.维护对象中的计数器(如我们上面给出的Uc_object类)

4.提供一种“聪明的”内存分配方式(operator new),将它用于对象的新建和删除操作中。

5.一个新的Manager类,它派生自Employee并同时也和其下属的Employee有着关联。

6.一个新的Big_shot_manager类,它可以用来批准不同的场合界限下的费用开支。

如果我们对此还不能确信,可以尝试先使用组合形式。如果它不能达到我们的期望,那么就考虑使用基类。

6.4结论

当我们在设计时考虑到继承时,我们应该应用那些在其他类中都是使用到的数据抽象的基础原则;我们必须意识到此时我们所面向的用户有两类而不是一类,我们应该避免将实现细节暴露给派生类。我们还应该知道其他人使用我们的类的可能方式,并在类中为派生类设计出相应的接口。

 

6.5小结

小结和前面的描述基本重复。

1.每个类都向外给出两个接口:一个面向用户,另外一个面向派身类。

2.对于一个会被作为基类的类来说,我们应该同时设计和记录下这两个接口(以及和它们相应的抽象模型)

3.为继承所进行的设计会带来运行时的性能损耗,并使得抽象模型更加复杂。对于某些类来说,它可能很有用,但对于其他类来说则非如此。

4.所有被用来继承的类都应该拥有一个虚析构函数

5.虚基类是在最外层的派身类中被初始化的

6.避免使用保护型数据

7.对小的,经常被调用的函数声明为虚函数会导致程序的运行速度显著变慢

8.对于中等和大规模的成员函数来说,将它们声明为虚函数时不要犹豫---这样做并不会给性能带来显著的变化

9.使用基类来提供影响到整个对象的服务

 

第7章模板

模板(也被称为参数化类型)可以被用来实现不依赖被操作的对象类型的算法和数据结构。

模板可以用来告诉编译器如何在编译期合成代码;从这点来看,模板有点类似于宏。与宏相比,模板有着如下的好处:

1.模板的定义看起来和类、函数的定义差不多;宏通常都需要额外的语法来保证预处理器正确处理它(如使用\来作为多行宏中的续行符,使用额外的括号来避免可能出现的优先级问题)

2.模板可以被自动实例化。编译器会检测模本的使用并会为没有被生成的模板实例自动合成合适的代码。而对于宏的用户来说,它们必须知道到底需要展开的是哪个宏,并且还必须确保宏不会对同一参数展开两次。

3.编译器会为模板产生更好的(所谓的“更好”只是一个相对的概念,实际上模板的出错信息还是很难理解的。)出错诊断信息。在宏中出现的错误很难被发现,这是因为大部分编译器仅仅是检测被调用的宏,而不是到底宏的那部分出错。

7.1模板类Pair

7.2一些有关模板的细节

1.模板类的两个不同实例间的关系必须被明确地区分开来。例如Pair<int,int>和Pair<double,double>是两个完全不同的类,它们之间没有任何关系。

7.3模本的实例化

不幸的是,现有的ISO/ANSIC++标准文档的草案并没有指定编译器应该如何实现模板的实例化。这意味着我们必须去咨询我们使用的编译器所附带的文档,来确定我们的模板成员函数应该放在什么地方。

再次重申一遍:不同的编译器对此的操作都不尽相同,我们必须根据我们编译器的文档来提供相应的机制。

7.4智能指针

这儿的例子是auto_ptr的一个雏形。

7.4.1对非自定义类型使用operator->

7.4.2使用了计数器的智能指针模板

7.5作为模板参数的表达式

大部分的模板参数都是类型,但我们也可以使用常量表达式作为我们的模板参数。

7.6模板函数

在函数的参数列表中,我们必须给出所有的类型参数信息(译注:原文这种说法是不完全正确的:我们可以不用在函数的参数列表中给出所有的类型参数,但只要在函数的调用语句中补足这些信息就可以了。如:

template<class T> foo();

foo<int>();

上面的代码就可以正常工作了。

7.6.2代码冗余

使用模板函数很容易得到大量的冗余的代码。模板必须精确匹配这条规则意味着:在通常情况下,我们需要为每个不同的类型集合产生一个独立的模板实例。在模板的实例化过程中不会出现隐式类型转换(如从Derived*转换至Base*).

我们必须谨慎地使用模板函数。它们最适合的地方是那些小函数,因为小函数不会带来太多的空间开销。

 

这一章没讲出模板什么东西,说得东西都平淡无奇。

7.7小结

1.当用于处理对象的逻辑和对象本身无关时,我们最好使用模板。

2.模板实例间的关系必须被明确地用程序注明。

3.当模板类同时也是模板参数时,请在两个>间加入空格以避免语法错误,如:

List<Set<int> >而不是List< Set<int>>

4.模板的实例化细节和我们使用的具体编译器相关

5.通过提供operatorT*和(对于指向自定义类型对象的指针来说)operator->,我们就可以实现智能指针模板

6.在模板中使用表达式作为参数使得使用模板的难度变大,但对于极度关注性能的类来说,这种不便还是值得的

7.尽量减小模板函数的规模

 

第8章模板的高级用法

在本章中,我们将会使用模板来构建用于提供基础的数据结构链表和集合的容器类。然后我们还会就如何对模板的设计和实现进行细化。(因为标准模板库中已经有了,所以这章对我没有什么吸引力,我们只要学会使用STL中的容器类就够了,没有花时间去理解容器类是如何实现的,常用的容器都被实现,没有必须自己实现某个类型的容器)

8.1使用了模板的容器类

由于管理容器的逻辑通常都和容器中的内容无关,所以用模板来实现容器是最适合不过的了。

 

8.9小结

1.用模板实现的容器应该是同类型的。我们可以提供使用一个包含有指向公共基类的指针(或“智能指针”)的容器来获得一个不同类型的效果。

2.指向容器内部的指针或引用的有效生命周期应该由容器的提供者来指定。理想情况下的生命周期应该是“直至容器被摧毁为止”;有着“直到下一次调用该操作”这样的生命周期应该避免。

3.迭代器应该指向元素之间或元素本身,在这一点上,由容器组成的集合应该达成共识。

4.模板中的每个成员都可以对其模板参数加以限制

5.将公用的逻辑独立出来并被所有实例共享可以使得常常从中受益

6.模板可以被特化。特化可以一直推迟到链接时才被确定。

 

第9章重用

重用是一种用来提高编程效率的最佳方案。然而,要做到那样并不容易。重用不只是会给我们带来好处,也会产生更多的开销。要想对代码获得成功的重用(如:我们从库中调用一个函数或者使用库中提供的类),那么我们从中所获得的好处必须要大于它产生的开销。那些开销包括:

寻找---用户必须从一堆可供使用的库中选出合适的库来

获取---用户必须在他/她的系统上拥有一个该库的版本

学习---用户必须理解该库所带的抽象模型,并熟悉其详细接口

控制权的丢失---用户不再具有对代码进行修改以使得它“正好”可以适合我们的应用这个权利。如果有两个彼此交互很紧密的库,这个问题就将变得愈发难以解决。

除错的效率变差---如果在项目中找到了bug,用户将不得不和代码的提供者一起合作来进行除错活动。如果我们没有其他的替代方案,这个问题将会变得十分严重。

风险---如果我们使用的库被证明是有问题的,或者速度很慢,或者没有应有的技术支持,那么我们就可能不得不将它从我们的项目中移除并重新亲自实现它。当开发进行到后期时,这就可能会导致项目的失败(而不仅仅是给项目成员的挫败感)。

更糟糕的是,开发人员通常都会低估构建自己所需使用的库所需的开销。我们必须保证,在准备重用时,可预期的好处必须要大于可预期的从头构建库所需的开销。

然而,重用在可行时仍然是一件让人兴奋的事情。本章将就“该如何在最小化开销时也取得受益”这个问题进行讨论,并将着重讨论下面的这些主题:

发现---如何让潜在的用户轻易地发现我们的库,并决定是否采用它。

健壮---如何使得我们的代码通过严格的考验,使得它能够以我们甚至不能预期的方式工作

名字冲突---如何来减少发生在我们库所提供的外部名字和程序中其他部分所定义的外部名字之间的名字冲突

性能---如何来分析和调整我们库运行时所需的空间和时间。

存储管理时导致程序bug和性能问题的一个常见来源,我们将会在讨论有关程序健壮性和性能的小节中对此进行讨论。

 

9.1发现和获得

程序员应该能够尽快地判断一个特定的库是否是有用的。如果是的话,那么它应该能够很容易地被设置并运行。

9.1.1简单的抽象

我们应该尽可能地用一行简短的总结性的文字来概括我们的库。

(如果办不到,很可能它不是高内聚的,意味着可能抽象得不对)

9.1.2文档

必须提供一份简洁的手册

9.1.3精心挑选的类名

为我们的类挑选一个合适的名字是设计库的过程中最困难的一部分。

9.1.4获取

 

9.2健壮性

避免对对象的大小进行假设是用于提高我们代码健壮性的一个重要途径。

9.2.1断言

作用:

1.内部查错

2.我们还可以使用断言来作为库中的前置条件校验。如果我们在文档中指出一个函数的参数不能为0,那么我们在该函数中所做的第一件事就是使用assert来判定是否为0。如果用户向函数中传递了一个0作为参数,那么我们就可以即时地在函数调用处获得一个断言错误,而不是在我们库中的某个地方得到一个程序崩溃的结果。(尽可能让崩溃发生在离错误的源头最近的地方)

9.2.2避免使用固定大小的数组

除非知道数组在多大时就已经足够大了,否则我们就应该使用容器来代替数组。

9.2.3为程序提供调试版本

在程序写一些供调试程序的代码(MFC中就有大量这样的代码)

9.2.4Linton法则

为了避免出现这种情况的bug,MarkLinton提出了如下的编程法则:

函数中不应该将指向其引用参数的指针保存在函数返回后还可以访问到的变量中。如果需要这么做的话,我们就应该使用指针参数。

 

9.3内存管理

在本节中,我们会展示一些用来保证正确删除的方案。这些方案中的核心思想就是让计算机以某种方式来记录被分配的内存,从而程序员就不需要时刻记着去删除它们。

9.3.1使用构造函数和析构函数来管理内存

最安全的方法就是在构造函数中使用new,在析构函数中使用delete。

9.4可选的内存分配方案

使用自动对象来处理内存的释放只适合于同一个函数中的内存分配和释放。然而我们不可能总是碰到这种情况下的内存分配和释放;在本节中,我们将会来考虑其他的一些常见的内存管理方案,它们甚至可以工作在“分配和释放不在同一个函数中”这种情况。

9.4.1使用计数器的智能指针

9.4.2通过记录所有的指针和对象来实现的垃圾收集机制

每隔一段时间,我们就会使用一个“垃圾收集器”去检测所有已有的指针,并对它们所指向的对象进行标注。如果存在未被标注的对象,它就会将该对象删除。(Java的做法,C++中现在还没有现成的垃圾收集机制)

9.4.3Arena

arena是用来管理内存的最简单的方案之一,但是它们只适用于某些特定的应用程序。在这些程序中,每个对象都是在arena中创建的。在后期,当arena被清空时,所有在arena中的对象都会被删除。

9.5传递参数给operaor new

如果我们希望控制对象到底存储在什么地方,C++也可以允许我们传递参数给new操作符。

9.6管理外部资源

我们必须时刻记住,世界上还有很多方法来结束一个进程并且还不会去调用那些已有自动对象的析构函数。

9.7寻找有关内存的bug

即使我们再努力,我们早晚还是要去调试有着内存问题的代码:问题有可能时内存泄漏,多次删除同一块内存,或者是使用以及被删除的内存。在本节中,我们将讨论一些常用的用来检测和修正这些bug的技巧。

9.7.1在析构函数中进行无意义的破坏

对于那些涉及到使用已经被删除的内存的bug,它们的处理尤为麻烦,因为这样的程序只要运气好,经常(而不是一直)都能工作。只有在内存分配器重新分配那些仍被使用的内存时,它们才会出现。如果在删除和(无效的)后续使用之间的间隔很小,那么内存分配器对该块内存的重新分配的几率会很小,这也使得我们很难重现该bug。

一种用来捕获这种bug的做法就是在每个析构函数中毫无理由地对该对象所使用的内存进行无效值填充。这样即使内存分配器并没有将这块内存重新分配,我们自后续对于该对象的使用时也可以即时地捕获这些bug。

如果对成员的填充所需的运行时间过长的话,我们可以选择条件编译的方式去除它:

Employee::~Employee()

{

#ifndefNDEBUG

    salary = -1;

#endif

}

9.7.2提供自定义的new和delete

9.7.3数组的删除

记住加[]

9.7.4利用商用的内存泄漏检测工具

9.8名字冲突

9.8.1类

选择一个不大可能与其他库相冲突的名字来作为我们的类名是很重要的一件事。

9.8.2函数

如同前面所说的那样,只要我们可以保证类名的唯一性,那么在参数列表中包含该类的函数就不可能与其他函数发生冲突。

具有C链接属性的函数

具有C链接属性的函数其签名中不包含它们的参数信息。因此,只要名字相同,不管它们所带的参数类型如何,所有具有C链接属性的函数都会发生名字冲突。

9.9性能

然而,随着程序员对于数据抽象(以及库的使用)的使用技巧提高,他们开始在一个较高的层次上面进行编程。这使得单个的程序员可以完成比以前要多得多的功能;但这也意味着,除非某些关键的代码,他们也变得不再去关心每行程序间的性能问题了。相反的是,程序员将会在一个更高的层次上来关注优化这个话题。

在开始进入讲解“如何让程序运行得更快”之前,我们先给出一个警告:程序得正确性要比它得速度更重要。和人力资源相比,计算机得运行时间的开销会变得越来越便宜。对于那些在提高性能的同时还会导致程序的可理解性和可维护性变差的方法,我们应该持谨慎态度。

9.10不要去猜想,而应该度量

9.11算法

9.11.1将性能复杂度记录到文档中

如果我们是库的提供商,我们就应该将库中函数的性能复杂度(尤其是那些运行时间不固定的函数)记录到文档中去。这样用户就可以用这些信息来评估他们应用程序的复杂度。

9.12动态内存分配中的瓶颈

9.12.1了解我们程序使用内存的方式

一旦确信问题不出在逻辑错误和有缺陷的算法上,我们该做的下一步就是了解内存是如何被使用的。这可以帮助我们了解如何来提炼我们的设计,或者帮助我们选择或设计一个特殊的内存分配器。

9.12.2尝试使用几种不同的内存分配器

在很多时候使用不同的malloc就是用来提升我们程序运行速度的最简单的做法。

9.12.3需要时编写自己的operator new

如果其他的方案都失败了的话,我们就可能需要编写自己的operatornew。(通过在自己设计的内存池中分配内存)

9.13内联

内联展开是C++中提供的一个最具威力的性能工具。

带有循环的函数不应该被内联化,迭代多次的循环所需的运行时间很容易将函数调用开销带来的影响消除,而且很多的编译器也不会对带有循环的函数进行内联化。

9.13.1内联函数调用内联函数

9.14Tiemann法则

最后,所有的库开发者都应该牢记tiemann法则:

“如果它从来没有被重用过,那它就不能被成为可重用的”--Michael Tiemann

Tiemannn法则也导致了下面这个推论的产生:

“如果它不可用的话,它也就不可被重用”

 

9.15小结

1.预期的用户应该通过阅读手册中的第一行就可以判断某个库是否可以被使用。

2.在我们的文档中,类名是其中最重要的部分。我们应该审慎地选择它们,并和其他人进行交流以确保它们对其他人也有着同样的含义。

3.频繁地使用断言来既是终止有问题的程序

4.除非知道数组的大小上限,否则我们应该避免使用固定大小的数组。

5.为库提供一个调试版本

6.不要使用诸如Object这样容易和其他库相冲突的名字来作为我们的类名。

7.使用静态成员来避免将名字导入全局名字空间

8.使用对象来管理内存,不要手动去管理它们

9.我们应该了解,进程在退出时可能不会去销毁所有的自动对象

10.即使里面的元素没有析构函数,我们也要用delete[]来删除数组

11.使用工具来查找内存泄漏的情况,编写自己的malloc和free,或使用商业化的产品

12.为了找到对已销毁对象的使用,我们应该在析构函数中为对象中的数据成员赋予一些明显无效的值。

13.程序的正确性要比其速度更重要

14.我们应该去度量性能,而不是猜测

15.记录下库函数的复杂度,尤其使那些不是以常量时间运行的函数

16.对算法进行改进通常都可以获得最大的性能提高

17.在对性能进行调整前,首先得确定导致性能问题的原因不是程序的bug

18.使用诸如Pool的类来构建快速的特定类的内存分配器

19.请牢记Tiemann法则

 

第10章异常

程序在运行过程中,有时候会产生错误。异常特性(见如下的评论)使得程序能处理那些非预期的情况而无需程序员显式地检查每一步代码来发现这些情况。在这一章中,我们将讨论在设计的什么地方应该使用异常,而在什么地方却需要避免。

10.2为什么需要异常

异常针对这些问题提供了一个好的解决办法,它们同样使得程序员在编码的时候无需考虑在函数的调用过程中会发生的错误。(不能理解)

10.3一个异常的例子

10.4异常只应该用来表述异常情况

异常不应该被用于程序的正常控制流。举例来说,一个异常不应该成为你是否到达一个链表是否到达一个链表结尾的标志符号。

在需要返回一个值就可以很好地工作的情况下,为什么还要使用到异常这样的高级特性呢?

10.5理解异常

10.6责任评估

首先,我们要搞清楚是谁或者什么导致了这个问题。最有可能的候选者如下:

10.6.1你

10.6.2其他程序员

如果你是一个库提供者,你将在你所提供的函数中的输入操作中提出一些限制。而必须遵守这些前置条件的责任就落到了使用你的库的程序员身上。如果在前置条件被破坏的情况下函数被调用,那么就应该产生一个相关的异常。比如说企图从3个字符组成的字符串中取第5个字符或者给一个函数传递一个非预期的空指针。

再次声明,用户的程序也不一定会去分析这些错误。出错消息应该能够让其他程序员理解,但是大多数程序都不会关心这两种情况之间的区别。(我所见过的程序员都是让程序直接挂掉,还没见谁抛异常)

10.6.3资源的提供者

有些问题并非是逻辑错误,而是由于在运行时对于资源的获得失败造成的。这些错误可能包括运行时内存耗尽,取得数据库锁失败,向磁盘写文件时磁盘满,系统调用超时,或者对于一些其他的资源申请失败(如许可证管理器中的许可证过期)。

这些错误是程序最有可能进行恢复的。举例略。

10.6.4用户

这些错误是由一些用户所导致的。它们可能包括破坏了输入的限制(比如在要求输入数字的时候输入了一个字母字符)或者请求了一个不合适的的操作(对一个只读文件进行写操作)。

对于用户的错误我们不应该使用异常。异常的最大好处就是可以让处理用户错误的代码和发现这些错误的代码相分离。然后,发现用户错误的代码往往也是处理这些错误的最好的代码。(故异常不擅长处理用户错误)

10.7设计异常对象

在进行异常对象的设计之前必须理解谁(或者是什么)来检测这个异常。如果是人负责检测这个异常,那么异常对象就应该本质上只是字符串。程序通常只关心异常的发生,而不是问题的细节。如果是由程序负责检测异常,异常对象就可能变得复杂一些,因为它将包含一些程序在检测中需要用到的数据。

回顾在第一章曾经讨论过的:在不确定情况下,我们偏向于把接口做得尽可能的小。任何遗漏的缺陷都可以通过添加新的异常类来弥补。但是如果你一开始就提供了大量的不同异常类,即使它们大多数都永远不会被用到,你也将受困于那些文档以及维护它们的工作。

10.7.1异常和继承

当设计异常对象的时候,我们可以考虑通过继承来封装两个或者多个类所持有的共同概念。

 

10.8小结

1.异常是C++中的一个新的特性,它正在不断变化中,请时刻关注标准化组织在这个领域方面的进展。

2.不要将异常用于正常的程序控制流程中。

3.在理解了异常将被人或者是程序来检测的基础上再设计你的异常类。

4.在你的异常类的相关联设计中使用继承。

5.不要把类取名为Exception(很可能带来名字冲突)

 

第11章向C++移植

11.3.1使用C++会导致设计阶段中得前期负担变重

C++中鼓励设计者在编码前花费大量的时间来充分地理解他所做的设计。这样做很有好处,因为在设计阶段中发现的问题较容易被解决。但这样做也带来另外一个问题,那就是:在我们进行第一行代码的编写前,我们必须花费大量的时间。

11.3.2迭代的设计方式

11.3.3专家们来设计类,其他人只管使用它们

11.3.4不要试图同时使用所有的特性

11.3.5使用通用的库

11.3.6分配更多的编译时间

11.3.7目标代码尺寸的增大

 

正文基本上没怎么看,重点放在小结

11.6小结

1.项目进度表必须为学习曲线预留余地

2.C++是一门优秀的通用编程语言,但对于特殊的应用程序来说,存在着更适用的编程语言来实现它们。

3.请逐步地采用C++,一开始将它作为更好的C来使用,然后开始采用数据抽象,最后使用面向对象的编程方式。

4.在使用C++的设计过程中,前期将会变得负担更重。

5.不要同时使用所有的特性

6.设计应该采用迭代的方式来进行

7.使用类要比构建它们容易

8.尽量使用通用库

9.用于处理高层次抽象的语言需要更多的编译周期。

10.我们程序的目标代码会变得更大。

11.除非我们计划对其进行新的开发,否则不要将已有的C代码转换为C++代码

12.重用表示一种业余爱好

 

本书虽然看完了,但实际学到的东西不多,以后不要再看这种关于语言的书了,应该把重点侧重到设计和技术上来,编程经验可以从工作中总结,语言怎么看都是那一点东西,看一百遍也没有什么进步。不过这本书的题可以好好思考,好像还不那么容易做出来。

 

 

 

 

 

 

 

 

 

 

原创粉丝点击