100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > C++中的各种“虚“-- 虚函数 纯虚函数 虚继承 虚基类 虚析构 纯虚析构 抽象类讲解

C++中的各种“虚“-- 虚函数 纯虚函数 虚继承 虚基类 虚析构 纯虚析构 抽象类讲解

时间:2023-08-09 11:12:21

相关推荐

C++中的各种“虚“-- 虚函数 纯虚函数 虚继承 虚基类 虚析构 纯虚析构 抽象类讲解

C++中的各种"虚"

1. 菱形继承1.1 虚继承 && 虚基类1.2 虚基类指针(vbptr)&& 虚基类表(vbtable) 2. 多态2.1 函数地址绑定时机(早/晚绑定)2.2 虚函数2.3 虚函数指针(vfptr)与虚函数表(vftable)2.3.1 多态的优点 2.4 纯虚函数 && 抽象类2.5 虚析构 && 纯虚析构

C++中的一些重要概念都与“”相关,比如:

① 虚基类、虚继承、虚基类指针(vbptr)、虚基类表(vbtable);

② 虚函数、纯虚函数、虚函数指针(vfptr)、虚函数表(vftable)、抽象类、虚析构、纯虚析构。

这里对上面罗列出的概念做一个总结,争取把这些都一次讲清楚:

1. 菱形继承

由于C++支持多继承,即一个类可以继承自多个类,故有时候会存在菱形继承(又叫钻石继承)的情景,即两个子类继承同一个父类而又有子类同时继承这两个子类

菱形继承示意伪代码:

class CA{public:int m_A;};class CB :public CA{};class CC :public CA{};class CD :public CB,public CC{};

即CB和CC继承自CA类,而CD由继承自CB类和CC类。

菱形继承会产生一些问题:

当CD对象想要调用成员变量m_A的时候,如果不使用作用域加以区分调用哪个父类中的m_A时,会产生错误,即产生二义性的问题:

CD *obj = new CD();obj->CB::m_A = 10;obj->CC::m_A = 20;//cout << obj->m_A << endl;// 报错cout << obj->CB::m_A << endl;// 成功执行,输出10cout << obj->CC::m_A << endl;// 成功执行,输出20delete obj;

菱形继承导致同个成员变量的多次继承,造成CD对象中m_A变量的冗余(空间浪费)。

而虚继承技术是用于解决菱形继承上述问题的方法。

1.1 虚继承 && 虚基类

利用虚继承可以解决菱形继承的问题,在继承之前,加上关键字virtual

class CA{int m_A;};class CB :virtual public CA{};class CC :virtual public CA{};class CD :public CB,public CC{};

CB和CC虚继承自CA类,此时CA类就是虚基类

此时不论加不加作用域,CD访问m_A都是在访问同一片地址:

CD *obj = new CD();obj->CB::m_A = 10;obj->CC::m_A = 20;cout << obj->m_A << endl;// 成功执行,输出20cout << obj->CB::m_A << endl;// 成功执行,输出20cout << obj->CC::m_A << endl;// 成功执行,输出20delete obj;

这是因为CD类的对象从CB和CC中继承下来的对象不再是他们各自的m_A,而是一个指针 ——vbptr (virtual base pointer,虚基类指针)

1.2 虚基类指针(vbptr)&& 虚基类表(vbtable)

CD实例化对象中的vbptr会指向其vbtable ( virtual base table,虚基类表 ), 而虚基类表中记录着vbptr指向实际变量的偏移量(offset),通过vbptr + offset的方式可以访问到唯一的成员变量,从而不再产生歧义和空间浪费的问题。

【注意】C++ 创建一个子类对象时会调用父类的构造函数,那么会创建父类对象吗?

答曰:不会创建另外一个父类对象,只是初始化子类中属于父类的成员,父子类上同名的成员变量和函数可以通过作用域来指定。

创建一个对象的时候,发生了两件事情,一是分配对象所需的内存,二是调用构造函数进行初始化。子类对象包含从父类对象继承过来的成员,实现上来说,一般也是子类的内存区域中有一部分就是父类的内存区域。调用父类构造函数的时候,这块父类对象的内存区域就被初始化了。为了避免未初始化的问题,语法强制子类调用父类构造函数。

2. 多态

多态是C++面向对象的三大特性之一。

多态可以分为两类:

静态多态: 函数重载 和 运算符重载属于静态多态,即复用函数名;动态多态: 基于派生类虚函数实现运行时多态。

静态多态和动态多态的区别:

静态多态:函数地址早绑定—— 编译阶段就已经确定函数地址;动态多态:函数地址晚绑定—— 运行阶段才能确定函数的地址。

2.1 函数地址绑定时机(早/晚绑定)

通过下面的C++伪代码来理解什么是函数地址早/晚绑定

