More Effective C++: 06杂项讨论

32:在未来时态下发展程序

         世事永远在变,好的软件对于变化有良好的适应能力:可以容纳新的性质,可以移植到新的平台,可以适应新的需求,可以掌握新的输入。所谓在未来时态下设计程序,就是接受“事情总会改变”的事实,并准备应因之道。

         要做到这件事情,办法之一就是以C++本身(而非只是注释或说明文件)来表现各种规范。比如,如果某个类在设计时绝不打算成为基类,那么就不应该只是在头文件的类定义上端摆一行注释就好,而是应该以C++语法来阻止派生的发生:如果一个类要求其所有对象实体都必须于heap内产生,那么应该以条款27厉行这项约束。如果复制和赋值对某个类没有意义,我们应该将其copy constructor和assignment operator 宣告为private(见条款27)。

你应该决定函数的意义,并决定它是否适合在派生类内被重新定义。如果是,就把它声明为virtual,即使当前并没有人重新定义之。如果不是,就把它声明为non-virtual。

为每一个类处理assignment和copy construction动作,即使没有人使用那样的动作。现在没有人使用,并不意味将来没有人使用。如果这些函数不易完成,请将它们声明为 private。

努力让类的运算符和函数拥有自然的语法和直观的语义。和内建类型的行为保持一致。

记住,任何事情只要能做,就会有人做。他们会丢出异常;会“将对象自己派给自己”;他们会在尚未获得初值前就使用对象、会给对象初值却从不使用它;他们会给对象过大的值、会给对象过小的值、会给对象空值。只要编译没问题,就会有人做。所以,请让你的类容易被正确地使用,不容易被误用。请接受“客户会犯错”的事实,并设计你的类有预防、侦测、或甚至更正的能力。

请设计你的代码,使“系统改变所带来的冲击”得以局部化。尽可能采用封装性质、尽可能让实现细节成为private;如果可用,就尽量用无具名的namespaces或文件内的static对象和static函数;尽量避免设计出virtual base classes,因为这种类必须被其每一个派生类(直接或间接)初始化;请避免以RTTI做为设计基础并因而导至一层一层的if-then-else 语句:因为每当继承体系一有改变,每一组这样的语句都得更新,如果你忘了其中一个,编译程序不会给你任何警告。

33:将非尾端类(non-leaf类)设计为抽象类

         考虑下面的代码:

class Animal {
public:
    Animal& operator=(const Animal&  rhs);
    ...
};

class Lizard: public Animal {
public:
    Lizard& operator=(const Lizard&  rhs);
    ...
};

class Chicken: public Animal {
public:
    Chicken& operator=(const Chicken&  rhs);
    ...
};

         Lizard(蜥蜴)和Chicken继承于Animal。针对上面的代码,如果有赋值操作:

Lizard liz1;
Lizard liz2;
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;

*pAnimal1 = *pAnimal2;

         因为operator=不是virtual的,所以上面的赋值操作将会导致部分赋值,也就是Lizard中的Animal部分会修改,而Lizard部分保持不变。

         如果使operator=成为了虚函数:

class Animal {
public:
    virtual Animal& operator=(const Animal&  rhs);
    ...
};

class Lizard: public Animal {
public:
    virtual Lizard& operator=(const Animal&  rhs);
    ...
};

class Chicken: public Animal {
public:
    virtual Chicken& operator=(const Animal&  rhs);
    ...
};

         virtual函数定义要求派生类中的版本必须与基类中的版本带有相同的参数,这就又引起了另外的问题:

Lizard liz;
Chicken chick;
Animal *pAnimal1 = &liz;
Animal *pAnimal2 = &chick;

*pAnimal1 = *pAnimal2; //将一只鸡指派给一只蜥蜴

         这是一种异型赋值,赋值运算符的左右两边的类型并不相同,因为C++是强类型语言,异型赋值向来不是问题,但是assignment成为虚函数,则打开了异型赋值的一扇门。

         如何避免局部赋值,又能避免异型赋值呢?可以通过dynamic_cast实现:

Lizard& Lizard::operator=(const Animal& rhs)
{
    // 确定 rhs  真的是一只蜥蜴
    const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);
    proceed with a normal assignment of rhs_liz to *this;
}

         只有rhs真的是Lizard时,才能将rhs赋值给*this,否则dynamic_cast会抛出异常。但是,这样一来,即使是正常的lizard1=lizard2这样的语句,也会使用dynamic_cast,它又需要访问一个type_info,显得得不偿失。

         还有一种方法,就是重载operator=:

class Lizard: public Animal {
public:
    virtual Lizard& operator=(const Animal& rhs);
    Lizard& operator=(const Lizard& rhs);
    ...
};
Lizard& Lizard::operator=(const Animal& rhs){
    return operator=(dynamic_cast<const Lizard&>(rhs));
}

