c++ 多态原理详解

Posted 不倒翁*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++ 多态原理详解相关的知识,希望对你有一定的参考价值。

1.虚函数表

首先我们来看一段代码输出为多少?

#include<iostream>
using namespace std;
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base

public:
	virtual void Func1()
	
		cout << "Func1()" << endl;
	
private:
	int _b = 1;
;
int main() 
	Base bs;
	cout << sizeof(Base) << endl;
	return 0;

这上面这段代码是计算一个类的大小,我们知道类的大小就是类里成员变量所占的大小。
该类中只有一个int类型的成员变量,那么该类的大小是否就为4呢?我们运行程序可以看到如下结果:

大小为8个字节。为什么是8呢?我们通过调试窗口来看一下:

我们发现这个类中多了个指针,这个指针我们把它叫做虚函数表指针简称虚表指针,
所以这个类的大小还要加上这个虚表指针的大小,故此类的大小为8.
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
接下来我们来看看下面代码

#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;
;
void Test(Base& bs) 
	bs.Func1();

int main()

	Base b;
	Test(b);
	Derive d;
	Test(d);
	return 0;

这里大家可以想一想为什么bs调用Func1时bs引用的是父类就调用父类的虚函数,引用子类调用的就是子类的虚函数。下面我们通过调试来进一步探究

我们可以发现以下几点问题:
1.派生类对象d中也有一个虚表指针,d的对象由两部分组成,一部分是父类继承下来的成员,虚表指针中也有一部分继承父类的成员(没有被重写的虚函数),也有一部分是自己的成员(重写后的虚函数,以及自己中单独存在的虚函数); Func1的地址发生改变,Func2的地址没有发生改变。

2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

  1. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

  2. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

总结:1.构成多态,指向谁调用的就是谁的虚函数,跟对象有关。
2.不构成多态,对象类型是什么调用的就是那个函数跟类型有关。

思考:为什么必须是父类的指针或者引用调用虚函数时发生多态,不能是对象呢?

这和继承中的切片有关,将子类的对象给父类的对象时会调用拷贝构造函数但是不是全部拷贝,而是只拷贝属于父类的那一部分并不会将虚函数表拷贝过去,这也就是为什么不能是父类的对象。

2.动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

编译时的多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。
运行时的多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或者引用访问派生类中的虚函数。

编译时多态与运行时多态的区别:
1.时期不同:编译时多态发生在程序编译的过程中,运行时多态发生在程序运行过程中。
2.实现方式不同:编译时多态运用泛型编程来实现,运行时多态借助虚函数来实现。

3.虚表是何时初始化的

对象中的虚表是何时初始化?虚表又是在什么阶段生成的呢?

#include<iostream>
using namespace std;
class Base

public:
	Base() 
		cout << "Base()调用" << endl;
	
	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:
	
private:
	int _d = 2;
;
void Test(Base& bs) 
	bs.Func1();

int main()

	Base b;
	Test(b);
	Derive d;
	Test(d);
 return 0;

我们继续调试

在进入初始化列表前我们可以看到虚表并没有初始化,此时我们按一下F10

此时我们可以看到虚表已经初始化了
总结:1.虚表中的指针是在构造函数初始化列表中初始化的。
2.虚表是在编译时就已经生成好了。
3.只有虚函数才会将函数的地址放入虚表中,虚函数和普通函数一样,编译完成后都是放在代码段中

我们知道了虚表中存放的是虚函数的地址,虚函数是存在代码段的,那么虚表是存在哪里的呢?
我们可以通过程序来判断,我们可以将虚表的地址取出来,然后在和这四个区域的地址进行对比,看虚表地址和哪个区域地址最相似。


#include<iostream>
using namespace std;
class Base

public:
	Base() 
		cout << "Base()调用" << endl;
	
	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:
	
private:
	int _d = 2;
;
void Test(Base& bs) 
	bs.Func1();

//定义一个函数指针
typedef void (*Func)();
int main()

	Base b;
	//1.先取出虚表的地址打印
	printf("vftptr:%p\\n", (Func*)(*(int*)(&b)));//对象的前四个字节放的就是虚表
	//打印栈上的地址
	int i = 0;
	printf("栈:%p\\n", &i);
	//打印堆上的地址
	int* p = new int(1);
	printf("堆:%p\\n", p);
	//打印常量区的地址
	const char* ptr = "12323";
	printf("常量区:%p\\n", ptr);
 
 return 0;


我们可以看到虚表的地址与常量区的地址最接近。
写一个程序打印虚表,确认虚表中的调用函数

#include<iostream>
using namespace std;
class Base

public:
	Base() 
		cout << "Base()调用" << endl;
	
	virtual void Func1()
	
		cout << "Base::Func1()" << endl;
	
	virtual void Func2()
	
		
			cout << "Base::Func2()" << endl;
	
	virtual void Func3()
	
		cout << "Base::Func3()" << endl;
	
private:
	int _b = 1;
;
class Derive : public Base

public:
	
private:
	int _d = 2;
;
void Test(Base& bs) 
	bs.Func1();

typedef void (*VFunc)();
void PrintVFT(VFunc* ptr) 
	printf("虚表地址:%p\\n", ptr);
	for (int i = 0; ptr[i]; i++) 
		ptr[i]();
	
	printf("\\n");

int main()

	Base b;
	//1.先取出虚表的地址打印
	//printf("vftptr:%p\\n", (VFunc*)(*(int*)(&b)));
	//对象的前四个字节放的就是虚表
	PrintVFT((VFunc*)(*(int*)(&b)));
 	return 0;

4.多继承中的虚表

#include<iostream>
using namespace std;
class Base1 
public:
	virtual void func1()  cout << "Base1::func1" << endl; 
	virtual void func2()  cout << "Base1::func2" << endl; 
private:
	int b1;
;
class Base2 
public:
	virtual void func1()  cout << "Base2::func1" << endl; 
	virtual void func2()  cout << "Base2::func2" << endl; 
private:
	int b2;
;
class Derive : public Base1, public Base2 
	
public:
	virtual void func1()  cout << "Derive::func1" << endl; 
	virtual void func3()  cout << "Derive::func3" << endl; 
private:
	int d1;
;
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])

	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	
	cout << endl;

int main()

	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;


当发生多继承时,子类中会有多份虚表(有几个父类就有几份)。

并且通过打印可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

以上是关于c++ 多态原理详解的主要内容,如果未能解决你的问题,请参考以下文章

c++ 多态原理详解

C++ 虚函数表及多态内部原理详解

C++ 虚函数表及多态内部原理详解

c++多态及虚函数表内部原理实战详解

学习攻略C++虚函数表及多态内部原理详解

C++多态详解