C++深入之虚函数、虚继承与带虚函数的多基派生问题

基础

在讲解带虚函数的多基派生问题时,我们要先弄清楚不带虚函数的多基派生存在什么样的问题,这样才好弄明白带虚函数的多基派生问题。

多基派生的二义性问题

一般来说,在派生类中对基类成员的访问应当具有唯一性,但在多基继承时,如果多个基类中存在同名
成员的情况,造成编译器无从判断具体要访问的哪个基类中的成员,则称为对基类成员访问的二义性问
题。如下面的例子,我们先定义3个不同的类A、B、C,这3个类中都有一个同名成员函数print,然后让
类D继承自A、B、C,则当创建D的对象d,用d调用成员函数print时,发生编译错误。

示例代码:

class A{
public:
	void print(){
		cout << "A::print()" << endl;
	}
};

class B{
public:
	void print(){
		cout << "B::print()" << endl;
	}
};

class C{
public:
	void print(){
		cout << "C::print()" << endl;
	}
};

class D
: public A
, public B
, public C
{

};


void test(){
	D d;
	d.print();//error,编译器无法判断其调用的是哪个基类中的print函数
	//解决办法:加作用域限定符
	d.A::print();//ok
	d.B::print();//ok
	d.C::print();//ok
}

有了这个前提之后再来看带上虚函数的多基派生问题的分析。

带虚函数的多基派生问题分析

先来看一段代码:

#include <iostream>

using namespace std;

//A类拥有三个虚函数:a、b、c
class A{
public:
	virtual
		void a(){
			cout << "A::a()" << endl;
		}
	virtual
		void b(){
			cout << "A::b()" << endl;
		}
	virtual
		void c(){
			cout << "A::c()" << endl;
		}
};

//B类用有两个虚函数a和b,两个非虚函数c和d
class B{
public:
	virtual
		void a(){
			cout << "B::a()" << endl;
		}
	virtual
		void b(){
			cout << "B::b()" << endl;
		}
	
		void c(){
			cout << "B::c()" << endl;
		}
		void d(){
			cout << "B::d()" << endl;
		}
};

//类C继承A和B,其有一个虚函数a,两个非虚函数c和d
class C: public A,public B{
public:
	virtual
		void a(){
			cout << "C::a()" << endl;
		}
	
		void c(){
			cout << "C::c()" << endl;
		}
		void d(){
			cout << "C::d()" << endl;
		}
};

void test(){
	C c; //栈对象
	A *pa = &c;
	//问题出现了:下面这三句代码,执行的是类A中的函数还是类C中的函数呢?
	pa->a();//这个毫无疑问,是多态机制,因为C重写了虚函数a,所以是类C中的函数a
	pa->b();//派生类没有重写虚函数b的话,那么调用的虚函数b就是属于基类型类A中的函数b
	/*对于函数c而言,比较难判断,因为A中的c函数是虚函数,而B中的c函数是非虚函数
	 * 且类C同时继承了类A和类B,意思就是类C同时有一份虚函数c和一份非虚函数c
	 * 这里其实调用的是类C中的c函数,因为它既没有重写A中的虚函数,又隐藏了B中的非虚函数
	 * 那不就只能调用到C中的c函数了*/
	pa->c();

	cout << endl;
	B* pb = &c;
	pb->a();//调用的是类C中的函数a,因为重写了虚函数a
	pb->b();//调用的是类B中的函数b,因为派生类C没有重写该虚函数
	pb->c();//因为函数c并非虚函数,所以调用的还是基类B中的函数b
	pb->d();//同上

	cout << endl;
	C* pc = &c;
	pc->a();//调用的是类C中的函数a
	//pc->b(); error
	/*这里的b函数会产生二义性,因为一份是类A中的b函数一份是类B中的b函数
	 * 产生了二义性,说明带虚函数的多基继承依然存在二义性问题,所以报错
	 * 正确写法依然还是像之前说的,使用作用域限定符即可,如下
	 * */
	pc->A::b();
	pc->B::b();
	/*这里的函数c也很难判断,因为类A中的是虚函数,类B中的是却是非虚函数
	 * 同时类C当中也有一个非虚函数c,这里直接无脑是类C中的非虚函数c就行了
	 * 这里的情况和上面pa->c()是一样的,类C中的c函数既没有重写A中的虚函数
	 * 又隐藏了类B中的非虚函数,那就只能调用到类C中的c函数本身了*/
	pc->c();
	pc->d();//类B中的d函数直接被隐藏了,所以这里是类C中的d函数
}