/* 动物类 */class Animal {public:void speak(){cout << "动物在说话" << endl; }};/* 猫类 */class Cat :public Animal{void speak(){cout << "迪奥纳特调~" << endl; }};/* 测试API */void doSpeak(Animal &animal){// 父类引用指向子类对象,animal.speak();}/* 测试案例 */void test01(){Cat cat;doSpeak(cat);//?问题:该行输出什么?}

C++中允许父子之间的类型转换(不需要强制转换),在doSpeak()函数中参数是父类的引用,test01()函数中传入的是子类对象,这在语法上是没毛病的。可能会有的同学认为我们传入的参数是Cat类,理应调用Cat类的speak()函数,但实际上 test01()函数中,输出的结果是"动物在说话",即调用的是父类Animal类的speak()函数

为什么会产生这样的现象?

原因就在于void doSpeak(Animal &animal)函数是地址早绑定的,即在编译时就已经确定doSpeak()内部speak()函数的调用地址是Animal类中的speak()函数,故此不论传入test01()函数的对象参数是继承自Animal类的猫类狗类还是别的什么类,最终的结果都将是调用父类Animal类的speak()函数。

如果想让猫说话,这个函数的地址就不能是早绑定的,需要在运行阶段进行绑定(晚绑定),通过派生类和虚函数实现,即运行时多态。

2.2 虚函数

在基类Animal类的void speak()函数前加上virtual关键字,使其成为虚函数:

virtual void speak(){cout << "动物在说话" << endl; }

继承自含有虚函数的基类后,子类重写父类中的虚函数,就可以实现地址晚绑定。

此时再次运行test01()函数后,输出的结果是"迪奥纳特调~"。特点就是会根据传入的对象不同,执行相应类的函数,总结如下:

动态多态满足条件

有继承关系子类重写父类中的虚函数

动态多态的使用

父类的引用指向子类传入对象

/* 假设Cat类继承自Animal类且重写了Animal类的虚函数 */ // 参数为父类引用void doSpeak(Animal &animal){animal.speak();}int main(){Cat cat;// 传入子类对象doSpeak(cat);// 执行Cat类中的speak()函数return 0;}

父类的指针指向子类传入对象

/* 假设Cat类继承自Animal类且重写了Animal类的虚函数 */Animal *obj = new Cat();obj.speak();// 执行Cat类中的speak()函数

2.3 虚函数指针(vfptr)与虚函数表(vftable)

当我们在给Animaal类的speak()函数加上virtual关键字之前,实例化一个Animal对象obj并用sizeof(obj),可以看到,大小为1字节。

这是因为C++类中只有非静态成员变量是存储在对象中的,其他的静态成员变量、静态成员函数、成员函数都由所有对象共享类中的一份实例,而为了区分空对象和NULL,C++中规定空对象的大小为1个字节。

但在给Animaal类的speak()函数加上virtual关键字之后,再使用sizeof()函数查看该对象大小,可以看到结果是4字节(32位OS)或8字节(64位OS),具体视操作系统位数而定。

这是为什么呢?

因为使用虚函数后,在对象的地址空间中存储了一个指针,即 vfptr(virtual function pointer,虚函数指针);

vfptr 指针会指向一张表,即 vftable(virtual function table,虚函数表),该表内部会记录虚函数的地址。

当子类即Cat类没有重写父类即Animal类中的虚函数时,子类会继承父类中的vfptr和vftable,如下示意图:

当子类即Cat类重写父类即Animal类中的虚函数之后,子类中 vftable 内部会替换成子类虚函数的地址(父类中的vftable没有改变),如下示意图:

在满足继承与虚函数的重写后,当父类的指针或者引用指向子类对象时,就会发生多态,具体执行子类还是父类中的函数由子类中 vfptr 查 vftable 决定。

2.3.1 多态的优点

使用多态有如下优点:

代码组织结构清晰,可读性强利于项目的前期开发和后期的拓展及维护

使用多态符合大型软件工程开发设计原则中的开闭原则,即对修改(源码)关闭,对添加(插件/功能/模块)开放。

举一个例子,比如我们要实现一个二元运算计算器,在没有掌握多态之前,通常会使用流程控制语句如if…else或goto、switch等 来对参数中的操作符做判断再执行相应运算;

这样写虽然简洁快速,但是对于大型的项目来说,如果需要给该计算器添加新的运算方式如求n次幂时,我们需要去源码的流程控制语句中添加一个判断和执行,这样就违背了开闭原则,不利于项目后期的维护与拓展;

如果使用多态,那么可以设计一个基类,该基类中包含两个操作数做成员变量,以及一个虚函数;

这样在需要后续扩展每种运算功能时,只需一个继承自该基类的子类,并重写基类中的虚函数为具体的计算函数即可(不需要修改源码,而是添加子类),即一个子类对应于一种运算。在需要进行运算时只需要将基类的指针或引用指向子类的对象,并调用该指针或引用的相应函数即可实现多态。

2.4 纯虚函数 && 抽象类

在多态中,通常父类中的虚函数的实现是没有意义的,主要都是调用子类重写父类的虚函数,因此,可以将虚函数改为纯虚函数

纯虚函数语法

virtual 返回值类型 函数名 (参数列表) = 0;

类中有了纯虚函数,这个类也成为抽象类

抽象类特点:

无法实例化对象子类必须重写抽象类中的纯虚函数,否则也属于抽象类

2.5 虚析构 && 纯虚析构

使用多态时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的膝盖函数改为虚析构纯虚析构

C++中构造函数的调用顺序由父类到子类依次构造,析构函数相反。

虚析构和纯虚析构共性:

可以解决父类指针释放子类对象都需要有具体的函数实现

虚析构和纯虚析构区别:

如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

/* 类内声明 */virtual ~类名() = 0;/* 类外实现 */类名::类名(){}

【注意】纯虚析构和纯虚函数不同,纯虚函数不需要实现,但纯虚析构仍需要实现。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。