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

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

  • 1.虚函数指针和虚函数表
  • 2.继承中的虚函数表
    • 2.1单继承中的虚函数表
    • 2.2多继承中的虚函数表
  • 3.多态的原理
  • 4.总结

1.虚函数指针和虚函数表

以下代码:
问类实例化出的对象占几个字节?

#include
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;
}

结果显而易见 sizeof(a)=4,因为成员函数存放在公共的代码段, 所以只计算成员变量m_a(int型)所占字节的大小。
在这里插入图片描述
在这里插入图片描述
当我们将成员函数定义为虚函数时,结果却出现了不同的情况:

#include
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;
}

在这里插入图片描述
在这里插入图片描述
我们注意到当成员函数定义为虚函数时,同一个类的实例化对象大小变为了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
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;
}

C++多态的原理(虚函数指针和虚函数表)_第1张图片
通过观察和测试,我们发现了以下几点问题:

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

总结:

当一个类中有虚函数时, 在编译期间,就会为这个类分配一片连续的内存 (虚表vftable), 来存放虚函数的地址, 类中只保存着指向虚表的指针 (虚函数指针_vfptr) , 虚函数其实和普通函数一样, 存放在代码段。当这个类实例出对象时, 每个对象都会有一个虚函数表指针_vfptr 。虚表本质上是一个在编译时就已经确定好了的void* 类型的指针数组 。
注意 : 虚函数表为了标志结尾, 会在虚表最后一个元素位置保存一个空指针。所以看到的虚表元素个数比实际虚函数个数多一个。

2.继承中的虚函数表

在有虚函数的类被继承后, 虚表也会被拷贝给派生类。编译器会给派生类新分配一片空间来拷贝基类的虚表, 将这个虚表的指针给派生类, 而并不是沿用基类的虚表。在发生虚函数的重写时, 重写的是派生类为了拷贝基类虚表新创建的虚表中的虚函数地址。 虚表为所有这个类的对象所共享,是通过给每个对象一个虚表指针_vfptr共享到的虚表。

2.1单继承中的虚函数表

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

所以, 重写实际上就是在继承基类虚表时, 把基类的虚函数地址修改为派生类虚函数的地址。

#include
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;
}

基类对象a1,a2中,虚表中的地址相同(虚函数func()和func1()的地址),是因为虚表为类的所有对象共享,是通过给每个对象一个虚表指针_vfptr共享到的虚表。
派生类对象b,继承了基类的虚表,虚函数指针_vptr却和a1,a2的不同,这是因为编译器新分配了一片空间来拷贝基类的虚表。派生类中重写了虚函数func(),由于被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址。所以派生类的虚表中func()的地址被改变了。
C++多态的原理(虚函数指针和虚函数表)_第2张图片
我们还发现,派生类中的虚函数func2()却没有出现在派生类中的虚表中。按理来说, 如果派生类中新增了虚函数, 则会加继承的虚表后面。其实这个虚函数地址是存在的,我们可以发现箭头所指的虚函数表vftable[4],其中应该有四个元素,除去虚表中多出的一个空指针,还有另外三个func(),func1(),func2(),只不过这里没有显示func2()。我们可以通过调用监视窗口来查看func2()。
在这里插入图片描述

2.2多继承中的虚函数表

  1. 多继承中不重写虚函数: 继承的多个基类中有多张虚表, 派生类会全部拷贝下来, 成为派生类的多张虚表, 如果派生类有新的虚函数, 会加在派生类拷贝的第一张虚表的后面(拷贝的第一张虚表是继承的第一个有虚函数或虚表的基类的)
  2. 多继承中重写虚函数 : 规则与不重写虚函数相同, 但需要注意的是, 如果多个基类中含有相同的虚函数, 例如func(),当派生类重写func()这个虚函数后, 所有含有这个函数的基类虚表都会被重写 (改的是派生类自己拷贝的基类虚表, 并不是基类自己的虚表)
#include
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;
}

派生类继承了两张虚表。派生类中重写了func()函数,派生类自己拷贝基类虚表中含func()的地址都被改变了。
C++多态的原理(虚函数指针和虚函数表)_第3张图片
第一张虚表vftable[4],说明其中含有四个元素,除了funcA()、func()、多出来的一个空指针外,还有派生类中新的虚函数funcC()。调用监视窗口可以看到,派生类中新增的虚函数funcC()被加在了派生类拷贝基类的第一张虚表的后面。
在这里插入图片描述

3.多态的原理

多态的构成条件:

  1. 通过基类对象的指针或者引用调用虚函数
  2. 基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。

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

4.总结

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

你可能感兴趣的:(C++)