Lizard liz1, liz2;
liz1=liz2;   //调用接受一个const Lizard的operator=

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
*pAnimal1=*pAnimal2;  //调用接受一个const Animal&的operator=

         但是这里还是用到了dynamic_cast。

         于是,只能回到原点:如何阻止clients一开始就作出有问题的赋值动作。解决办法是设计一个抽象类AbstractAnimal,使Animal、Lizard和Chicken都继承该类:

class AbstractAnimal {
protected:
    AbstractAnimal& operator=(const AbstractAnimal& rhs);
public:
    virtual ~AbstractAnimal() = 0;
    ...
};

class Animal: public AbstractAnimal {
public:
    Animal& operator=(const Animal& rhs);
    ...
};
class Lizard: public AbstractAnimal {
public:
    Lizard& operator=(const Lizard& rhs);
    ...
};
class Chicken: public AbstractAnimal {
public:
    Chicken& operator=(const Chicken& rhs);
    ...
};

这个设计允许Lizard、Chicken、Animal之间同型赋值;局部赋值和异型赋值都在禁止之列;派生类的assignment操作符也可以调用base class的assignment运算符。

AbstractAnimal是个抽象类,它必须内含至少一个纯虚函数。大部份时候,选出一个这样的函数不成问题,但是像AbstractAnimal这样的类,其中没有任何成员函数可以很自然地被声明为纯虚函数。这种情况下,传统作法是让析构函数成为纯虚函数。但是,一旦在抽象类中自己定义了析构函数(不管析构函数是纯虚函数,还是其他成员函数是纯虚函数),就必须定义析构函数的函数体。因为只要调用派生类的析构函数,就会调用到基类的析构函数。

将一个具体基础类如Animal者,以一个抽象基础类如AbstractAnimal者取代,好处不只在于让operator=的行为更容易被了解,也降低了“企图以多态方式对待数组”的机会(条款3)。这个技术最具意义的地方在于设计层面,因为将具体基础类以抽象基础类取而代之,可强迫你明白认知有用之抽象性质的存在。

如果你有两个具体类C1和C2,而你希望C2以public方式继承C1。你应该将原本的双类别继承体系改为三类别继承体系:产生一个新的抽象类A,并令C1和C2都以public方式继承A:

 

这种转变的主要价值在于,它强迫你实现抽象类A。很显然,C1和C2有某些共同的东西。如果采用上述转变,你就必须鉴定出所谓“某些共同的东西”是什么。而且必须将那些共同的东西形式化为一个类,使它比一个模糊的概念更具体化些,进而成为一个正式而条理分明的抽象性质,有着定义完好的成员函数和定义完好的语义。

只有在原有具体类被当做基础类使用时,才强迫导入一个新的抽象类。这样的抽象性是有用的,因为透过先前的阐述,它们证明了自己当之无愧。

当使用第三方的库时,如果你发现你需要产生一个具体类,继承自链接库中的一个具体类,而该库不能修改,怎么办?可以有以下的解决办法,但都不怎么吸引人:

1、将你的具体类派生自既存的(链接库中的)具体类,但需注意本条款一开始所验证的assignment相关问题,并且小心条款3所描述的数组相关陷阱。

2、试着在链接库的继承体系中找一个更高层的抽象类,其中有你需要的大部份功能,然后继承它。当然,这可能不是一个合适的类别,你也可能必须重复许多努力,这些努力其实已经存在于你希望为之扩张机能的那个具体类的实现代码身上。

3、以“你所希望继承的那个链接库类”来实现你自己的新类。例如,你可以令链接库中的类成为你的数据成员,然后在你的新类中重新实现该链接库类的接口:

class Window  { //这一个是链接库内的类
public:
    virtual void resize(int newWidth, int newHeight);
    virtual void repaint() const;
    int width() const;
    int height() const;
};

class SpecialWindow { //这一个是你希望继承自Window的类
public:
    int width() const { return w.width(); }
    int height() const { return w.height(); }
    //  重新实现那些继承而来的虚函数
    virtual void resize(int newWidth, int newHeight);
    virtual void repaint() const;
    ...
private:
    Window w;
};

采用这个策略,每当链接库厂商修改你所依赖的类时,你就必须也修改你自己的类。

34:如何在同一个程序中结合C++和C

以C++组件搭配C组件一起构建程序,类似于以多个C编译程序所产生的object文件组合出整个C程序,需要考虑的事情是一样的。需要编译器在那些“编译器相关的特性”上(如int和double的大小、参数传递机制等)取得一致。C和C++混合使用,就像上述一样,所以在尝试这么做之前,需要确定你的C++和C编译器产生兼容的object文件。