int main(){
	test();
	return 0;
}


分析的情况都在代码的注释中了,请好好研读。

运行结果:
在这里插入图片描述
从图中可以看出,结果验证了我们的猜想。

从内存布局的层面进行分析

虚函数的底层实现

简单来说,就是通过一张虚函数表(Virtual Fucntion Table)实现的。具体地讲,当类中定义了一个虚函数后,会在该类创建的对象的存储布局的开始位置多一个虚函数指针(vfptr),该虚函数指针指向了一张虚函数表,而该虚函数表就像一个数组,表中存放的就是各虚函数的入口地址。如下图

在这里插入图片描述

当一个基类中设有虚函数,而一个派生类继承了该基类,并对虚函数进行了重定义,我们称之为覆盖(override). 这里的覆盖指的是派生类的虚函数表中相应虚函数的入口地址被覆盖。

那么虚函数机制是如何被激活的呢,或者说动态多态是怎么表现出来的呢?从上面的例子,可以得出结论:

  1. 基类定义虚函数
  2. 派生类重定义(覆盖、重写)虚函数
  3. 创建派生类对象
  4. 基类的指针指向派生类对象
  5. 基类指针调用虚函数

多基派生的底层实现

从代码可以知道,我们先讨论三个类各自的情况,先不谈多基继承的问题:

类A有三个虚函数a、b和c,对应上图,因为类A有虚函数,所以其会产生一张虚函数表,里面存放的是类A的三个虚函数的入口地址,而类A的内存地址空间的第一个位置存放虚函数表指针,其指向该表。

类B有三个两个虚函数a和b,因此类B也会具有一个虚函数表,其内存放的是类B的两个虚函数的入口地址,虚函数指针vfptr指向其虚表。

而类C也有一个虚函数a,因此其也会有一张虚表,存放其虚函数的入口地址,情况如下:
在这里插入图片描述

没问题吧?我们继续往后讨论,现在我们加入代码中继承的情况,即类C继承了类A和类B的情况:

在类C的内存地址空间中,因为继承关系,类C会继承得到类A中的三个虚函数和类B中的两个虚函数以及两个非虚函数(即继承得到类A和类B的两张虚表),因此地址开头的两块空间被用来存放了类A和类B的虚函数指针。所以从图中可以看到,因为类C重写了虚函数a,所以其覆盖了基类A中的虚函数a的地址,而C中并重未写函数b,所以在类A的虚表中函数b还是属于类A的(即调用的是类A中的函数b),函数c则因为没有被类C重写因此是属于类C的(调用都是类C的函数c)。

同理在类B的虚函数表中,类C重写了函数a,因此覆盖了类B中虚表里虚函数a的入口地址,所以调用的是类C的函数c,而b函数没被重写,因此依然属于类B。
在这里插入图片描述

这样的分析要比之前的代码中的注释应该要好理解一些吧。

这就是多基派生的底层实现原理。

番外:虚拟继承

两个概念:

虚拟继承是指在继承定义中包含了virtual关键字的继承关系。虚基类是指在虚继承体系中的通过virtual
继承而来的基类。

语法格式如下:

class Baseclass;
class Subclass
: public/private/protected virtual Baseclass{
public:
	//...
private:
	//...
protected:
	//...
};
//其中Baseclass称之为Subclass的虚基类, 而不是说Baseclass就是虚基类

