目录
一、继承特性
二、派生类的定义格式
三、派生类的继承方式
3.1 三种继承方式概述
3.2 接口继承和实现继承
四、派生类的构造和析构函数
五、类的成员重定义
六、多重继承
6.1 多继承
6.2 多继承中的二义性问题
七、虚基类
八、总结
一、继承特性
在生物学中,遗传(或称继承)是指所有生物从其祖先那里继承特定特征的过程。这一概念同样适用于设计复杂系统,它提供了一种将系统组件组织成继承结构的方法,以便于系统描述,并且是实现代码重用的强大工具。
在C++中,代码重用是一个核心特性。与C语言通过复制和修改代码来重用代码的低级方式不同,C++提供了继承和组合两种高级方式来实现代码重用。通过继承或组合现有类来创建新类,而不是从头开始编写。通常,这些类已经过充分测试,因此在继承时,潜在的错误仅可能出现在新增的代码部分。
组合重用涉及将一个类作为另一个类的对象成员,通过委托成员对象来实现功能。另一种方式是继承。
继承的优势包括:
-
继承允许使用已存在的类来创建新类,新类继承了原有类的所有属性和操作,并可以在其基础上进行修改和扩展。
-
新类被称为派生类或子类,而原有类被称为基类或父类。
-
派生类是基类的具体化,通常情况下,派生类的表示范围比基类要小得多。
比如教师是一个人,人不一定是一个教师,派生类范围小且具体,基类范围大更抽象。
C++的继承支持单继承和多继承
单继承:派生类只有一个直接基类的继承方式
多继承:派生类有多个直接基类的继承方式
二、派生类的定义格式
单继承的基本格式如下:
class<派生类名>:<继承方式><基类名>
{
//<派生类新成员的定义>
}
其中,派生类名就是派生类的名字,并且派生类是按指定的继承方式派生的。继承方式有:
A. public 公有继承
B.private 私有继承
C.protected 保护继承
代码示例:
class CClassA{
public:
CClassA(int nNum) {
m_nNumA = nNum;
}
void print() {
cout << m_nNumA << endl;
}
private:
int m_nNumA;
};
class CClassB :public CClassA {
public:
void print() {
cout << m_nNumB << endl;
}
private:
int m_nNumB;
};
多继承的定义方式和单继承差不多。
class<派生类名>:<继承方式1)<基类名1〉,<继承方式2><基类名2>,……
{
//<派生类新成员定义>
};
多继承派生类有多个基类,基类名之间用逗号分隔,每个基类名前都应有一个该基类的继承方式说明,假如不写继承方式的话,默认的继承方式为私有继承。
三、派生类的继承方式
3.1 三种继承方式概述
类的继承方式主要包括三种:
-
公有继承(public):在这种方式下,基类的每个成员在派生类中保持其原有的访问权限不变。
-
私有继承(private):选择私有继承时,基类的所有成员在派生类中均变为私有成员,且这些成员无法被派生类的子类进一步访问。
-
保护继承(protected):在保护继承模式下,基类中的公有成员和保护成员在派生类中转变为保护成员,而私有成员则保持其私有性质。
无论采用哪种继承方式,派生类的成员函数及友元函数均有权访问基类中的公有成员和保护成员,但无法触及私有成员。特别地,在公有继承情况下,派生类的对象仅能访问基类的公有成员;而在保护继承和私有继承情况下,派生类的对象则无法访问基类中的任何成员。
3.2 接口继承和实现继承
在面向对象编程中,我们通常将类的公有成员函数视为其接口,这些接口对外界提供服务和功能。
-
公有继承(public inheritance):在这种继承模式下,基类的公有成员函数在派生类中保持其公有性质,即基类的接口被完整地继承并成为派生类的接口。因此,这种继承方式被称为接口继承。通过公有继承,父类的接口得以传递至子类,并且子类还可以继续向下派生至孙子类。
-
实现继承:对于私有继承(private inheritance)和保护继承(protected inheritance),派生类并不继承基类的公有接口。这意味着派生类不再对外提供基类的公有接口,其目的仅在于重用基类的实现细节。因此,这两种继承方式被称为实现继承。
-
保护继承:保护继承导致基类的公有接口在派生类中失效,这些接口只能在派生类内部被调用。尽管如此,保护继承仍然允许派生类继续向下派生至孙子类。
-
私有继承:私有继承同样使得基类的所有接口仅能在派生类内部使用。然而,与保护继承不同的是,私有继承切断了基类与后续孙子类之间的直接联系,即私有继承的派生类无法再向其子类传递基类的接口。
-
四、派生类的构造和析构函数
基类的构造函数不被继承,派生类中需要声明自己的构造函数。声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化要调用基类构造函数完成。假如基类构造函数没有缺省的构造函数,派生类的构造函数需要给基类的构造函数传递参数。
<派生类名>(<总参数表>)
:<基类构造函数名>(<参数表 1>),<子对象名>(<参数表 2>)
{
//<派生类中数据成员的初始化>
}
请看如下示例代码:
class CBase {
public:
CBase(int b) :m_b(b){
cout << "CBase ..." << endl;
}
~CBase() {
cout << "~CBasecoutend" << endl;
}
int m_b;
};
class Derived : public CBase {
public:
Derived(int b, int d) :m_d(d), CBase(b) {//调用父类构造函数
cout << "Derived ..." <<endl;
}
~Derived(){
cout << "~Derived ..."<< endl;
}
int m_d;
};
在C++中,当涉及到类的继承和包含对象成员时,构造函数和析构函数的调用顺序遵循特定的规则:
-
构造函数的调用顺序:
-
首先,调用基类的构造函数。如果基类提供了无参构造函数,则默认情况下会自动调用该构造函数;若基类没有提供无参构造函数,则派生类构造函数必须显式调用基类的某个构造函数。
-
其次,调用数据成员(即对象成员)的构造函数。
-
最后,调用派生类自身的构造函数。
-
-
析构函数的调用顺序:
-
首先,调用派生类的析构函数。
-
接着,调用数据成员的析构函数。
-
最后,调用基类的析构函数。
-
值得注意的是,析构函数的调用顺序与构造函数相反,确保了在对象生命周期结束时,按照创建时的逆序进行资源清理和状态还原。
示例代码:
#include <iostream>
using namespace std;
class ObjectB {
public:
ObjectB(int objb) :m_obj(objb) {
cout << "ObjectB ..." << endl;
}
~ObjectB() {
cout << "~0bjectB ..." << endl;
}
int m_obj;
};
class ObjectD {
public:
ObjectD(int objd) :m_objd(objd) {
cout << "ObjectD ..." << endl;
}
~ObjectD() {
cout << "~0bjectD ..." << endl;
}
int m_objd;
};
class Base{
public:
Base(int b) :m_b(b), m_objb(111) {//对象成员没有默认构造函数,必须在初始化列表中显示调用构造函数。
cout << "Base ..." << endl;
}
Base(const Base& other) :m_objb(other.m_objb), m_b(other.m_b) {
cout << "Copy " << "Base ..." << endl;
}
~Base(){
cout << "~Base ..." << endl;
}
int m_b; ObjectB m_objb;
};
class Derived :public Base {
public:
Derived(int b, int d) :m_d(d), Base(b), m_objd(222) {
//基类,与对象成员没有默认构造函数,必须在初始化列表中显示调用构造函数
cout << "Derived .."<<endl;
}
Derived(const Derived& other): m_d(other.m_d), m_objd(other.m_objd), Base(other) {
cout << "Copy" << "Derived ..." << endl;
}
~Derived() {
cout << "~Derived .."<< endl;
}
int m_d; ObjectD m_objd;
};
int main() {
Derived d(100, 200);
cout << d.m_b << " " << d.m_d << endl;
return 0;
}
五、类的成员重定义
在面向对象编程中,当子类需要对基类的某个成员进行功能上的修改或扩展时,会采用重定义(overwrite)机制。重定义主要分为以下两种情况:
-
对基类数据成员的重定义:子类可以重新定义与基类同名的数据成员,此时,派生类对象在访问该数据成员时,将直接引用派生类中的定义,从而隐式地屏蔽了基类中的同名数据成员。
-
对基类成员函数的重定义:
-
完全相同的重定义:子类重新定义一个与基类完全相同的成员函数(即函数名、参数列表和返回类型均一致),这种情况下,派生类对象调用该函数时,将执行派生类中的版本,基类的同名函数被隐藏。
-
参数不同的重定义:子类定义一个与基类成员函数名相同但参数列表不同的函数,尽管参数不同,但这种做法仍属于重定义而非重载。同样地,基类的同名函数会被隐藏,调用时将执行派生类中的函数。
-
注意事项:
-
当基类与派生类中存在重定义的数据成员时,派生类对象访问的是派生类中的数据成员,基类的数据成员被隐藏。
-
对于完全相同的重定义成员函数,基类的函数在派生类中被隐藏,调用时执行的是派生类的函数。
-
即使成员函数名相同但参数不同,这种重定义也会隐藏基类的函数,这是一个需要特别注意的点。
-
在不同作用域(如基类和派生类)中定义的函数,即使函数名相同且参数不同,也不构成重载,而是重定义。重载是指在同一作用域内,函数名相同但参数列表不同的现象,通常发生在同一个类内部。
示例代码:
#include <iostream>
using namespace std;
class Base {
public:
Base() :m_x(0) {}
int GetBaseX() const {
return m_x;
}
void Show() {
cout << "Base::Show ..."<< endl;
}
int m_x;
};
class Derived : public Base {
public:
Derived() :m_x(0) {}
int GetDerivedX() const {
return m_x;
}
void Show(int n) {
cout << "Derived::Show " << n << endl;
}
void Show() {
cout << "Derived::Show .." << endl;
}
int m_x;
};
class Test {
public:
Base m_b;
int m_x;
};
int main() {
Derived d;
d.m_x = 10; //如果没有重定义的话,它改变的就是基类的
d.Base::m_x = 20; //如果想使用基类的重定义的数据成员要是用作用域符号。
cout<< d.GetBaseX()<< endl;
cout << d.GetDerivedX() << endl;
d.Show();
d.Base::Show();//想要调用父类的函数可以通过作用域符号
cout << sizeof(Derived) << endl;
cout << sizeof(Test) << endl;
return 0;
}
六、多重继承
6.1 多继承
有一些事物,在分类学上属于不同的分类,比如野鸭,是鸟,也是鸭。这个时候就需要用到多继承。
单重继承--一个派生类最多只能有一个基类
多重继承--一个派生类可以有多个基类,如果派生类有两个或两个以上的直接基类,称为多继承。
如下图所示:
多继承派生类的定义格式:
class<派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,...
{
//派生类的类体
}
class CClassC : public CClassB, private CClassA
{
}
多继承派生类的构造函数格式:
<派生类名>(<总参数表>)
:<基类名1>(<参数表1>),<基类名 2>(<参数表 2>),<子对象名 3>(<参数 3>),…
{
//派生类构造函数
}
CClassC(int nNum)
:CClassB(nNum),CClassA(nNum),mnNum(15)
{
}
执行顺序:先执行所有基类的构造函数,再执行派生类本身的构造函数,包含子对象在内,请看下面的例子:
class A
{ ... };
class B
{ ... };
classC
{ ... };
class D:public A, public B, public C
{ ... };
派生类D继承了三个基类,继承方式都是公有继承。类D的成员包含了类A、类B和类C中的成员,以及它本身的成员。如果要创建类D的对象,首先要顺序执行类A、类B和类C的构造函数,再执行派生类D本身的构造函数。
6.2 多继承中的二义性问题
一般地,派生类成员的访问是唯一的。但是在多继承的情况下,可能出现派生类对其类成员访问的不唯一性,即二义性。下面是出现二义性的两种情况:调用不同类的具有相同名字成员时可能出现二义性:我们以下面这个沙发床例子来说明二义性问题。
示例代码:
#include <iostream>
using namespace std;
class Bed {
public:
Bed(int weight) :m_weight(weight) {}
void Sleep() {
cout << "Sleep ..." << endl;
}
int m_weight;
};
class Sofa {
public:
Sofa(int weight) :m_weight(weight) {}
void watchTV() {
cout << "Watch TV ..." << endl;
}
int m_weight;
};
class SofaBed :public Bed, public Sofa {//继承了之后,沙发床既可以睡觉,也可以看电视。
public:
SofaBed() :Bed(0), Sofa(0) {
FoldIn();
}
void FoldOut() {
cout << "FoldOut ..." << endl;
}
void FoldIn() {
cout << "FoldIn ..." << endl;
}
};
int main() {
SofaBed sofaBed;
//sofaBed.mweight=10;//这样访问是不明确的
//sofaBed.mweight=20://同上
sofaBed.Bed::m_weight = 10;
sofaBed.Sofa::m_weight = 20;
sofaBed.watchTV();
sofaBed.FoldOut();
sofaBed.Sleep();
return 0;
}
但是我们思考:沙发床应该只有一个重量,不应该分成两个重量。很自然的我们可以想到:将沙发和床进行抽象为一个整体,比如家具,在家具中安插一个重量,代码如下:
#include <iostream>
using namespace std;
class Furniture {
public:
Furniture(int weight) :m_weight(weight) {}
int m_weight;
};
class Bed : public Furniture {
public:
Bed(int weight) : Furniture(weight) {}
void Sleep() {
cout << "Sleep ..." << endl;
}
int m_weight;
};
class Sofa : public Furniture {
public:
Sofa(int weight) :Furniture(weight) {}
void watchTV() {
cout << "Watch TV ..." << endl;
}
int m_weight;
};
class SofaBed :public Bed, public Sofa {
public:
SofaBed() :Bed(0), Sofa(0) {
FoldIn();
}
void FoldOut() {
cout << "FoldOut ..." << endl;
}
void FoldIn() {
cout << "FoldIn ..." << endl;
}
};
int main() {
SofaBed sofaBed;
//sofaBed.m_weight=10;//这样访问是不明确的
//sofaBed.m_weight=20://同上
sofaBed.Bed::m_weight = 10;
sofaBed.Sofa::m_weight = 20;
sofaBed.watchTV();
sofaBed.FoldOut();
sofaBed.Sleep();
return 0;
}
这么安排,可能很多人就会觉得问题已经解决了,但是这里依然还是会出现二义性问题因为从 C++继承的角度讲,Bed中包含一个furniture,sofa中包含一个furnitur。所以重量这个东西还是有两份。
那该怎么办呢?这下就要用虚继承来解决这个问题。
七、虚基类
在多重继承的架构中,当一个派生类继承自多个基类,而这些基类又共享同一个共同的基类时,访问该共同基类中的成员可能会引发二义性问题。为了解决这一问题,C++引入了虚基类的概念。
虚基类的引入旨在应对存在共同基类的情况,其主要作用是消除因多次继承同一基类而可能产生的二义性。通过使用虚基类,可以确保在最底层的派生类中,对于共同的基类成员只有一份唯一的副本,避免了数据的重复。
具体而言,如果一个派生类拥有多个直接基类,而这些直接基类均继承自同一个共同的基类,那么在最终的派生类中,原本会保留该间接共同基类的多份同名数据成员。C++通过提供虚基类的机制,确保在继承过程中仅保留一份共同基类的成员,从而避免了成员的重复和潜在的二义性问题。
虚基类(virtual base class)说明格式如下:
virtual<继承方式><基类名>
virtual public CClassA
代码示例:
#include <iostream>
using namespace std;
class Furniture {
public:
Furniture(int weight) :m_weight(weight) {}
int m_weight;
};
class Bed : virtual public Furniture {
public:
Bed(int weight) : Furniture(weight) {}
void Sleep() {
cout << "Sleep ..." << endl;
}
};
class Sofa : virtual public Furniture {
public:
Sofa(int weight) :Furniture(weight) {}
void watchTV() {
cout << "Watch TV ..." << endl;
}
};
class SofaBed :public Bed, public Sofa {
public:
SofaBed() :Bed(0), Sofa(0) {
FoldIn();
}
void FoldOut() {
cout << "FoldOut ..." << endl;
}
void FoldIn() {
cout << "FoldIn ..." << endl;
}
};
int main() {
SofaBed sofaBed;
sofaBed.m_weight = 10;
return 0;
}
使用虚继承后,可以实现如下的效果,项层类中的数据在对象中只会存在一份。
八、总结
C++语言提供了类继承的机制,这是面向对象编程中的核心概念之一。在继承关系中,派生类的成员函数和友元函数能够访问基类中所有的公有和保护类型的数据成员及成员函数。然而,派生类对象本身仅能访问基类中的公有数据成员和成员函数。
多重继承是指一个类从多个基类中继承特性的机制,派生类因此获得了所有基类的属性。当一个类作为两个或更多基类的派生类时,必须在派生类的声明中,在类名和冒号后面列出所有基类的名称,各基类之间以逗号分隔。派生类的构造函数有责任激活所有基类的构造函数,并将必要的参数传递给它们。
派生类本身也可以成为其他类的基类,从而形成一个继承链。在这种情况下,当派生类的构造函数被调用时,它的所有基类的构造函数也会被依次激活。为了解决多重继承中可能出现的二义性问题,C++引入了虚基类的概念。虚基类的目的是确保公共基类在其派生对象中仅产生一个基类子对象,从而避免因多次继承同一基类而导致的重复和冲突。