确定这个大前题之后,另有四件事情你需要考虑:name mangling(名称修饰)、statics对象初始化、易失存储器配置、数据结构的兼容性。

1、 name mangling(名称修饰)

C++支持函数重载,而C不支持,因此,C++和C的函数的名称修饰规则是不一样的。C++编译器需要将重载函数的名称修饰为不同的名称,以便连接器能够处理。

比如,在C++的头文件中如果有这么一句声明:void drawLine(int x1, int y1, int x2, int y2),针对该函数的调用就会被C++编译器翻译为一个修饰后的函数名称,所以,当你调用该函数时:drawLine(a,b,c,d),实际上目标文件可能是这样的调用代码:xyzzy(a,b,c,d)。但是,如果drawLine是个C函数,那么包含该函数的目标文件中,C编译器修饰后的函数名是mnoom(a,b,c,d),当链接该目标文件后,连接器企图寻找xyzzy,但找不到这样的函数。

要解决这种问题,需要使用C++的extern “C”指令:

extern "C"
void drawLine(int x1, int y1, int x2, int y2);

extern “C”是一种叙述,表示该函数应该以C的方式来调用。实际上,也可以将C++函数声明为extern “C”,这样声明之后,使用该函数的代码可以以C的方式调用它。

extern “C”也支持下面的形式:

extern "C" {
    void drawLine(int x1, int y1, int x2, int y2);
    void twiddleBits(unsigned char bits);
    void simulate(int iterations);
    ...
}

extern "C"的运用可以简化“必须同时被C和C++ 使用”的头文件的维护工作。当头文件用于C++时,你希望含有extern"C";用于C时,你不希望如此。由于预处理器符号 __cplusplus只针对C++才有定义,所以头文件的架构如下:

#ifdef  __cplusplus
extern "C" {
#endif
    void drawLine(int x1, int y1, int x2, int y2);
    void twiddleBits(unsigned char bits);
    void simulate(int iterations);
    ...
#ifdef  __cplusplus
}
#endif

2、 静态对象的初始化

如果C++库中的代码有静态对象,将其连接至可执行程序(C或C++)时,库中的静态对象的初始化工作也会在main之前进行,销毁也是在main之后。

3、 动态内存分配

C++使用new/delete进行内存分配和释放,而C使用malloc/free,对着一个new返回的指针调用free,会导至未定义的行为,对着一个malloc返回的指针调用delete,情况也一样。

4、 数据结构的兼容性

想要让C函数了解C++的特性是不可能的,所以两个语言之间的对话层次必须限制于C能够接受的范围。因此,没有任何具移植性的作法,可以将对象或是成员函数指针传给C函数。然而由于C了解普通指针、struct等,所以两个语言的函数可以安全地交换对象指针,非成员函数指针,或是static函数指针。struct以及内建类型变量(如int,char等)也可以安全跨越C++/C边界。

由于C++中struct内存布局的规则,与C语言的相关规则一致,所以同一个struct定义在两种语言编译器中被编译出来后,应该有相同的布局。如此的struct可安全地在C++和C之间往返。如果你为C++版的struct加上一些非虚拟函数,其内存布局应该不会改变。所以struct(或class)之中如果只含非虚拟函数,其对象应兼容于C structs(译注:因为C++成员函数并不在对象布局中留下任何蛛丝马迹)。如果加上虚函数,这场游戏就玩不下去了,因为加入虚函数会造成对象采用不同的内存布局;令struct继承另一个struct(或class)通常也会改变其布局;所以一个struct如果带有base structs(或classes),无法和C函数交换。

35:让自己习惯于标准C++语言

我们一般所谓的string类,其实是basic_string<char>。由于其使用极为频繁,标准程序特别提供了一个typedef:typedef basic_string<char> string;

完整的basic_string声明如下:

template<class charT, 
         class traits = string_char_traits<charT>, 
         class Allocator = allocator>
class basic_string;

如果需要自行指定字符串所容纳的字符类型(char, wide char, unicode char或其他),或是想要微调那些字符的行为,或是想要篡夺字符串的内存配置控制权,basic_string template 允许你那么做。

         C++标准程序库中的每一样东西几乎都是template。它的所有成分都位于namespace std中。C++标准程序库中最大的组成分子就是STL:Standard Template Library。STL以3个基本概念为基础:containers,iterators和algorithms。containers持有一系列对象;iterators是一种类似指针的对象,让你可以遍历STL containers,就像以指针来遍历数组一样;algorithms是可作用于STL containers身上的函数,以iterators来协助工作。

猜你喜欢

转载自www.cnblogs.com/gqtcgq/p/9638460.html