C++多态和虚函数的实现原理
一、关于多态
多态是C++中的重要内容,多态的意思是可以使用父类指针指向子类,并通过这个指针调用子类的函数。
多态可以分为两种形式:
- 编译时多态,主要指泛型编程。
- 运行时多态,主要指虚函数和子类继承。
一般我们讨论的都是运行时多态,运行时多态的几个基本条件为:
- 继承和虚函数。
- 父类对象指向子类对象。
多态形成的原理就是vptr
指针和vtable
虚函数表,当类中有虚函数时,编译器会自动生成虚函数表vtable,这个表属于这个类,所有类实例共享。同时还成一个vptr
指针指向这个虚函数表,执行调用的时候,先通过vptr指针找到自己的虚函数表,然后执行表中的函数。
二、证明vptr指针存在
多态形成的必要条件是虚函数和继承,当两者其中之一都不存在的时候,编译器不会形成多态,因此也不会生成vptr
指针。只有两者同时存在的时候,编译器才会生成vptr
指针。
以下代码通过类的大小来证明了vptr
指针的存在:
#include <iostream>
using namespace std;
class A {
public:
A() {};
~A() {};
void print() { cout << "AAAAAAAAAA" << endl; };
};
class B : public A {
public:
B() {};
~B() {};
void print() { cout << "BBBBBBBBBB" << endl; };
};
int main() {
cout << "sizeof(A): " << sizeof(A) << endl;
cout << "sizeof(B): " << sizeof(B) << endl;
return 0;
}
C++对空类会分配一个字节的内存,此时的输出A和B的大小都是1:
sizeof(A): 1
sizeof(B): 1
将A中的print()
函数设置为虚函数后:
virtual void print() { cout << "AAAAAAAAAA" << endl; };
两个结构体的大小会变成8,因为添加虚函数后,类中会生成vptr
指针,64位系统指针是四个字节,所以A和B的大小就变成了8。
三、vptr指针的工作原理
3.1 vptr和vtable
在创建一个含有虚函数的类时,系统会为每个类生成虚函数表,存放了当前对象所有的函数列表,而vptr
指针就指向这个表的地址。子类继承父类后,也会生成一个属于自己的虚函数表和vptr
指针,如果子类有重写父类的虚函数,虚函数表中的函数地址就是自己类中的成员函数。执行子类调用时,先通过vptr指针定位到对应的虚函数表,然后执行对应的函数。
以上是一个图形示例,类B
继承了类A
,类B重写了f1
和f2
函数,它的函数表中f1
和f2
都指向自己的成员函数。而f3
并未继承,所以指向父类。因此执行子类函数时,f1
和f2
都是用的自己的成员函数。
关于静态联编和动态联编
说到多态肯定免不了要提到动态联编,动态联编就是指程序在运行时才能决定的运行片段,例如if
和switch
代码段,它们只有在运行时才能知道下一步是什么,走哪个代码代码段。多态也是如此,运行到了虚函数的时候才能决定执行哪个函数。而静态联编就是指程序在编译阶段就能决定的事情,就像main
函数,编译后就固定从它开始执行。
3.2 vptr指针绑定的顺序
子类中vptr指针的值的绑定顺序:
- 运行父类构造函数时,先指向父类的虚函数表。
- 运行子类构造函数时,再把指针指向子类的虚函数表。
通过以下代码和GDB调试器可以验证这个观点:
#include<iostream>
using namespace std;
class A {
public:
A() {
};
~A() {};
virtual void print() { cout << "AAAAAAAAAA" << endl; };
};
class B : public A {
public:
B() {
};
~B() {};
void print() { cout << "BBBBBBBBBB" << endl; };
};
int main() {
A a;
B b;
return 0;
}
带调试模式编译:
g++ main.cpp -g -o app -std=c++11
使用GDB调试执行,先在父类构造函数中添加断点:
子类构造函数中也添加断点:
输入run
执行,执行到第一个断点,是正在构造a对象的时候,此时打印出A的vptr指针地址:
继续往下执行,下一个断点依旧是在类A的构造函数处,此时正在构造对象b,因为B继承了A,所以先执行A的构造函数。此时打印出b的vptr指针地址,可以看到b对象的vptr指针和a对象中的vptr指针地址一样:
再继续执行,走到类B的构造函数,此时对象b的vptr指针就被修改成了类B自己的虚函数表地址了:
过程图示:
第一步,执行父类构造函数时,指向父类的虚函数表。
第二步,执行子类自己的构造函数时,再指向子类的虚函数表。
此处评论已关闭