C++第十三篇:继承
目录
一、继承的格式
二、继承的实现
三、菱形继承
四、虚继承
在 C++ 中,继承是面向对象编程的三大核心特性之一(另外两个是封装和多态),它允许一个类(子类 / 派生类)继承另一个类(父类 / 基类)的属性和方法,从而实现代码复用和层次化设计。
什么时候使用继承呢?
is a的关系。(就是说子类时父类的一个特殊的类型,就好比”圆形“和“图形”的关系,圆形是归属图形的一种,圆形有图形的特征,需要继承自图形。在好比打王者荣耀时的“韩信”和“英雄”的关系,韩信是一个英雄,它有英雄的基本特征,如血条,蓝条等等。)
存在层次关系和实现多态的场景会使用继承。
在没有必要使用继承时,尽量去减少继承的使用,否则可能会导致类关系复杂,难以梳理。
一、继承的格式
class 派生类名:继承方式 基类名1,继承方式 基类名2,....
{
派生类成员;
};
这里继承的有不同方式,分别有public、protected、private继承,继承方式的作用是限制基类成员在派生类中的访问级别。下面是不同继承下的访问权限:
这张图的意思就是,基类的private成员,不论派生类使用哪种继承,都是不可访问的;基类的protected成员,在protected和public继承下是属于protected的,在private继承下属于private;基类的public的成员在,在三种不用的继承下,分别属于不同的级别。
这里顺便在说一下三种级别的区别:
public
:类内、派生类内、类外(通过对象)都能访问。protected
:仅类内和派生类内可访问,类外不可。private
:仅类内可访问,派生类和类外都不可。
二、继承的实现
举例:
#include <iostream>
#include <string>
using namespace std;// 基类:英雄类
class Hero {
protected:string name; // 英雄名称int health; // 生命值int attack; // 攻击伤害float moveSpeed; // 移动速度
public:// 构造函数:初始化英雄基本属性Hero(string n, int h, int a, float ms) : name(n), health(h), attack(a), moveSpeed(ms) {cout << "英雄 [" << name << "] 已创建!" << endl;}// 析构函数~Hero() {cout << "英雄 [" << name << "] 已销毁!" << endl;}
};// 派生类:韩信(继承自英雄类)
class HanXin : public Hero {
private:int skillDamage; // 技能额外伤害
public:// 构造函数HanXin() : Hero("韩信", 3800, 180, 4.5), skillDamage(350) {cout << "韩信:\"到达胜利之前,无法回头!\"" << endl;}// 析构函数~HanXin() {cout << "韩信:\"我的心,可不冷...\"" << endl;}
};int main() {HanXin hanxin; // 创建韩信对象return 0;
}
结果如下:
这就是一个继承的实例,在运行的结果我们可以看到构造函数的调用顺序,先构造基类,然后是派生类,释放时先调用派生类的析构,再调用基类的析构函数。
这就是一个继承,上面介绍的时候就说了,继承也是为了多态的实现。那下来就来来看看什么事多态。
三、菱形继承
菱形继承是多继承中一种特殊且容易引发问题的场景,因类继承关系图呈菱形而得名。
菱形继承的关系。
B
和C
都继承自A
;D
同时继承B
和C
;- 此时
D
会间接包含两份A
的成员(一份来自B
,一份来自C
),可能导致问题。
代码示例:
#include <iostream>
using namespace std;// 基类 A
class A {
public:int a;A() : a(10) { cout << "A 构造" << endl; }
};// 基类 B(继承 A)
class B : public A {
public:B() { cout << "B 构造" << endl; }
};// 基类 C(继承 A)
class C : public A {
public:C() { cout << "C 构造" << endl; }
};// 派生类 D(同时继承 B 和 C)
class D : public B, public C {
public:D() { cout << "D 构造" << endl; }
};int main() {D d;// cout << d.a << endl; 错误:二义性// 必须显式指定来源cout << "B::a = " << d.B::a << endl; // 来自 B 继承的 Acout << "C::a = " << d.C::a << endl; // 来自 C 继承的 Areturn 0;
}
结果如下:
从结果上发现,这个A被构造了两次,同时在访问数据上,必须要指明数据的来源。这就是菱形继承带来的问题,数据冗余和二义性。
怎么解决呢?这里又有了虚继承。
四、虚继承
虚继承是为了解决菱形继承所带来的数据冗余和二义性。直接看代码:
#include <iostream>
using namespace std;class A {
public:int a;A() : a(10) { cout << "A 构造" << endl; }
};// 虚继承 A
class B : virtual public A {
public:B() { cout << "B 构造" << endl; }
};// 虚继承 A
class C : virtual public A {
public:C() { cout << "C 构造" << endl; }
};class D : public B, public C {
public:D() { cout << "D 构造" << endl; }
};int main() {D d;// 正常访问:仅一份 A::acout << "d.a = " << d.a << endl; // 10return 0;
}
结果如下:
可以看到,A类现在只被创建了一次,同时数据也可以直接访问了。
这个虚继承的思想很好理解。实际就是虚基类(A
)的实例只被创建一次,并被所有间接继承它的中间基类(B
、C
)和派生类(D
)共享。有个虚基类表和虚基类指针的作用是记录这个唯一实例的地址,确保B
、C
、D
都能通过统一的路径访问A
的成员,从而避免数据冗余和二义性。