来举个例子加以说明,先来看一段代码,代码逻辑是类C继承了类B,类B继承了类A,三个类各自有一个成员变量,在main函数中初始化类C对象然后传入了三个值:

#include <ctime>
#include <iostream>

using namespace std;

class A{
	public:
		A(){
			cout << "A()" << endl;
		}

		A(int ia):_ia(ia){
			cout << "A(int)" << endl;
		}
	protected:
		int _ia;
};

class B: public A{
	public:
		B(){
			cout << "B()" << endl;
		}
		B(int ia,int ib):A(ia),_ib(ib){
			cout <<  "B(int,int)" << endl;
		}
	protected:
		int _ib;
};

class C:public B{
	public:
		C(){
			cout << "C()" << endl;
		}
		C(int ia,int ib,int ic):B(ia,ib),_ic(ic){
			cout << "C(int,int,int)" << endl;
		}

		void show() const{
			cout << " ia = " << _ia << endl
				<< "ib = " << _ib << endl
				<< "ic = " << _ic << endl;
		}
	protected:
		int _ic;
};

int main(){
	C c(10,20,30);
	c.show();
	return 0;
}

其运行结果:
在这里插入图片描述
当我们将类B继承类A改成虚拟继承时:

class B: public virtual A{
	public:
		B(){
			cout << "B()" << endl;
		}
		B(int ia,int ib):A(ia),_ib(ib){
			cout <<  "B(int,int)" << endl;
		}
	protected:
		int _ib;
};

此时运行结果截然不同:
在这里插入图片描述
可以看到ia变成了一个随机值,为什么?

细心对比的话,我们可以发现第一次调用时构造函数调用的是有参构造函数A(int),而第二次调用的则是无参构造函数A();

我们明明显式调用了A(int),却在使用了虚拟继承之后就成调用无参构造函数了(相当于没调用到A(int)),从这一点可以看出,派生类B并不负责虚基类A的数据成员的初始化。

那么谁来初始化虚基类A的数据成员呢?我们来将类C进行改写:

class C:public B{
	public:
		C(){
			cout << "C()" << endl;
		}
		//改写位置
		C(int ia,int ib,int ic):A(ia),B(ia,ib),_ic(ic){
			cout << "C(int,int,int)" << endl;
		}

		void show() const{
			cout << " ia = " << _ia << endl
				<< "ib = " << _ib << endl
				<< "ic = " << _ic << endl;
		}
	protected:
		int _ic;
};

运行结果如下:
在这里插入图片描述
可以发现,虚基类A成员变量的初始化是由继承体系中的最后一个类来负责初始化的。

为什么?

在 C++ 中,如果继承链上存在虚继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造。即我们需要显式调用虚基类的构造函数来完成初始化,如果不显式调用,则编译器会调用虚基类的缺省构造函数,不管初始化列表中次序如何,对虚基类构造函数的调用总是先于普通基类的构造函数。如果虚基类中没有定义的缺省构造函数,则会编译错误。因为如果不这样做,虚基类部分会在存在的多个继承链上被多次初始化。很多时候,对于继承链上的中间类,我们也会在其构造函数中显式调用虚基类的构造函数,因为一旦有人要创建这些中间类的对象,我们要保证它们能够得到正确的初始化。

菱形继承问题

在C++中,菱形继承是指一个类同时继承自两个不同的类,而这两个类又都继承自同一个基类。这种继承结构形成一个菱形的图形,导致了一些潜在的问题,其中最主要的问题是"菱形继承"问题(Diamond Inheritance Problem)。

问题的本质在于,如果不使用虚继承(virtual inheritance),最终派生类会包含两份相同的基类(共享的基类会被重复继承),导致数据冗余和访问冲突。

