### 类的继承

继承是面向对象的主要特征（此外还有封装和多态）之一，它使得一个类可以从现有类中派生，而不必重新定义一个新类。例如，定义一个员工类，它包含员工 ID、员工姓名、所属部门等信息，再定义一个操作员类，通常，操作员属于公司的员工，因此它也包含员工 ID、员工姓名、所属部门等信息，此外还包含密码信息、登录方法等。如果当前已经定义了员工类，在定义操作员类时可以将其从员工类派生一个新的员工类，向其中添加密码信息，登录方法等就可以了，不必重新定义员工 ID、员工姓名等信息了因为它已经继承了员工类的信息。

例如，下面的代码演示了操作员类是如何继承员工类的。

#define MAXLEN 128 //定义一个宏

class CEmployee //定义员工类

{

public:

int m_ID; //定义员工 ID

char m_Name[MAXLEN]; //定义员工姓名

char m_Depart[MAXLEN]; //定义所属部门

CEmployee() //定义默认构造函数

{

memset(m_Name,0,MAXLEN); //初始化 m_Name

memset(m_Depart,0,MAXLEN); //初始化 m_Depart

printf("员工类构造函数被调用\n"); //输出信息

}

void OutputName() //定义共有方法

{

printf("员工姓名: %s\n",m_Name); //输出员工姓名

}

};

class COperator :public CEmployee //定义一个操作员类，从 CEmployee 类派生而来

{

public:

char m_Password[MAXLEN]; //定义密码

bool Login() //定义登录方法

{

if (strcmp(m_Name,"MR")==0 && //比较用户名

strcmp(m_Password,"KJ")==0) //比较密码

{

printf("登录成功!\n"); //输出信息

return true; //设置返回值

}

else

{

printf("登录失败!\n"); //输出信息

return false; //设置返回值

}

}

};

上述代码在定义 COperator 类时使用了:运算符，表示该类派生于一个基类，public 关键字表示派生的类型为公有型，其后的 CEmployee 表示 COperator 类的基类，也就是父类。这样，COperator 类将继承 CEmployee 类的所有非私有成员（private 类型成员不能被继承）。

当一个类从另一个类继承时，可以有 3 种派生类型，分别为公有型（public）、保护型（protected）和私有型（private）。公有型派生表示对于基类中的 public 数据成员和方法，在派生类中仍然是 public，对于基类中的 protected 数据成员和方法，在派生类中仍然是 protected。保护型派生表示对于基类中的 public、protected 数据成员和方法，在派生类中均为 protected。私有型派生表示对于基类中的 public、protected 数据成员和方法，在派生类中均为 private。

下面定义一个操作员对象，演示通过操作员对象调用操作员类的方法以及调用基类员工类的方法。

int main(int argc, char* argv[])

{

COperator optr; //定义一个 COperator 类对象

strcpy(optr.m_Name,"MR"); //访问基类的 m_Name 成员

strcpy(optr.m_Password,"KJ"); //访问 m_Password 成员

optr.Login(); //调用 COperator 类的 Login 方法

optr.OutputName(); //调用基类 CEmployee 的 OutputName 方法

return 0;

}

执行上述代码，效果如图 1 所示。

图 1 访问父类方法

用户在父类中派生子类时，可能存在一种情况，即在子类中定义了一个与父类的同名的方法，此时，称为子类隐藏了父类的方法。例如，重新定义 COperator 类，添加一个 OutputName 方法。

示例，子类隐藏了父类的方法。

class COperator :public CEmployee //定义 COperator 类

{

public:

char m_Password[MAXLEN]; //定义数据成员

void OutputName() //定义 OutputName 方法

{

printf("操作员姓名: %s\n",m_Name); //输出操作员姓名

}

bool Login() //添加成员函数

{

if (strcmp(m_Name,"MR")==0 && //比较用户名

strcmp(m_Password,"KJ")==0) //比较密码

{

printf("登录成功!\n"); //输出信息

return true; //返回结果

}

else

{

printf("登录失败!\n"); //输出信息

return false; //返回结果

}

}

};

定义一个 COperator 类对象，调用 OutputName 方法。

int main(int argc, char* argv[]) //主方法

{

COperator optr; //定义 COperator 对象

strcpy(optr.m_Name,"MR"); //设置 m_Name 数据成员

optr.OutputName(); //调用 COperator 类的 OutputName 方法

return 0;

}

