C++的继承机制详解
- 引言
- 一、继承和派生
- 1.1、继承的概念和意义
- 1.2、派生类的定义
- 二、继承中的构造和析构
- 2.1、子类的构造和析构顺序
- 2.2、子类调用成员对象、父类的有参构造
- 三、子类和父类的同名处理
- 3.1、子类和父类同名成员数据
- 3.2、子类和父类同名成员函数
- 3.3、子类重定义父类的同名函数
- 四、子类不能继承父类的哪些成员?
- 五、多继承
- 5.1、多继承的概念
- 5.2、多继承中的同名成员处理
- 六、菱形继承
- 七、虚继承
- 7.1、虚继承的方式
- 7.2、通过VS查看虚继承的实现原理
- 总结
引言
💡 作者简介:专注于C/C++高性能程序设计和开发,理论与代码实践结合,让世界没有难学的技术。包括C/C++、Linux、MySQL、Redis、TCP/IP、协程、网络编程等。
👉
🎖️ CSDN实力新星,社区专家博主
👉
🔔 专栏介绍:从零到c++精通的学习之路。内容包括C++基础编程、中级编程、高级编程;掌握各个知识点。
👉
🔔 专栏地址:C++从零开始到精通
👉
🔔 博客主页:https://blog.csdn.net/Long_xu
🔔 上一篇:【031】C++类和对象之运算符重载详解和代码实践(最全讲解)
一、继承和派生
1.1、继承的概念和意义
C++继承是一种面向对象编程的重要特性,它允许程序员创建一个新类(称为派生类或子类),该类可以从另一个现有类(称为基类或父类)继承属性和行为。这意味着派生类会自动拥有基类中的所有成员变量和成员函数,并且可以在此基础上进行修改、扩展和优化。
C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来重新定义新的数据类型,新的类不仅有旧类的成员,还拥有新定义的成员。
一个B类继承于A类,或者说从类A派生类B;这样一来,类A成为了基类(父类),类B称为派生类(子类)。派生类中的成员包含两部分:一部分是从基类中继承过来的,一部分是自己增加的成员。从基类继承过来的表现其共性,而新增的成员体现其个性。
继承的主要作用是提高代码的复用性和可维护性,提高开发效率。通过将公共部分放在基类中定义,不同的派生类只需要添加其特定功能即可,避免了代码冗余,并且使得修改更加容易。另外,继承还支持多态性,即同一个方法名可以在不同的子类中具有不同的实现,这大大增强了程序灵活性和可扩展性。
1.2、派生类的定义
语法:
class 父类{
};
class 子类:继承方式 父类{
// 新增子类数据
};
继承方式有:
- private
- protected
- public(推荐)
所有父类的私有数据在子类中不可访问,公共继承保持不必,保护继承变保护,私有继承变私有。
示例:
#include <iostream>
using namespace std;
class A{
private:
int a;
protected:
int b;
public:
int c;
};
class B:public A{
public:
void func(void)
{
//cout<<a<<endl; // error,不可访问
cout<<b<<c<<endl;
}
};
int main()
{
B ob;
//cout<<ob.b<<endl;// error,不可访问
cout<<ob.c<<endl;// OK
return 0;
}
二、继承中的构造和析构
2.1、子类的构造和析构顺序
C++子类的构造和析构顺序如下:
-
当创建子类对象时,首先会调用父类的默认构造函数来初始化父类部分。
-
然后调用子类的构造函数来初始化子类部分。在子类构造函数中可以通过初始化列表调用父类的有参构造函数或者其他构造函数。
-
如果子类没有定义自己的析构函数,则会使用默认的析构函数。当销毁一个子类对象时,首先会调用子类的析构函数,然后再调用父类的析构函数。
-
如果子类定义了自己的析构函数,则需要在其中显式地调用父类的析构函数以确保正确释放资源。
示例:
#include <iostream>
using namespace std;
class Base {
public:
Base()
{
cout << "Base构造函数" << endl;
}
~Base()
{
cout << "Base析构函数" << endl;
}
};
class Other {
public:
Other()
{
cout << "Other构造函数" << endl;
}
~Other()
{
cout << "Other析构函数" << endl;
}
};
class Son:public Base {
private:
Other ot;
public:
Son()
{
cout << "Son构造函数" << endl;
}
~Son()
{
cout << "Son析构函数" << endl;
}
};
int main()
{
Son ob;
return 0;
}
输出:
Base构造函数
Other构造函数
Son构造函数
Son析构函数
Other析构函数
Base析构函数
2.2、子类调用成员对象、父类的有参构造
子类实例化对象时会自动调用成员对象、父类的默认构造函数。
子类实例化对象时必须使用初始化列表调用 成员对象、父类的有参构造。
初始化列表时:父类写类名称,成员对象用对象名。
示例:
#include <iostream>
using namespace std;
class Base {
public:
int a;
public:
Base()
{
cout << "Base构造函数" << endl;
}
Base(int a)
{
cout << "Base有参构造函数" << endl;
this->a = a;
}
~Base()
{
cout << "Base析构函数" << endl;
}
};
class Other {
public:
int b;
public:
Other()
{
cout << "Other构造函数" << endl;
}
Other(int b)
{
cout << "Other有参构造函数" << endl;
this->b = b;
}
~Other()
{
cout << "Other析构函数" << endl;
}
};
class Son:public Base {
private:
Other ot;
public:
int c;
public:
Son()
{
cout << "Son构造函数" << endl;
}
Son(int a, int b, int c) :Base(a), ot(b)
{
cout << "Son有参构造函数" << endl;
this->c = c;
}
~Son()
{
cout << "Son析构函数" << endl;
}
};
int main()
{
Son ob(100,200,300);
return 0;
}
输出:
Base有参构造函数
Other有参构造函数
Son有参构造函数
Son析构函数
Other析构函数
Base析构函数
三、子类和父类的同名处理
同名成员 最简单 最安全的处理方式:加作用域。主要是加父类的作用域,如果是访问子类的成员不需要加作用域即可默认访问子类的成员,所以要访问父类的成员最好加上父类的作用域。
3.1、子类和父类同名成员数据
子类默认优先访问子类的同名成员。必须加父类作用域访问父类的同名成员。
示例:
#include <iostream>
using namespace std;
class Base {
public:
int a;
public:
Base()
{
cout << "Base构造函数" << endl;
}
Base(int a)
{
cout << "Base有参构造函数" << endl;
this->a = a;
}
~Base()
{
cout << "Base析构函数" << endl;
}
};
class Son:public Base {
public:
int a;
public:
Son()
{
cout << "Son构造函数" << endl;
}
Son(int a, int b) :Base(a)
{
cout << "Son有参构造函数" << endl;
this->a = b;
}
~Son()
{
cout << "Son析构函数" << endl;
}
};
int main()
{
Son ob(100,200);
// 子类默认优先访问子类的同名成员
cout << ob.a << endl;// 输出200
// 必须添加父类作用域访问父类的同名成员
cout << ob.Base::a << endl;// 输出100
return 0;
}
输出:
Base有参构造函数
Son有参构造函数
200
100
Son析构函数
Base析构函数
3.2、子类和父类同名成员函数
与子类和父类同名成员数据类似。
示例:
#include <iostream>
using namespace std;
class Base {
public:
int a;
public:
Base()
{
cout << "Base构造函数" << endl;
}
Base(int a)
{
cout << "Base有参构造函数" << endl;
this->a = a;
}
void func()
{
cout << "Base无参 func 函数" << endl;
}
~Base()
{
cout << "Base析构函数" << endl;
}
};
class Son:public Base {
public:
int a;
public:
Son()
{
cout << "Son构造函数" << endl;
}
Son(int a, int b) :Base(a)
{
cout << "Son有参构造函数" << endl;
this->a = b;
}
void func()
{
cout << "Son无参 func 函数" << endl;
}
~Son()
{
cout << "Son析构函数" << endl;
}
};
int main()
{
Son ob(100,200);
// 子类默认优先访问子类的同名成员
cout << "----------------------------" << endl;
ob.func();
// 必须添加父类作用域访问父类的同名成员
ob.Base::func();
cout << "----------------------------" << endl;
return 0;
}
输出:
Base有参构造函数
Son有参构造函数
----------------------------
Son无参 func 函数
Base无参 func 函数
----------------------------
Son析构函数
Base析构函数
3.3、子类重定义父类的同名函数
重载:无继承,同一作用域,参数的个数不同、顺序不同、类型不同都可以重载。
重定义:有继承,子类重定义父类的同名函数(参数可以不同,非虚函数),子类一旦重定义了父类的同名函数(不管参数是否一致),子类都将屏蔽父类所有的同名函数。重定义是继承之间的一个重载,子类重载父类的同名函数。
示例:
#include <iostream>
using namespace std;
class Base {
public:
void func()
{
cout << "Base无参 func 函数" << endl;
}
void func(int a)
{
cout << "Base 一个参数 func 函数" << endl;
}
void func(int a,int b)
{
cout << "Base 两个参数 func 函数" << endl;
}
};
class Son:public Base {
public:
void func(const char *str)
{
cout << "Son char*参数的 func 函数" << endl;
cout << str << endl;
}
};
int main()
{
Son ob;
cout << "----------------------------" << endl;
ob.func("hello,world");
// ob.func(); // 屏蔽了,不识别父类的func
// ob.func(100); // 屏蔽了,不识别父类的func
// ob.func(100,200); // 屏蔽了,不识别父类的func
cout << "----------------------------" << endl;
// 需要加父类的作用域才能识别屏蔽的函数
ob.Base::func();
ob.Base::func(100);
ob.Base::func(100,200);
return 0;
}
输出:
----------------------------
Son char*参数的 func 函数
hello,world
----------------------------
Base无参 func 函数
Base 一个参数 func 函数
Base 两个参数 func 函数
四、子类不能继承父类的哪些成员?
不是所有的函数都能自动从基类继承到派生类。不能继承的成员:
- 构造函数。
- 析构函数。
- operator=。
构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,不能被继承,必须为每个特定的派生类分别创建。
另外,operator也不能被继承,因为它完成类似构造函数行为。尽管我们知道如何由=右边对象初始化-左边对象的所有成员,但这不意味着对派生类依旧有效。
在继承中,如果没有创建这些函数,编译器自动生成它们。
五、多继承
5.1、多继承的概念
C++的多继承是指一个类可以从多个基类中继承数据和函数成员。这种机制允许程序员将不同的功能集成到一个类中,以便更好地组织代码和实现复杂的功能。
多继承是非常受争议的,从多个类继承可能导致函数、变量等同名造成较多的歧义。
在C++中,多继承的语法形式如下:
class Derived : access-specifier Base1, access-specifier Base2, ..., access-specifier BaseN {
//...
};
其中Derived是派生类,Base1、Base2、…、BaseN都是基类,access-specifier表示派生类对基类的访问权限(public、protected或private)。
示例:
#include <iostream>
using namespace std;
class Base1{
public:
int a;
public:
void func()
{
cout<<"Base1 func"<<endl;
}
};
class Base2{
public:
int b;
public:
void func2()
{
cout<<"Base2 func"<<endl;
}
};
class Son:public Base1,public Base2{
public:
int c;
};
int main()
{
Son obj;
cout<<obj.a<<obj.b<<obj.c<<endl;// OK
return 0;
}
5.2、多继承中的同名成员处理
在多继承中,如果不同的基类拥有相同名称的成员函数或数据成员,则需要通过作用域限定符明确调用所需的成员。例如:
class A {
public:
void func() { cout << "A::func" << endl; }
};
class B {
public:
void func() { cout << "B::func" << endl; }
};
class C : public A, public B {
public:
void test() {
A::func(); // 明确调用A中的func
B::func(); // 明确调用B中的func
}
};
此外,在多继承中也存在菱形继承问题,即当一个派生类同时从两个不同的基类继承某个共同父类时,会出现重复定义和二义性等问题。为了解决这个问题,C++提供了虚拟继承(virtual inheritance)的机制。
六、菱形继承
有公共祖先的继承,称为菱形继承。最底层的子类数据会包含多份(公共祖先的数据)。
菱形继承是指一个派生类同时继承两个基类,而这两个基类又间接或直接继承同一个虚基类的情况。
示例:
class A {
public:
int a;
};
class B : virtual public A {
public:
int b;
};
class C : virtual public A {
public:
int c;
};
class D : public B, public C {
public:
int d;
};
在上述代码中,A 是虚基类,B 和 C 都从 A 继承。D 类通过多重继承同时从 B 和 C 继承,并且由于 B 和 C 都从 A 虚拟继承,因此 D 只会得到一个 A 的实例。
菱形继承带来的问题主要是二义性和空间浪费。例如,在上述代码中如果有以下函数:
void func(D* d) {
d->a = 1; // 哪个 a?
}
这里就不知道应该访问哪个 a 变量了。这时候可以通过指定作用域来访问,例如:
void func(D* d) {
d->B::a = 100;
d->C::a = 200;
}
为了避免这些问题,在使用多重继承时需要特别小心并且合理设计程序结构。
七、虚继承
针对菱形继承,能不能只要公共祖先的一份数据呢?可以通过虚继承解决菱形继承中多份公共祖先数据的问题。
7.1、虚继承的方式
虚继承是 C++ 中用于解决多重继承中出现的菱形继承问题的一种技术。在虚继承中,派生类通过 virtual 关键字指定从基类进行继承,这样就可以保证只有一个实例存在。
例如:
#include <iostream>
using namespace std;
class A {
public:
int a;
};
class B : virtual public A {
public:
int b;
};
class C : virtual public A {
public:
int c;
};
class D : public B, public C {
public:
int d;
};
int main()
{
D obj;
cout<<obj.a<<endl;// 可以访问
return 0;
}
在上述代码中,B 和 C 都通过虚拟继承方式从 A 继承,这样 D 类只会得到一个 A 的实例,从而避免了菱形继承所带来的二义性和空间浪费问题。
在使用虚继承时需要特别小心并且合理设计程序结构。由于虚继承需要额外的时间和空间开销,因此应该遵循“最小依赖原则”,即尽可能减少对基类的依赖关系,并且将共同部分抽象成接口或抽象基类。
7.2、通过VS查看虚继承的实现原理
首先建立一个项目,输入如下代码:
#include <iostream>
using namespace std;
class MyA {
public:
int a;
};
class MyB : virtual public MyA {
public:
int b;
};
class MyC : virtual public MyA {
public:
int c;
};
class MyD : public MyB, public MyC {
public:
int d;
};
int main()
{
MyD obj;
obj.a = 100;
cout << obj.a << endl;// 可以访问
return 0;
}
(1)打开命令行开发者模式。
(2)找到类所在源文件的路径(右击项目解决方案)。
(3)命令行切到源文件目录。
(4)在命令行中导出类的布局。
格式:
cl /d1 reportSingleClassLayout<自己的类名称> 源文件名
例如:
cl /d1 reportSingleClassLayoutMyD main.cpp
(5)各个类的布局情况。
MyA的布局:
main.cpp
class MyA size(4):
+---
0 | a
+---
Microsoft (R) Incremental Linker Version 14.16.27045.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:main.exe
main.obj
MyB的布局:
class MyB size(12):
+---
0 | {vbptr}
4 | b
+---
+--- (virtual base MyA)
8 | a
+---
MyB::$vbtable@:
0 | 0
1 | 8 (MyBd(MyB+0)MyA)
vbi: class offset o.vbptr o.vbte fVtorDisp
MyA 8 0 4 0
Microsoft (R) Incremental Linker Version 14.16.27045.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:main.exe
main.obj
MyC的布局:
class MyC size(12):
+---
0 | {vbptr}
4 | c
+---
+--- (virtual base MyA)
8 | a
+---
MyC::$vbtable@:
0 | 0
1 | 8 (MyCd(MyC+0)MyA)
vbi: class offset o.vbptr o.vbte fVtorDisp
MyA 8 0 4 0
Microsoft (R) Incremental Linker Version 14.16.27045.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:main.exe
main.obj
MyD的布局:
main.cpp
class MyD size(24):
+---
0 | +--- (base class MyB)
0 | | {vbptr}
4 | | b
| +---
8 | +--- (base class MyC)
8 | | {vbptr}
12 | | c
| +---
16 | d
+---
+--- (virtual base MyA)
20 | a
+---
MyD::$vbtable@MyB@:
0 | 0
1 | 20 (MyDd(MyB+0)MyA)
MyD::$vbtable@MyC@:
0 | 0
1 | 12 (MyDd(MyC+0)MyA)
vbi: class offset o.vbptr o.vbte fVtorDisp
MyA 20 0 4 0
Microsoft (R) Incremental Linker Version 14.16.27045.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:main.exe
main.obj
可以看出,虚继承会在子类中产生虚基类指针(vbptr)指向虚基类表(vbtable),虚基类表记录的是通过该指针访问公共祖先的数据的偏移量。
注意:
虚继承只能解决具备公共祖先的多继承带来的二义性问题,不能解决没有公共祖先的多继承。在工程开发中真正意义上的多继承几乎不被使用,因为多继承带来的代码复杂性远大于其带来的便利性;多继承对代码维护上的影响是灾难性的,在设计方法上任何多继承都可以被单继承替代。
总结
C++继承机制是面向对象编程的核心概念之一,它通过子类继承父类的属性和方法实现代码复用和扩展。以下是C++继承机制的总结:
(1) 继承类型。C++中有3种继承类型:公有继承、私有继承和保护继承。
- 公有继承(public inheritance):子类可以访问父类的公有成员和保护成员,但不能访问父类的私有成员。
- 私有继承(private inheritance):子类可以访问父类的公有成员、保护成员和私有成员,但在外部不能通过子类对象访问这些成员。
- 保护继承(protected inheritance):子类可以访问父类的公有成员和保护成员,但不能访问父类的私有成员,在外部也不能通过子类对象访问这些成员。
(2) 构造函数和析构函数。子类对象创建时会先调用父类的构造函数,再调用自己的构造函数;而销毁时则相反,先调用自己的析构函数,再调用父类的析构函数。同时需要注意虚析构函数和多重继承时可能会出现死循环等问题。
(3) 虚函数。虚函数是用于多态的重要机制,子类可以通过覆盖(override)父类的虚函数实现自己的行为。当调用一个指向父类对象的指针或引用时,如果该对象被子类继承并重写了虚函数,则会调用子类中的对应函数。
(4)虚继承。当存在多个继承路径时,可能会导致同一个父类在内存中有多个拷贝,造成浪费和不一致。此时可以使用虚继承(virtual inheritance),让所有从同一基类派生出来的子类共享该基类,在内存中只保留一个拷贝。
(5)多重继承。C++允许同时从多个父类继承属性和方法,称为多重继承。但需要注意解决命名冲突、二义性和虚表等问题。