使用virtual关键字可以解决这个问题,即在派生类对共同的基类使用虚继承。虚继承的作用是确保只有一份共同的基类子对象,而不会出现重复。这样,菱形继承结构中的最终派生类只包含一份共同的基类,从而解决了数据冗余和访问冲突的问题。

代码示例:

class Base {
public:
    // ...
};

class Derived1 : public virtual Base {
public:
    // ...
};

class Derived2 : public virtual Base {
public:
    // ...
};

class FinalDerived : public Derived1, public Derived2 {
public:
    // ...
};

在这个示例中,Derived1和Derived2都使用了virtual继承自Base类。这确保了FinalDerived最终只包含一份Base类的实例,从而解决了菱形继承问题。

菱形继承深入

虚继承主要解决的是共享基类时的数据冗余问题,而不是成员函数的冲突问题。

让我们更深入地理解为什么使用virtual关键字可以解决数据冗余的问题。

在C++中,当一个类使用虚继承时,基类的子对象在派生类中只会有一份实例,而不是像普通继承那样每次都有一份。这是通过在派生类对象中引入虚指针(vpointer)和虚表(vtable)的机制来实现的(参考前文的讲述内存布局的部分)。

让我们看一下使用虚继承的例子:

class Base {
public:
    int data;
};

class Derived1 : public virtual Base {
    // ...
};

class Derived2 : public virtual Base {
    // ...
};

class FinalDerived : public Derived1, public Derived2 {
    // ...
};

在这个例子中,Derived1 和 Derived2 都使用了虚继承,因此它们共享一个虚表和虚指针,指向共同的 Base 子对象。当 FinalDerived 继承这两个虚基类时,由于它们共享相同的虚表和虚指针,最终 FinalDerived 中只包含一份 Base 类的子对象,而不是两份。

如果没有使用虚继承,FinalDerived 将分别继承 Derived1 和 Derived2 中的 Base 类子对象,导致 FinalDerived 中包含两份 Base 类的数据成员,造成了数据冗余。

虚继承的实现涉及到额外的内存结构,包括虚指针和虚表。这些机制确保了在派生类中只有一份共享的基类子对象,从而解决了数据冗余的问题。

关于菱形继承的一点疑惑

上面的深入部分我是问的GPT回答的,但我感觉不太对劲,因为在没有如果基类并不存在虚函数的话,那么虚函数表应该不会存在啊(学艺不精,等俺后面对这些概念更加清晰了再回来补这个坑)…

在有虚函数的基类中用上面的虚函数表理论比较好懂,但是在没有虚函数的基类中我觉得用虚基表的存在来解释这个更好理解:

在C++中,虚基表(Virtual Table,简称vtable)是用于支持多态性(polymorphism)和虚函数(virtual function)的一种机制。虚基表是针对包含虚函数的类层次结构而言的。

在C++中,当一个类包含至少一个虚函数时,编译器会为该类创建一个虚函数表(vtable)。虚函数表是一个数组,其中存储了指向每个虚函数的指针。当一个类派生自另一个类,而这两个类都包含虚函数时,派生类会继承基类的虚函数表,并在其自己的虚函数表中添加新的虚函数或覆盖基类的虚函数。

虚基表(virtual base table)是为了解决C++中的菱形继承问题而引入的。菱形继承指的是一个类同时继承自两个不同路径上的同一个基类,导致基类的实例在派生类中存在多份拷贝。为了解决这个问题,C++引入了虚基类(virtual base class)和虚基表。

虚基表的作用是为了跟踪虚基类的偏移量,确保在派生类中正确访问虚基类的成员。当一个类包含虚基类时,它的虚函数表中会包含一个指向虚基表的指针,虚基表中记录了虚基类的偏移量信息。这样,通过虚基表,派生类可以正确访问基类的成员,避免了菱形继承问题带来的二义性和数据冗余。

总的来说,虚基表是为了支持多继承和解决菱形继承问题而引入的,通过虚基表,C++能够正确地处理包含虚函数和虚基类的类层次结构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/334119.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