执行上述代码，效果如图 2 所示。

图 2 隐藏基类方法

从图 2 中可以发现，语句“optr.OutputName();”调用的是 COperator 类的 OutputName 方法，而不是 CEmployee 类的 OutputName 方法。如果用户想要访问父类的 OutputName 方法，需要显式使用父类名。例如：

COperator optr; //定义一个 COperator 类

strcpy(optr.m_Name,"MR"); //赋值字符串

optr.OutputName(); //调用 COperator 类的 OutputName 方法

optr.CEmployee::OutputName(); //调用 CEmployee 类的 OutputName 方法

如果子类中隐藏了父类的方法，则父类中所有同名的方法（重载方法）均被隐藏。因此下面黑体部分代码访问是错误的。

例如，如果子类中隐藏了父类的方法，则父类中所有同名的方法均被隐藏。其代码如下：

#define MAXLEN 128 //定义一个宏

class CEmployee //定义 CEmployee 类

{

public:

int m_ID; //定义数据成员

char m_Name[MAXLEN]; //定义数据成员

char m_Depart[MAXLEN]; //定义数据成员

CEmployee()

{

memset(m_Name,0,MAXLEN); //初始化数据成员

memset(m_Depart,0,MAXLEN); //初始化数据成员

printf("员工类构造函数被调用\n"); //输出信息

}

void OutputName() //定义重载方法

{

printf("员工姓名: %s\n",m_Name); //输出信息

}

void OutputName(const char* pchData) //定义重载方法

{

if (pchData != NULL) //判断参数是否为空

{

strcpy(m_Name,pchData); //复制字符串

printf("设置并输出员工姓名: %s\n",pchData); //输出信息

}

}

};

class COperator :public CEmployee //定义 COperator 类

{

public:

char m_Password[MAXLEN]; //定义数据成员

void OutputName() //定义 OutputName 方法，隐藏基类的方法

{

printf("操作员姓名: %s\n",m_Name); //输出信息

}

bool Login() //定义 Login 方法

{

if (strcmp(m_Name,"MR")==0 && //比较用户名称

strcmp(m_Password,"KJ")==0) //比较用户密码

{

printf("登录成功!\n"); //输出信息

return true; //设置返回值

}

else

{

printf("登录失败!\n"); //输出信息

return false; //设置返回值

}

}

};

int main(int argc, char* argv[])

{

COperator optr; //定义 COperator 类对象

optr.OutputName("MR"); //错误的代码，不能访问基类的重载方法

return 0;

}

上述代码中在 CEmployee 类中定义了重载的 OutputName 方法，而在 COperator 类中又定义了一个 OutputName 方法，导致父类中的所有同名方法被隐藏。语句“optr.OutputName("MR");”是错误的。如果用户想要访问被隐藏的父类方法，依然需要指定父类名称。例如：

COperator optr; //定义一个 COperator 对象

optr.CEmployee::OutputName("MR"); //调用基类中被隐藏的方法

在派生完一个子类后，可以定义一个父类的类型指针，通过子类的构造函数为其创建对象。例如：

CEmployee *pWorker = new COperator (); //定义 CEmployee 类型指针，调用子类构造函数

如果使用 pWorder 对象调用 OutputName 方法，例如执行“pWorker->OutputName();”语句，执行的是 CEmployee 类的 OutputName 方法还是 COperator 类的 OutputName 方法呢？答案是调用 CEmployee 类的 OutputName 方法。编译器对 OutputName 方法进行的是静态绑定，即根据对象定义时的类型来确定调用哪个类的方法。由于 pWorker 属于 CEmployee 类型，因此调用的是 CEmployee 类的 OutputName 方法。那么是否有方法将“pWorker->OutputName();”语句改为执行 COperator 类的 OutputName 方法呢？答案是通过定义虚方法可以实现这一点。

在定义方法（成员函数）时，在方法的前面使用 virtual 关键字，该方法即为虚方法。使用虚方法可以实现类的动态绑定，即根据对象运行时的类型来确定调用哪个类的方法，而不是根据对象定义时的类型来确定调用哪个类的方法。

下面的代码修改了 CEmployee 类的 OutputName 方法，使其变为虚方法。

例如，利用虚方法实现动态绑定。

#define MAXLEN 128 //定义一个宏

