首页 > 编程语言 >C++多态的原理(虚函数指针和虚函数表) --imxiangzi 好好看

C++多态的原理(虚函数指针和虚函数表) --imxiangzi 好好看

时间:2023-06-19 11:46:40浏览次数:80  
标签:虚表 函数 基类 多态 C++ 派生类 函数指针 重写 cout

 

C++多态的原理 (虚函数指针和虚函数表)
1.虚函数指针和虚函数表
2.继承中的虚函数表
2.1单继承中的虚函数表
2.2多继承中的虚函数表
3.多态的原理
4.总结
1.虚函数指针和虚函数表
以下代码:
问类实例化出的对象占几个字节?

#include<iostream>
using namespace std;
class A {
int m_a;
public:
void func() {
cout << "调用类A的func()函数" << endl;
}
};
int main() {
A a;
cout <<"sizeof(a):"<<sizeof(a) << endl;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
结果显而易见 sizeof(a)=4,因为成员函数存放在公共的代码段, 所以只计算成员变量m_a(int型)所占字节的大小。


当我们将成员函数定义为虚函数时,结果却出现了不同的情况:

#include<iostream>
using namespace std;
class A {
int m_a;
public:
virtual void func() {
cout << "调用类A的func()函数" << endl;
}
};
int main() {
A a;
cout <<"sizeof(a):" <<sizeof(a) << endl;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


我们注意到当成员函数定义为虚函数时,同一个类的实例化对象大小变为了8个字节。多出来的4个字节是怎么回事呢?
另外在对象a中还多出了一个void**类型名为_vfptr的变量。它是一个二级指针, 指针在32位平台中占4字节, 所以这里的结果是8(m_a的4字节+_vfptr的4字节), 那么_vfptr到底是个什么东西? 类中有了虚函数之后才有了_vfptr, 它们之间到底有着什么联系?

当一个类中有虚函数时,编译期间就会为这个类分配一片连续的内存 (虚表vftable),来存放虚函数的地址。类中只保存着指向虚表的指针 (虚函数表指针_vfptr) ,当这个类实例出对象时,每个对象都会有一个虚函数表指针_vfptr 。虚函数其实和普通函数一样,存放在代码段。


一个含有虚函数的类中都至少都有一个虚函数表,因为虚函数的地址要被放到虚函数表中,那么派生类中这个表放了些什么呢?我们接着往下分析。

针对上面的代码我们做出以下改造:

1.我们增加一个派生类去继承基类
2.基类中重写Func1
3.派生类再增加一个虚函数Func2和一个普通函数Func3
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

通过观察和测试,我们发现了以下几点问题:

派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在的部分,的另一部分是自己的成员。
基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1()完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖, 覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
Func2()继承下来后是虚函数,所以放进了虚表,Func3()也继承下来了,但是不是虚函数,所以不会放进虚表。
虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
虚函数存在哪的?虚表存在哪的?
答:虚函数存在虚表,虚表存在对象中。上面的回答是错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现在vs下是存在代码段的。
总结:

当一个类中有虚函数时, 在编译期间,就会为这个类分配一片连续的内存 (虚表vftable), 来存放虚函数的地址, 类中只保存着指向虚表的指针 (虚函数指针_vfptr) , 虚函数其实和普通函数一样, 存放在代码段。当这个类实例出对象时, 每个对象都会有一个虚函数表指针_vfptr 。虚表本质上是一个在编译时就已经确定好了的void* 类型的指针数组 。
注意 : 虚函数表为了标志结尾, 会在虚表最后一个元素位置保存一个空指针。所以看到的虚表元素个数比实际虚函数个数多一个。
2.继承中的虚函数表
在有虚函数的类被继承后, 虚表也会被拷贝给派生类。编译器会给派生类新分配一片空间来拷贝基类的虚表, 将这个虚表的指针给派生类, 而并不是沿用基类的虚表。在发生虚函数的重写时, 重写的是派生类为了拷贝基类虚表新创建的虚表中的虚函数地址。 虚表为所有这个类的对象所共享,是通过给每个对象一个虚表指针_vfptr共享到的虚表。

2.1单继承中的虚函数表
单继承中未重写虚函数: 会继承基类的虚表, 如果派生类中新增了虚函数, 则会加继承的虚表后面。
单继承中重写虚函数: 继承的虚表中被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址。(注意: 此时基类的虚表并没有被修改, 修改的是派生类自己的虚表)
所以, 重写实际上就是在继承基类虚表时, 把基类的虚函数地址修改为派生类虚函数的地址。

#include<iostream>
using namespace std;

class Base {
int m_a;
public:
virtual void func() {
cout << "类A的func" << endl;
}
virtual int func1() {
cout << "类A的func1" << endl;
return 0;
}
};
class Derive :public Base {
public:
virtual void func() {
cout << "类B的func" << endl;
}
virtual void func2() {
cout << "类B的func2" << endl;
}
};
int main() {
Base a1;
Base a2;
Derive b;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
基类对象a1,a2中,虚表中的地址相同(虚函数func()和func1()的地址),是因为虚表为类的所有对象共享,是通过给每个对象一个虚表指针_vfptr共享到的虚表。
派生类对象b,继承了基类的虚表,虚函数指针_vptr却和a1,a2的不同,这是因为编译器新分配了一片空间来拷贝基类的虚表。派生类中重写了虚函数func(),由于被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址。所以派生类的虚表中func()的地址被改变了。

我们还发现,派生类中的虚函数func2()却没有出现在派生类中的虚表中。按理来说, 如果派生类中新增了虚函数, 则会加继承的虚表后面。其实这个虚函数地址是存在的,我们可以发现箭头所指的虚函数表vftable[4],其中应该有四个元素,除去虚表中多出的一个空指针,还有另外三个func(),func1(),func2(),只不过这里没有显示func2()。我们可以通过调用监视窗口来查看func2()。


2.2多继承中的虚函数表
多继承中不重写虚函数: 继承的多个基类中有多张虚表, 派生类会全部拷贝下来, 成为派生类的多张虚表, 如果派生类有新的虚函数, 会加在派生类拷贝的第一张虚表的后面(拷贝的第一张虚表是继承的第一个有虚函数或虚表的基类的)
多继承中重写虚函数 : 规则与不重写虚函数相同, 但需要注意的是, 如果多个基类中含有相同的虚函数, 例如func(),当派生类重写func()这个虚函数后, 所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表, 并不是基类自己的虚表)
#include<iostream>
using namespace std;
class Base {
int m_a;
public:
virtual void funcA() {
cout << "基类Base的funcA()" << endl;
}
virtual void func() {
cout << "基类Base的func()" << endl;
}
};
class Base2 {
public:
virtual void funcB() {
cout << "基类Base2的funcB()" << endl;
}
virtual void func() {
cout << "基类Base2的func()" << endl;
}
};
class Derive :public Base, public Base2 {
public:
virtual void func() {
cout << "派生类重写的func()" << endl;
}
virtual void funcC() {
cout << "派生类中新增的虚函数funcC()" << endl;
}
};
int main() {
Derive d;
Base2 c;
Base a;
system("pause");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
派生类继承了两张虚表。派生类中重写了func()函数,派生类自己拷贝基类虚表中含func()的地址都被改变了。

第一张虚表vftable[4],说明其中含有四个元素,除了funcA()、func()、多出来的一个空指针外,还有派生类中新的虚函数funcC()。调用监视窗口可以看到,派生类中新增的虚函数funcC()被加在了派生类拷贝基类的第一张虚表的后面。


3.多态的原理
多态的构成条件:

通过基类对象的指针或者引用调用虚函数
基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。
原理: 利用虚函数可以重写的特性, 当一个有虚函数的基类有多个派生类时, 通过各个派生类对基类虚函数的不同重写, 实现指向派生类对象的基类指针或基类引用调用同一个虚函数, 去实现不同功能的特性。抽象来说就是, 为了完成某个行为, 不同的对象去完成时会产生多种不同的状态。

4.总结
当一个类中有虚函数时, 在编译期间,就会为这个类分配一片连续的内存 (虚表vftable), 来存放虚函数的地址。
对象中存放的是虚函数指针_vfptr,并非虚表。_vptr是虚表的首地址,指向虚表。
虚表中存放的是虚函数地址,不是虚函数。虚函数和普通函数一样存放在代码段。
虚表是在编译阶段生成的,一般存放在代码段中。
虚表本质上是一个在编译时就已经确定好的void* 类型的指针数组 。
派生类的虚表生成:
单继承:
①先将基类中的虚表内容拷贝一份到派生类虚表中。
②如果派生类重写了基类中某个虚函数,派生类中的虚函数地址替换虚表中基类的虚函数地址。
③派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
多继承:
①继承的多个基类中有多张虚表, 派生类会全部拷贝下来, 成为派生类的多张虚表 。
②如果派生类重写了基类中的某个虚函数,所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表, 并不是基类自己的虚表)。
③派生类自己新增加的虚函数加在派生类拷贝的第一张虚表后
————————————————
原文链接:https://blog.csdn.net/imp0128/article/details/107141021/

 

标签:虚表,函数,基类,多态,C++,派生类,函数指针,重写,cout
From: https://www.cnblogs.com/im18620660608/p/17490754.html

相关文章

  • c++11新特性之线程相关所有知识点
    c++11关于并发引入了好多好东西,这里按照如下顺序介绍:std::thread相关std::mutex相关std::lock相关std::atomic相关std::call_once相关volatile相关std::condition_variable相关std::future相关async相关std::thread相关c++11之前你可能使用pthread......
  • C++ 高级开发者需要掌握的10个特性
    C++正在快速向前发展,所以想要紧跟其脚步并不是一件容易的事。我们在之前的文章中讨论过这个问题,讨论了C++的演变以及如何实现遗留C++代码现代化。在这篇文章中,我们将重点介绍经验丰富的C++开发人员可以跟上的高级主题列表。我们将尝试涵盖我们认为相关的内容,而不限于特定......
  • Java多态
    Java多态多态基础Java多态,多态是面向对象特征之一,它允许我们以不同的方式执行单个动作。例如,假设我们有一个Animal类,它有一个方法sound()。由于这是一个泛型类,所以我们不能给它一个实现,如:汪汪,喵喵等。我们不得不给出一个通用的消息。publicclassAnimal{...pub......
  • 介绍一个C++奇巧淫技
    你能实现这样一个函数吗:MyTypetype;HisTypehtype;serialize_3(11,type,htype);serialize_4(type,htype,type,htype);serialize_4(11,type,htype,htype);参数类型自由,个数自由,怎么做呢?往下看:[xiaochu.yh@OBmacro]$catauto_type.cpp/**(C)1999-2013......
  • 《C++》多态
    多态多态分为两种:静态多态:函数重载和运算符重载属于静态多态,复用函数名动态多态:派生类和虚函数实现运行时多态静态多态函数地址早绑定--编译阶段确定函数地址动态多态函数地址晚绑定--运行阶段确定函数地址virtual  //修饰虚函数,使之变为动态多态特点代码结构清晰可读性强......
  • C++常用数据结构
    数据结构1.线性表由n个具有相同性质的数据元素1.1顺序表(数组)定义:用一组地址连续的存储单元依次存储线性表中每个数据元素特点:逻辑关系相邻的两个元素在物理位置上也相邻#c++实现template<typenameT>classsqlist{public:sqlist(intmaxsize=10):Maxsize(......
  • 现代C++学习指南-类型系统
    在前一篇,我们提供了一个方向性的指南,但是学什么,怎么学却没有详细展开。本篇将在前文的基础上,着重介绍下怎样学习C++的类型系统。写在前面在进入类型系统之前,我们应该先达成一项共识——尽可能使用C++的现代语法。众所周知,出于兼容性的考虑,C++中很多语法都是合法的。但是随着新......
  • #yyds干货盘点#C++命名空间
    命名空间命名空间是C++语言的新特性,它能够解决命名冲突问题。例如,小明定义了一个函数swap(),C++标准程序库中也存在一个swap()函数。此时,为了区分调用的是哪个swap()函数,可以通过命名空间进行标识。C++中的命名空间包括两种,具体介绍如下。usingnamespacestd;1.标准命名空间std是C+......
  • C++17特性
    构造函数模板推导在C++17前构造一个模板类对象需要指明类型:pair<int,double>p(1,2.2);//beforec++17C++17就不需要特殊指定,直接可以推导出类型,代码如下:pairp(1,2.2);//c++17自动推导vectorv={1,2,3};//c++17结构化绑定1.获取值//绑定tuplestd::tupl......
  • C++练习题
    多态判断Q1:虚函数可以是内联的?A1:错误。内联是编译时刻决定的,虚函数是运行时刻动态决定的,所以虚函数不能是内联函数。虚函数前加上inline不会报错,但是会被忽略。Q2:一个类内部,可以同时声明staticvoidfun()和virutalvoidfun()两个函数?A2:错误。虽然静态函数......