国考省考行测:语句排序2刷题

国考省考行测&#xff1a;语句排序2刷题 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能&#xff0c;附带行测和申论&#xff0c;而常规国考省考最重要的还是申论和行测&#xff0c;所以大家认真准备吧&#xff0c;我讲一起屡屡申论和行测的重要知识点…

RabbitMQ 部署与配置[CentOS7]

# RabbitMQ,Erlang 版本包对应 https://rabbitmq.com/which-erlang.html#eol-seriescd /usr/local/src# Erlang下载 # https://github.com/rabbitmq/erlang-rpm/releases https://github.com/rabbitmq/erlang-rpm/releases/download/v23.3.4.5/erlang-23.3.4.5-1.el7.x86_64.rp…

鸿蒙原生应用/元服务开发-延迟任务说明(一)

一、功能介绍 应用退至后台后&#xff0c;需要执行实时性要求不高的任务&#xff0c;例如有网络时不定期主动获取邮件等&#xff0c;可以使用延迟任务。当应用满足设定条件&#xff08;包括网络类型、充电类型、存储状态、电池状态、定时状态等&#xff09;时&#xff0c;将任务…

STM32G4芯片SPI1 CLK管脚AF Mode自动变化为0的问题

1 问题描述 最近在调试SPI Slave程序&#xff0c;遇到一个很奇怪的问题&#xff1a;单步调试时SPI1 CLK管脚AF Mode自动变化为0&#xff1b;但是在管脚初始化时&#xff0c;已经将其配置为5了。 2 问题现象 通过视频可见&#xff1a; STM32G4芯片SPI1 CLK管脚AF Mode自动变化…

写点东西《什么是网络抓取?》

写点东西《什么是网络抓取&#xff1f;》 什么是网络抓取&#xff1f; 网络抓取合法吗&#xff1f; 什么是网络爬虫&#xff0c;它是如何工作的&#xff1f; 网络爬虫示例 网络抓取工具 结论 您是否曾经想同时比较多个网站上同一件商品的价格&#xff1f;或者自动提取您最喜欢的…

win系统环境搭建(十四)——Windows系统下使用docker安装mysql8和mysql5.7

windows环境搭建专栏&#x1f517;点击跳转 win系统环境搭建&#xff08;十四&#xff09;——Windows系统下使用docker安装mysql8和mysql5.7 文章目录 win系统环境搭建&#xff08;十四&#xff09;——Windows系统下使用docker安装mysql8和mysql5.7MySQL81.新建文件夹2.创建…

实战之-Redis商户查询缓存

一、什么是缓存? 前言:什么是缓存? 就像自行车,越野车的避震器 举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样; 同样,实际开发中,系统也需要"避震器",防止过高…

Docker-nacos集群部署

nacos单机模式 先拉取一个mysql docker pull mysql:5.7 定义一个挂载目录 mkdir -p /mysql/{conf,data,script} 配置一个my.cnf放到conf目录下 开启mysql容器 privilegedtrue:使用该参数&#xff0c;container内的root拥有真正的root权限&#xff0c;否则&#xff0c;cont…

48-DOM节点,innerHTML,innerText,outerHTML,outerText,静态获取,单机click,cssText

1.DOM基础 Document Object Module,文档对象模型,window对象,document文档,都可以获取和操作 1)文档节点 2)属性节点(标签内的属性href,src) 3)文本节点(标签内的文字) 4)注释节点 5)元素节点(标签) 2.获取元素节点 2.1通过标签名获取getElementsByTagName() …

LeetCode、374. 猜数字大小【简单,二分】

文章目录 前言LeetCode、374. 猜数字大小【简单&#xff0c;二分】题目及类型思路及代码实现 资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝2W&#xff0c;csdn博客专家、Java领域优质创作者&#xff0c;博客之星、阿里云平台优质作者、专注于Java后端技术领域。 涵盖技…