class CEmployee //定义 CEmployee 类

{

public:

int m_ID; //定义数据成员

char m_Name[MAXLEN]; //定义数据成员

char m_Depart[MAXLEN]; //定义数据成员

CEmployee() //定义构造函数

{

memset(m_Name,0,MAXLEN); //初始化数据成员

memset(m_Depart,0,MAXLEN); //初始化数据成员

}

virtual void OutputName() //定义一个虚方法

{

printf("员工姓名: %s\n",m_Name); //输出信息

}

};

class COperator :public CEmployee //从 CEmployee 类派生一个子类

{

public:

char m_Password[MAXLEN]; //定义数据成员

void OutputName() //定义 OutputName 虚方法

{

printf("操作员姓名: %s\n",m_Name); //输出信息

}

};

上述代码中，在 CEmployee 类中定义了一个虚方法 OutputName，在子类 COperator 类中改写了 OutputName 方法，其中 COperator 类中的 OutputName 方法仍为虚方法，即使没有使用 virtual 关键字。下面定义一个 CEmployee 类型的指针，调用 COperator 类的构造函数构造对象。

int main(int argc, char* argv[])

{

CEmployee *pWorker = new COperator(); //定义 CEmployee 类型指针，调用 COperator 类构造函数

strcpy(pWorker->m_Name,"MR"); //设置 m_Name 数据成员信息

pWorker->OutputName(); //调用 COperator 类的 OutputName 方法

delete pWorker; //释放对象

return 0;

}

执行上述代码，效果如图 3 所示。

图 3 虚方法

从图 3 中可以发现，“pWorker->OutputName();”语句调用的是 COperator 类的 OutputName 方法。

在 C++语言中，除了能够定义虚方法之外，还可以定义纯虚方法，也就是通常所说的抽象方法。一个包含纯虚方法的类被称为抽象类，抽象类是不能够被实例化的，通常用于实现接口的定义。下面的代码演示了纯虚方法的定义。

#define MAXLEN 128 //定义一个宏

class CEmployee //定义 CEmployee 类

{

public:

int m_ID; //定义数据成员

char m_Name[MAXLEN]; //定义数据成员

char m_Depart[MAXLEN]; //定义数据成员

virtual void OutputName() = 0; //定义抽象方法

};

上述代码中为 CEmployeel 类定义了一个纯虚方法 OutputName。纯虚方法的定义是在虚方法定义的基础上在末尾添加“= 0”。对于包含纯虚方法的类来说，是不能够实例化的，因此下面的语句是错误的。

CEmployee Worker; //错误的代码，不能实例化抽象类

抽象类通常用于作为其他类的父类，从抽象类派生的子类如果抽象类，则子类必须实现父类中的所有纯虚函数。

例如，实现抽象类中的方法。

class COperator :public CEmployee //定义 COperator 类，派生于 CEmployee 类

{

public:

char m_Password[MAXLEN]; //定义数据成员

void OutputName() //实现父类中的纯虚方法

{

printf("操作员姓名: %s\n",m_Name); //输出信息

}

COperator() //定义 COperator 类的默认构造函数

{

strcpy(m_Name,"MR"); //设置数据成员 m_Name 信息

}

};

class CSystemManager :public CEmployee //定义 CSystemManager 类

{

public:

char m_Password[MAXLEN]; //定义数据成员

void OutputName() //实现父类中的纯虚方法

{

printf("系统管理员姓名: %s\n",m_Name); //输出信息

}

CSystemManager() //定义 CSystemManager 类的默认构造函数

{

strcpy(m_Name,"SK"); //设置数据成员 m_Name 信息

}

};

上述代码从 CEmployee 类派生了两个子类，分别为 COperator 和 CSystemManager。这两个类分别实现了父类的纯虚方法 OutputName。下面编写一段代码演示抽象类的应用。

int main(int argc, char* argv[]) //主函数

{

CEmployee *pWorker; //定义 CEmployee 类型指针对象

pWorker = new COperator(); //调用 COperator 类的构造函数为 pWorker 赋值

pWorker->OutputName(); //调用 COperator 类的 OutputName 方法

delete pWorker; //释放 pWorker 对象

pWorker = NULL; //将 pWorker 对象设置为空

pWorker = new CSystemManager(); //调用 CSystemManager 类的构造函数与为 pWorker 赋值

pWorker->OutputName(); //调用 CSystemManager 类的 OutputName 方法

delete pWorker; //释放 pWorker 对象

pWorker = NULL; //将 pWorker 对象设置为空

return 0;

}

