- 条款32:在未来时态下发展程序
- 条款33:将非尾端类(non-leaf classes)设计为抽象类 (abstract classes)
- 条款34:如何在同一个程序中结合C++和C
- 条款35:让自己习惯于标准C++语言
条款32:在未来时态下发展程序
- 请避免”demand-apged“式的虚函数,那会使你习惯于”不让任何函数成为virtual,除非有人需要“。应该有自己的判断,决定函数的意义,而不要只为了图某人的方便就改变其定义。请确定所做的改变对于整个class的上下关系乃至它所表现的抽象性是合理的。
- 请为每个class处理assignment和copy construction动作,即使没有人使用那个动作。如果不易完成,请声明为private,那就不会有人因为不经意调用编译器自动产生却行为错误的版本。
- 不要做出令人大吃一惊的怪异行为:努力让classes的操作符和函数拥有自然的语法和直观的语义。请和内置类型的行为保持一致。请让你的classes容易被正确地使用,不容易被误用。
- 请努力写出可移植代码。
- 设计你的代码,使系统改变所带来的冲击得以局部化。尽量采用封装性质,尽可能让实现细目成为private。如果可用,尽量用匿名namespaces或文件内的static对象和static函数。尽量避免设计出virtual base classes,因为这种casses必须被其每个derived classes初始化。请避免以RTTI作为设计基础并因而导致一层层的if-then-else语句,因为灭当class继承体系有改变,每一组这样的语句都得更新,而忘了其中某一项,编译器并不会给出警告。
条款33:将非尾端类(non-leaf classes)设计为抽象类 (abstract classes)
- 有个类继承体系: Animal负责具体化所有东吴的共同特征,Lizard和Chicken是需要特殊对待的两种动物,它们的简化定义像这样:
1
2
3graph BT
Lizard --> A(Animal)
Chicken --> A考虑一段代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class 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);
...
};其中有两个问题。1
2
3
4
5
6Lizard liz1;
Lizard liz2;
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;- 最后一行调用Animal class的assignment操作符,即使对象类型是Lizard。而只有例子
的Animal成分被修改,这就是所谓的部分复制(partial assignments)。在这个动作之后,liz1的Animal members于liz2相同,但是lizard members则没变化。 - 很多人用指针这样写。但复制动作很容易错用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class 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);
...
}; - 最后一行调用Animal class的assignment操作符,即使对象类型是Lizard。而只有例子
- assignment操作符为了返回一个正确的reference类型,代表class,使得在class中为虚函数声明完全相同的参数类型(Animal)。意味着任何种类的Animal对象都可以出现在assignment操作符的右边: 这就是异型赋值。语言的强烈类型检验(strong typing)通常会将它们视为不合法。
1
2
3
4
5
6lizard liz;
Chicken chick;
Animal *pAnimal1 = &liz;
Animal *pAnimal2 = &chick;
...
*pAnimal1 = *pAnimal2; // 将鸡赋值给蜥蜴
这使我们为难。一方面希望通过指针同型赋值,但又禁止通过那样的指针进行异型赋值。要区分这些情况,得在运行期才有办法,dynamic_cast可以协助完成上述愿望:同型类型不需要dynamic_cast,成本太高,通过别的实现方式:1
2
3
4
5
6
7Lizard& Lizard::operator=(const Animal& rhs)
{
// 确定rhs真的是一只蜥蜴
//将rhs转为const Lizard&,失败则抛出bad_cast exception
const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);
// proceed with a normal assignment of rhs_liz to *this;
}运行时期的所有类型检验动作,以及对dynamic_casts的各种运用,都有其固有的缺点:某些编译器不支持dynamic_cast不说,它竟然要求每一次动作都需要待命捕捉bad_cast exceptions。为此,将clients可能在一开始就出现问题的赋值动作扼杀在编译器。阻止这种赋值动作的最简单方法就是,让operator=成为Animal的private函数:1
2
3
4
5
6
7
8
9
10
11
12class Lizard : public Animal {
public:
virtual Lizard& operator=(const Aniaml &rhs);
Lizard& operator=(const Lizard &rhs); // 加上这行
...
};
Lizard& Lizard::oeprator=(const Animal &rhs)
{
// 转型成功,调用正常的assignmen操作符,否则抛出bad_cast exception
return operator=(dynamic_cast<const Lizard&>(rhs));
}为了完成Animal对象间的相互赋值,可把private声明为protected,然而,前一个问题又出现了。因此,最简单的办法就是消除Animal对象之间相互赋值的需要,以下是新的继承体系:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class Animal {
private:
Animal& operator=(const ANimal &rhs);
...
};
class Lizard : public Animal {
public:
Lizard& operator=(const Lizard &rhs);
...
};
class Chicken : public Animal {
public:
Chicken& operaotr=(const Chicken &rhs);
...
};
Lizard liz1, liz2;
...
liz1 = liz2; // good
Chicken chick1, chick2;
...
chick1 = chick2;// good
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &chick1;
...
*pAnimal = *pAnimal2; // error!企图调用private Animal::operaor=1
2
3
4graph BT
Lzard --> A(AbstractAnimal)
Animal --> A
Chicken --> A剩余分析过程实际上体现了这样一种思想:当具体类被当做基类使用时,应该将具体类转变为抽象基类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class AbstractAnimal {
protected:
AbstractAnimal& operator=(const AbstractAnimal &rhs);
public:
virtual ~AbstractAniaml() = 0;
...
};
class Aniaml : public AbstractAnimal {
public:
Animal& operaotr=(const Animal &rhs);
...
};
class Lizard : public AbstractAnimal {
public:
Lizard& operator=(const Lizard &rhs);
...
};
class Chicken : public AbstractAnimal {
public:
Chicken& operaotr=(const Chicken &rhs);
...
};
然而有时候需要使用第三方库,并继承其中一个具体类,由于无法修改该库,也就无法将该具体类转为抽象基类,这是就需要采取其他选择:- 将具体类继承自既存的(程序库)具体类,注意assignment相关问题,和条款3的数组陷阱
- 试着在继承体系中找一个更高层的抽象类,然后继承它
- 以”所希望继承的那么程序库类”来实现新类.例如使用复合或private继承并提供相应接口.此策略不具灵活性
- 为”所希望继承的那么程序库类”定义一些non-member,不再定义新类
条款34:如何在同一个程序中结合C++和C
- C++和C混合使用的前提之一就是编译器产生兼容的目标文件(.lib和.dll等).所谓”兼容”,指的是编译器在”预编译器相依的特性上”一致,如int和double大小,参数压栈机制等,只有在这个基础上才能讨论结合使用C++和C模块的问题
- 有四个事情需要考虑:name mangling(名字重整)、statics(静态对象)初始化、动态内存分配、数据结构的兼容性。
Name Mangling(名字重整)
- Name mangling是C++用于支持函数重载的机制,它对函数名称进行一定的修正,使得每个函数有独一无二的名称,但是C不支持函数重载。当C++企图调用C中的某个函数时,会因为找不到重整的函数名而报错。因此要压抑name mangling,必须使用C++的extern “C”指令。: 技术上说,extern “C”意味着这个函数有C linkage。如果你写了供给其他语言的程序,那么也可以使用extern “C”,只要压抑了C++函数名称的name mangling程序,就可以了。
1
2extern "C"
void drawLine(int x1, int y1, int x2, in y2);
extern “C”也可以施行于整一组函数身上:1
2
3
4
5
6
7// 预处理器符号__cplusplus只针对C++才有定义,可以选择来进行C/C++语言间的判断
extern "C" {
void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
}
Statics的初始化
- 程序的入口实际上并不是main,而是编译器提供的特殊函数(如 mainCRTStartup(void)等,在mainCRTStartup主要任务之一就是全局对象(包括static class对象,全局对象,namespace作用域内的对象以及文件作用域内的对象)的初始化工作,由于C++支持动态初始化(如全局变量int b=a;)而C仅支持静态初始化,因此全局对象的动态初始化涉及到动态初始化就应该在C++中撰写main,而将C main重命名为realMain,然后让C++ main调用realMain,像这样:
1
2
3
4
5
6extern "C" // implement this
int realMain(int argc, char *argv[]); // C程序库的函数,用于完成主函数功能
int main(int argc, char *argv[]) // C++代码
{
return realMain(argc, argv);
}
动态内存分配
- 动态内存分配规则很简单:C++部分使用new和delete,C部分使用malloc(及其变种)和free。new分配就delete删除,malloc分配就free释放。
数据结构的兼容性
C和C++对于struct和内置类型变量是兼容的,因为C/C++对于struct的内存布局相同,但如果C++为struct加上非虚成员函数,由于struct内存布局不改变,其对象仍兼容于C,但如果加上虚函数,由于vtbl和vptr的存在,struct内存布局便发生改变,也就不再兼容于C。
也就是说,在C和C++之间对数据结构做双向交流是安全的——前提是结构的定义式在C和C++中都可编译;如果为C++ struct加上非虚函数,虽然不再兼容于C,但可能不影响兼容性;其他改变如虚函数和继承等则会产生影响。
条款35:让自己习惯于标准C++语言
- 在从1990年发行之后,The Annotated C++ Reference Manual(ARM)成了程序员的参考凭借。直到ISO/ANSI委员会对这个语言的标准化工作,最后这个参考的标准,ARM不再适合。
- C++ ISO/ANSI标准规格,是编译器厂商的咨询对象,也是人们遇到问题与纠纷时的手边依据和最后仲裁。ARM出版后的这些年里,C++最重要几项改变如下:
- 增加了一些新的语言特性:RTTI、namespaces、bool、关键字mutable和explicit、enums作为重载函数参数所引发的类型晋升转换,以及在class定义区内直接为整数型const static class members设定初值的能力。
- 扩充了Templates的弹性:允许member tempates存在、接纳”明白指示template当场实例化“的标准语法、允许function templates接受非类型自变量(non-type arguments)、可用class templates作为其他template的自变量。
- 强化了异常处理机制(Exception handing):编译期更严密检验exception specifications、允许unexpected函数抛出bad-exception对象。
- 修改了内存分配例程:加入operator new[]和operator delete[],内存未能分配成功时由operators new/new[]抛出一个exception,提供一个operators new/new[]新版本,在内存分配失败时返回0.
- 增加了新的转型形式:static_cast,dynamic_cast,const_cast和reinterpret_cast。
- 语言规则更为优雅精炼:重新定义虚函数时返回类型不再一定得与原定义完全符合,临时对象的寿命也有了明确规范。
- 描述STL之前,有两个C++标准程序库特质需要知道:一是标准程序库中的每样东西几乎都是template;二是所有成分都位于namespace std内。
Standard Template Library(STL)
- STL不难理解,它以3个基本概念为基础:containers,iterators和algorithms。Containers持有一系列对象。Iterators是一种类似指针的对象,用以遍历STL containers,Algorithms可用于STL containers身上的函数,以iterators来协助工作。
- STL是可扩充的,只要遵循STL的标准,可以将自己的容器,迭代器,算法等结合STL使用。(要使自定义的迭代器适用于STL的泛型算法,需要了解C++的traits技法,见Effective C++ 条款47)