在C++中可以使用子类来覆写基类的虚函数,然后并使用基类的指针指向子类的实例,但可以通过基类的指针去调用到子类所覆写的虚函数,也就是C++中的动态绑定
下面举一个例子
#include <iostream>
class Base{
public:
Base(int a):a(a){}
virtual void print(){
std::cout << "Base::print()" << std::endl;
}
virtual ~Base(){
std::cout << "Base::~Base()" << std::endl;
}
private:
int a;
};
class Derived: public Base{
public:
Derived(int a, int b):Base(a),b(b){}
void print() override{
std::cout << "Derived::print()" << std::endl;
}
~Derived(){
std::cout << "Derived::~Derived()" << std::endl;
}
private:
int b;
};
int main(){
Base* b = new Derived(1,2);
b->print();
delete b;
return 0;
}
上面函数执行的结果是
Derived::print()
Derived::~Derived()
Base::~Base()
不难看出我们所调用的是子类所重写的print函数,那么为什么会出现这种情况呢?这是因为在C++中存在虚函数指针和虚函数表的存在,可以在运行时动态访问函数。
虚函数表指针是什么,虚函数表又是什么,它们之间的关系又是什么?下面我们一点点进行说明。
虚函数表指针,顾明思议就是指向虚函数表的指针,那么这个指针在哪里呢?其实当一个类中存在virtual关键字声明定义的函数时,虚函数指针和虚函数表在编译时就存在了,所谓的虚函数表指针也被作为了类的一个成员,只不过我们没有办法访问?为什么这么说,我们看下面一个例子
#include <iostream>
//我们在定义一个这样的类
class A{
public:
A(int a, int b):a(a),b(b){}
~A(){
std::cout << "A::~A()" << std::endl;
}
private:
int a;
int b;
};
int main(){
std::cout << "A sizeof: " << sizeof(A) << std::endl;
std::cout << "Derived sizeof: " << sizeof(Derived) << std::endl;
}
执行结果如下
A sizeof: 8
Derived sizeof: 16
我们可以看出Derived比A的大小大了8个字节,这通常在64位系统中就是一个指针大小,所以说当有virtual时会有一个虚函数表指针成员。
我们在看一下下面的例子
std::cout << "A sizeof: " << sizeof(A) << std::endl;
std::cout << "Base sizeof: " << sizeof(Base) << std::endl;
std::cout << "Derived sizeof: " << sizeof(Derived) << std::endl;
这个输出的结果是
A sizeof: 8
Base sizeof: 16
Derived sizeof: 16
是不是感觉到有点疑惑?为什么基类的大小会与子类的大小一样呢,这是因为计算机的对齐要求,因为这个会导致编译器会在其后增加4个字节进行对齐,这样就能看到这个情况了,那么你是否还有点疑惑呢,那么我们可以在文件类的声明前后分别加入这么一行代码
//声明前加
#pragma pack(push, 1)
//声明后加
#pragma pack(pop)
然后重新运行程序,结果如下
A sizeof: 8
Base sizeof: 12
Derived sizeof: 16
现在是不是就清楚了,那么我们就再讨论一下虚函数表是什么,虚函数表实际上就是存放虚函数的函数指针的一个指针数组,每一个类只有一张虚函数表,它被放在代码段,每当一个类的对象被声明定义时,其虚函数表指针指向的就是该类的虚函数表的位置。
像上面一个例子
//A的虚函数表
[0x3F6ECC]
//B的虚函数表
[0x3F6FFF]
//A的虚函数print在代码段的位置
0x3F6ECC void print
//B的虚函数的位置
0x3F6FFF void print
基本类似于上面的样子,通过虚函数表指针查找到虚函数表的位置,通过所调用的函数找到虚函数指针的位置,通过虚函数指针找到虚函数,然后进行调用函数。
这样就实现了动态绑定
虚函数表的创建时机
- 什么时候生成?编译器编译的时候生成,会为具有virtual关键字修饰的函数的类生成一个虚函数表
- 存放在哪里?可执行程序(磁盘),运行状态(内存)