执行上述代码，效果如图 4 所示。

图 4 纯虚方法

从图 4 中可以发现，同样的一条语句“pWorker->OutputName();”，由于 pWorker 指向的对象不同，其行为也不同。

下面分析一下子类对象的创建和释放过程。当从父类派生一个子类后，定义一个子类的对象时，它将依次调用父类的构造函数、当前类的构造函数来创建对象。在释放子类对象时，先调用的是当前类的析构函数，然后是父类的析构函数。下面的代码说明了这一点。

#define MAXLEN 128 //定义一个宏

class CEmployee //定义 CEmployee 类

{

public:

int m_ID; //定义数据成员

char m_Name[MAXLEN]; //定义数据成员

char m_Depart[MAXLEN]; //定义数据成员

CEmployee() //定义构造函数

{

printf("CEmployee 类构造函数被调用\n"); //输出信息

}

~CEmployee() //析构函数

{

printf("CEmployee 类析构函数被调用\n"); //输出信息

}

};

class COperator :public CEmployee //从 CEmployee 类派生一个子类

{

public:

char m_Password[MAXLEN]; //定义数据成员

COperator() //定义构造函数

{

strcpy(m_Name,"MR"); //设置数据成员

printf("COperator 类构造函数被调用\n"); //输出信息

}

~COperator() //析构函数

{

printf("COperator 类析构函数被调用\n"); //输出信息

}

};

int main(int argc, char* argv[]) //主函数

{

COperator optr; //定义一个 COperator 对象

return 0;

}

执行上述代码，效果如图 5 所示。

图 5 构造函数调用顺序

从图 5 中可以发现，在定义 COperator 类对象时，调用的是父类 CEmployee 的构造函数，然后是 COperator 类的构造函数。子类对象的释放过程则与其构造过程恰恰相反，先调用自身的析构函数，然后再调用父类的析构函数。

在分析完对象的构建、释放过程，考虑这样一种情况，定义一个基类类型的指针，调用子类的构造函数为其构建对象，当对象释放时，将调用父类的析构函数还是先调用子类的析构函数，再调用父类的析构函数呢？答案是如果析构函数是虚函数，则先调用子类的析构函数，然后再调用父类的析构函数，如果析构函数不是虚函数，则只调用父类的析构函数。可以想象，如果在子类中为某个数据成员在堆中分配了空间，父类中的析构函数不是虚方法，上述情况将使子类的析构函数不会被调用，其结果是对象不能被正确地释放，导致内存泄露的产生。因此，在编写类的析构函数时，析构函数通常是虚函数。

前面所介绍的子类的继承方式属于单继承，即子类只从一个父类继承公有的和受保护的成员。与其他面向对象语句不同，C++语言允许子类从多个父类继承公有的和受保护的成员，这被称之为多继承。例如，鸟能够在天空飞翔，鱼能够在水里游，而水鸟即能够在天空飞翔，又能够在水里游。那么在定义水鸟类时，可以将鸟和鱼同时作为其基类。

例如，下面的代码演示了多继承的应用。其代码如下：

class CBird //定义鸟类

{

public:

void FlyInSky() //定义成员函数

{

printf("鸟能够在天空飞翔!\n"); //输出信息

}

void Breath() //定义成员函数

{

printf("鸟能够呼吸!\n"); //输出信息

}

};

class CFish //定义鱼类

{

public:

void SwimInWater() //定义成员函数

{

printf("鱼能够在水里游!\n"); //输出信息

}

void Breath() //定义成员函数

{

printf("鱼能够呼吸!\n"); //输出信息

}

};

class CWaterBird: public CBird, public CFish //定义水鸟，从鸟和鱼类派生

{

public:

void Action() //定义成员函数

{

printf("水鸟即能飞又能游!\n"); //输出信息

}

};

int main(int argc, char* argv[]) //主函数

{

CWaterBird waterbird; //定义水鸟对象

waterbird.FlyInSky(); //调用从鸟类继承而来的 FlyInSky 方法

waterbird.SwimInWater(); //调用从鱼类继承而来的 SwimInWater 方法

return 0;

}

执行上述代码，效果如图 6 所示。