RabbitMQ安装和使用

简介 RabbitMQ是一套开源&#xff08;MPL&#xff09;的消息队列服务软件&#xff0c;是由LShift提供的一个Advanced Message Queuing Protocol (AMQP) 的开源实现&#xff0c;由以高性能、健壮以及可伸缩性出名的Erlang写成。所有主要的编程语言均有与代理接口通讯的客户端库…

Ubuntu使用QtCreator + CMake 开发C/C++程序

平台 OS: Ubuntu 20.04 cmake: 3.16.3 IDE: Qt Creator 4.11.1 Based on Qt 5.14.1 (GCC 5.3.1 20160406 (Red Hat 5.3.1-6), 64 bit) Built on Feb 5 2020 12:48:30 From revision b2ddeacfb5 Copyright 2008-2019 The Qt Company Ltd. All rights reserved. The program …

【算法】斐波那契数列 [递推,矩阵快速幂]

方法一. 递推 class Solution { public:int fib(int n) {int MOD 1e9 7;if (n < 2) return n;int p 0, q 0, r 1;for (int i 2; i < n; i) {p q;q r;r (p q) % MOD;}return r;} }; 方法二&#xff1a;矩阵快速幂 class Solution { public:const int MOD 1e…

【Docker】Nacos的单机部署及集群部署

一、Nacos的介绍 Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 动态服务发现&#xff1a;Nacos支持DNS与RPC服务发现&#xff0c;提供原生SDK、OpenAPI等多种服务注册方式和DNS、HTTP与API等多种服务发现方式。服务健康监测&#xff1a;Nacos提供…

75.网游逆向分析与插件开发-背包的获取-背包结构与指针的逆向分析

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;物品名称与物品编号的映射关系分析-CSDN博客 通过这个内容以及可以通过物品的id得到一个名字&#xff0c;知道了它的算法&#xff0c;它的算法自己封装好了&#xff0c;我们直接用就好&#xff0c;接…

MySQL 索引(上)

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL-进阶篇 &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现…

智能洗地机哪个牌子好?口碑最好的洗地机

在清洁家务领域&#xff0c;吸尘器标志着清洁用具的转型&#xff0c;随后扫地机器人、蒸汽拖把、洗地机等科技清洁产品相继推出。洗地机因高效清洁表现&#xff0c;销售额迅速上升&#xff0c;成为热门清洁家电之一。这反映了人们在试错中逐渐找到清洁家务的真正方向。在选择清…

Unity中URP下的SimpleLit片元着色器

文章目录 前言一、SimpleLit片元着色器大体框架1、传入 和 返回2、GPU实例化部分3、准备 BlinnPhong 光照模型计算需要的 SurfaceData4、准备 BlinnPhong 光照模型计算需要的 InputData5、进行 BlinnPhong 的计算、雾效颜色混合及透明度计算 二、准备SurfaceData1、SurfaceData…

金银花行业分析:预计未来市场需求量会大幅度提升

银花老根被作为根雕作品艺术品&#xff0c;各种根雕作品惟妙惟肖、栩栩如生。经过艺术加工&#xff0c;废弃的金银花树变成了价格不菲的艺术品&#xff0c;一个笔架&#xff0c;一盆盆景&#xff0c;少则几百元&#xff0c;多则上千、上万元&#xff0c;金银花树变成了“摇钱树…

【计算机网络】HTTP协议以及简单的HTTP服务器实现

文章目录 一、HTTP协议1.认识URL2.urlencode和urldecode3.HTTP协议格式4.HTTP的方法5.HTTP的状态码6.HTTP常见Header7.重定向8.长连接9.会话保持10.基本工具 二、简单的HTTP服务器实现1.err.hpp2.log.hpp3.procotol.hpp4.Sock.hpp5.Util.hpp6.httpServer.hpp7.httpServer.cc8.总…
最新文章