高质量C++教程 — 第10章 类的继承与组合

高质量C++教程 -- 第10章 类的继承与组合
来源:www.vcworld.net 

对象(Object)是类(Class)的一个实例(Instance)。如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。

对于C++程序而言,设计孤立的类是比较容易的,难的是正确设计基类及其派生类。本章仅仅论述“继承”(Inheritance)和“组合”(Composition)的概念。

注意,当前面向对象技术的应用热点是COM和CORBA,这些内容超出了C++教材的范畴,请阅读COM和CORBA相关论著。

10.1 继承

如果A是基类,B是A的派生类,那么B将继承A的数据和函数。例如:

class A

{

public:

void Func1(void);

void Func2(void);

};

 

class B : public A

{

public:

void Func3(void);

void Func4(void);

};

 

main()

{

B b;

b.Func1(); // B从A继承了函数Func1

b.Func2(); // B从A继承了函数Func2

b.Func3();

b.Func4();

}

 

这个简单的示例程序说明了一个事实:C++的“继承”特性可以提高程序的可复用性。正因为“继承”太有用、太容易用,才要防止乱用“继承”。我们应当给“继承”立一些使用规则。

 

【规则10-1-1】如果类A和类B毫不相关,不可以为了使B的功能更多些而让B继承A的功能和属性。不要觉得“白吃白不吃”,让一个好端端的健壮青年无缘无故地吃人参补身体。

【规则10-1-2】若在逻辑上B是A的“一种”(a kind of
),则允许B继承A的功能和属性。例如男人(Man)是人(Human)的一种,男孩(Boy)是男人的一种。那么类Man可以从类Human派生,类Boy可以从类Man派生。

class Human

{

};

class Man : public Human

{

};

class Boy : public Man

{

};

 

注意事项

【规则10-1-2】看起来很简单,但是实际应用时可能会有意外,继承的概念在程序世界与现实世界并不完全相同。

例如从生物学角度讲,鸵鸟(Ostrich)是鸟(Bird)的一种,按理说类Ostrich应该可以从类Bird派生。但是鸵鸟不能飞,那么Ostrich::Fly是什么东西?

class Bird

{

public:

virtual void Fly(void);

};

 

class Ostrich : public Bird

{

};

 

例如从数学角度讲,圆(Circle)是一种特殊的椭圆(Ellipse),按理说类Circle应该可以从类Ellipse派生。但是椭圆有长轴和短轴,如果圆继承了椭圆的长轴和短轴,岂非画蛇添足?

所以更加严格的继承规则应当是:若在逻辑上B是A的“一种”,并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性。

 

10.2 组合

【规则10-2-1】若在逻辑上A是B的“一部分”(a part
of),则不允许B从A派生,而是要用A和其它东西组合出B。

例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head应该由类Eye、Nose、Mouth、Ear组合而成,不是派生而成。如示例10-2-1所示。

class Eye

{
public:

void Look(void);

};

class Nose

{
public:

void Smell(void);

};

class Mouth

{
public:

void Eat(void);

};

class Ear

{
public:

void Listen(void);

};

// 正确的设计,虽然代码冗长。

class Head

{

public:

void Look(void) { m_eye.Look(); }

void Smell(void) { m_nose.Smell(); }

void Eat(void) { m_mouth.Eat(); }

void Listen(void) { m_ear.Listen(); }

private:

Eye m_eye;

Nose m_nose;

Mouth m_mouth;

Ear m_ear;

};

示例10-2-1 Head由Eye、Nose、Mouth、Ear组合而成

 

如果允许Head从Eye、Nose、Mouth、Ear派生而成,那么Head将自动具有Look、
Smell、Eat、Listen这些功能。示例10-2-2十分简短并且运行正确,但是这种设计方法却是不对的。

// 功能正确并且代码简洁,但是设计方法不对。

class Head : public Eye, public Nose, public Mouth, public
Ear

{

};

示例10-2-2 Head从Eye、Nose、Mouth、Ear派生而成

 

一只公鸡使劲地追打一只刚下了蛋的母鸡,你知道为什么吗?

因为母鸡下了鸭蛋。

很多程序员经不起“继承”的诱惑而犯下设计错误。“运行正确”的程序不见得是高质量的程序,此处就是一个例证。

高质量C++教程 — 第1章 文件结构

高质量C++教程 -- 第1章 文件结构
来源:www.vcworld.net 

每个C++/C程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。

C++/C程序的头文件以“.h”为后缀,C程序的定义文件以“.c”为后缀,C++程序的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。

 

1.1 版权和版本的声明

版权和版本的声明位于头文件和定义文件的开头(参见示例1-1),主要内容有:

(1)版权信息。

(2)文件名称,标识符,摘要。

(3)当前版本号,作者/修改者,完成日期。

(4)版本历史信息。

 

示例1-1 版权和版本的声明

 

1.2 头文件的结构

头文件由三部分内容组成:

(1)头文件开头处的版权和版本声明(参见示例1-1)。

(2)预处理块。

(3)函数和类结构声明等。

假设头文件名称为 graphics.h,头文件的结构参见示例1-2。

 

●【规则1-2-1】为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。

●【规则1-2-2】用 #include 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。

●【规则1-2-3】用 #include “filename.h”
格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。

●【建议1-2-1】头文件中只存放“声明”而不存放“定义”

在C++
语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。

●【建议1-2-2】不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。

// 版权和版本声明见示例1-1,此处省略。

 

#ifndef GRAPHICS_H // 防止graphics.h被重复引用

#define GRAPHICS_H

 

#include // 引用标准库的头文件

#include “myheader.h” // 引用非标准库的头文件

void Function1(…); // 全局函数声明

class Box // 类结构声明

{

};

#endif

示例1-2 C++/C头文件的结构

 

1.3 定义文件的结构

定义文件有三部分内容:

(1) 定义文件开头处的版权和版本声明(参见示例1-1)。

(2) 对一些头文件的引用。

(3) 程序的实现体(包括数据和代码)。

假设定义文件的名称为 graphics.cpp,定义文件的结构参见示例1-3。

// 版权和版本声明见示例1-1,此处省略。

 

#include “graphics.h” // 引用头文件

 

// 全局函数的实现体

void Function1(…)

{

}

 

// 类成员函数的实现体

void Box::Draw(…)

{

}

示例1-3 C++/C定义文件的结构

 

1.4 头文件的作用

早期的编程语言如Basic、Fortran没有头文件的概念,C++/C语言的初学者虽然会用使用头文件,但常常不明其理。这里对头文件的作用略作解释:

(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。

(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

 

1.5 目录结构

如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护。

例如可将头文件保存于include目录,将定义文件保存于source目录(可以是多级目录)。

如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。