图 6 多继承

上述代码定义了鸟类 CBird，定义了鱼类 CFish、然后从鸟类和鱼类派生了一个子类水鸟类 CWaterBird。水鸟类自然继承了鸟类和鱼类的所有共有和受保护的成员。因此 CWaterBird 类对象能够调用 FlyInSky 和 SwimInWater 方法。在 CBird 类中提供了一个 Breath 方法，在 CFish 类中同样提供了 Breath 方法，如果 CWaterBird 类对象调用 Breath 方法，将会执行哪个类的 Breath 方法呢？答案是将会出现编译错误，编译器将产生歧义，不知道具体调用哪个类的 Breath 方法。为了让 CWaterBird 类对象能够访问 Breath 方法，需要在 Breath 方法前具体指定类名。例如：

waterbird.CFish::Breath(); //调用 CFish 类的 Breath 方法

waterbird.CBird::Breath(); //调用 CBird 类的 Breath 方法

在多继承中存在这样一种情况，假如 CBird 类和 CFish 类均派生于同一个父类，例如 CAnimal 类。那么当从 CBird 类和 CFish 类派生子类 CWaterBird 时，在 CWaterBird 类中将存在两个 CAnimal 类的拷贝。能否在派生 CWaterBird 类时，使其只存在一个 CAnimal 基类呢？为了解决该问题，C++语言提供了虚继承的机制。下面的代码演示了虚继承的使用。

class CAnimal //定义一个动物类

{

public:

CAnimal() //定义构造函数

{

printf("动物类被构造!\n"); //输出信息

}

void Move() //定义成员函数

{

printf("动物能够移动!\n"); //输出信息

}

};

class CBird : virtual public CAnimal //从 CAnimal 类虚继承 CBird 类

{

public:

CBird() //定义构造函数

{

printf("鸟类被构造!\n"); //输出信息

}

void FlyInSky() //定义成员函数

{

printf("鸟能够在天空飞翔!\n"); //输出信息

}

void Breath() //定义成员函数

{

printf("鸟能够呼吸!\n"); //输出信息

}

};

class CFish: virtual public CAnimal //从 CAnimal 类虚继承 CFish

{

public:

CFish() //定义构造函数

{

printf("鱼类被构造!\n"); //输出信息

}

void SwimInWater() //定义成员函数

{

printf("鱼能够在水里游!\n"); //输出信息

}

void Breath() //定义成员函数

{

printf("鱼能够呼吸!\n"); //输出信息

}

};

class CWaterBird: public CBird, public CFish //从 CBird 和 CFish 类派生子类 CWaterBird

{

public:

CWaterBird() //定义构造函数

{

printf("水鸟类被构造!\n"); //输出信息

}

void Action() //定义成员函数

{

printf("水鸟即能飞又能游!\n"); //输出信息

}

};

int main(int argc, char* argv[]) //主函数

{

CWaterBird waterbird; //定义水鸟对象

return 0;

}

执行上述代码，效果如图 7 所示。

图 7 虚继承

上述代码在定义 CBird 类和 CFish 类时使用了关键字 virtual 从基类 CAnimal 派生而来。实际上，虚继承对于 CBird 类和 CFish 类没有多少影响，但是却对 CWaterBird 类产生了很大影响。CWaterBird 类中不再有两个 CAnimal 类的拷贝，而只存在一个 CAnimal 的拷贝，图 7 充分说明了这一点。

通常，在定义一个对象时，先依次调用基类的构造函数，最后才调用自身的构造函数。但是对于虚继承来说，情况有些不同。在定义 CWaterBird 类对象时，先调用基类 CAnimal 的构造函数，然后调用 CBird 类的构造函数，这里 CBird 类虽然为 CAnimal 的子类，但是在调用 CBird 类的构造函数时将不再调用 CAnimal 类的构造函数了，此操作被忽略了，对于 CFish 类也是同样的道理。

说明：在程序开发过程中，多继承虽然带来了很多方便，但是很少有人愿意使用它，因为多继承会带来很多复杂的问题，并且多继承能够完成的功能，通过单继承同样可以实现。如今流行的C#、Delphi、Java等面向对象语言只采用了单继承，而没有提供多继承的功能是经过设计者充分考虑的。因此，在开发应用程序时，如果能够使用单继承实现，尽量不要使用多继承。