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() = ;
...
}; 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来协助工作。

More Effective C++: 06杂项讨论的更多相关文章

  1. Effective C++ —— 杂项讨论(九)

    条款53 : 不要轻忽编译器的警告 请记住: 1. 严肃对待编译器发出的警告信息.努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉. 2. 不要过度倚赖编译器的报警能力,因为不同的编 ...

  2. Effective C++: 06继承与面向对象设计

    32:确定你的public继承塑模出is-a关系 以C++进行面向对象编程,最重要的一个规则是:public继承表示的是"is-a"(是一种)的关系. 如果令class D以pub ...

  3. Effective Java 06 Eliminate obsolete object references

    NOTE Nulling out object references should be the exception rather than the norm. Another common sour ...

  4. Effective C++ .06 阻止编译器自动生成函数以及被他人调用

    这节讲了下如何防止对象拷贝(隐藏并不能被其他人调用) 两种方法: 1. 将拷贝构造函数声明为private 并且声明函数但不进行定义 #include <iostream> #includ ...

  5. 《Effective C++》读书摘要

    http://www.cnblogs.com/fanzhidongyzby/archive/2012/11/18/2775603.html 1.让自己习惯C++ 条款01:视C++为一个语言联邦 条款 ...

  6. C++学习书籍推荐《Effective C++ 第三版》下载

    百度云及其他网盘下载地址:点我 编辑推荐 <Effective C++:改善程序与设计的55个具体做法(第3版)(中文版)(双色)>前两个版本抓住了全世界无数程序员的目光.原因十分明显:S ...

  7. Effective C++读书笔记(转)

    第一部分 让自己习惯C++ 条款01:视C++为一个语言联邦 一.要点 ■ c++高效编程守则视状况而变化,取决于你使用c++的哪一部分. 二.扩展 将c++视为一个由相关语言组成的联邦而非单一语言会 ...

  8. Effective C++学习记录

    Effective C++算是看完了,但是并没有完全理解,也做不到记住所有,在此记录下55个条款及条款末的"请记住". 让自己习惯C++ 条款01:视C++为一个语言联邦 ① C ...

  9. 读书笔记——Effective C++

    1.让自己习惯C++ 条款01:视C++为一个语言联邦 C++高效编程守则视状况而变化,取决于你使用C++的哪一部分. 条款02:尽量以const.enum.inline替换 #define 对于单纯 ...

随机推荐

  1. 【agc019f】AtCoder Grand Contest 019 F - Yes or No

    题意 有n个问题答案为YES,m个问题答案为NO. 你只知道剩下的问题的答案分布情况. 问回答完N+M个问题,最优策略下的期望正确数. 解法 首先确定最优策略, 对于\(n<m\)的情况,肯定回 ...

  2. linux命令统计文件中某个字符串出现的次数

    1.使用grep linux grep命令在我的随笔linux分类里有过简单的介绍,这里就只简单的介绍下使用grep命令统计某个文件这某个字符串出现的次数,首先介绍grep命令的几个参数,详细参数请自 ...

  3. 通过java反射实现的excel数据导出

    Excel.java @SuppressWarnings("deprecation") public static <T> void ExportExcel(Strin ...

  4. 卸载VS2015之后,安装VS2017出错

    安装出现问题. 可通过以下方式排查包故障问题: 1. 使用以下搜索 URL 来搜索针对每个包故障的解决方案 2. 针对受与影响的工作负荷或组件修改选项,然后重新尝试安装 3. 从计算机上删除产品,然后 ...

  5. Django项目:CRM(客户关系管理系统)--30--22PerfectCRM实现King_admin数据添加

    登陆密码设置参考 http://www.cnblogs.com/ujq3/p/8553784.html # king_urls.py # ————————02PerfectCRM创建ADMIN页面—— ...

  6. js构造函数+原型

    注:普通对象与函数对象 var o1 = {}; var o2 =new Object(); var o3 = new f1(); function f1(){}; var f2 = function ...

  7. GIT生成公钥和私钥

    转载至:https://blog.csdn.net/gwz1196281550/article/details/80268200 打开 git bash! git config --global us ...

  8. 学习Python笔记---操作列表

    1.for循环: 编写for循环时,对于用语存储列表中每个值的临时变量,可指定任何名称. 在for循环中,想包含多少行代码都可以,每个缩进的代码行都是循环的一部分,且将针对列表中的每个值都执行一次. ...

  9. shell linux基本命令实例、笔记

    1. 在当前文件夹下.查找20分钟内,被訪问过的文件, 并将文件的详情显示出来: find ./ -name '*.log' -mmin -20 -exec ls -l {} \;   当然,须要指出 ...

  10. jsp中生成的验证码和存在session里面的验证码不一致的处理

    今天在调试项目的时候发现,在提交表单的时候的验证码有问题,问题是这样的:就是通过debug模式查看得知:jsp页面生成的验证码和表单输入的页面输入的一样,但是到后台执行的时候,你会发现他们是不一样的, ...