1. 摘要
- 讲解C++中虚函数的实现机制,主要是Vptr和Vtbl的讲解,有了虚函数才可以拥有像多态这种强大的功能。
- 虚函数主要是出现在类的继承体系中。
2.虚指针vptr和虚表vtbl
虚指针及虚表的概念(来自参考资料5)
首先要清楚,所谓指针其实质就是一个内存地址值,形如0x12345678;其次,要知道,函数名本身就是一个地址。
虚指针:其实就是一个地址值,以该地址为起始地址的一片内存单元存放着各虚函数的入口地址,这一片内存单元合起来就称为虚函数表(想象一下:一片内存单元存着许多函数地址,想执行哪个虚函数就来这片内存单元查找该虚函数的入口地址,就像查表一样,故称虚函数表)。经过以上解释,可以发现,所谓虚指针,就是个指向指针的指针。
2.1. 从上图可以看出:
- 基类和派生类的第一个条目存放的是虚指针,虚指针指的是虚表vtbl的起始地址,虚表中存放的是虚函数的入口地址,可以通过函数指针获取到对应的函数去执行。
- 相关的代码见下节。
3. 相关代码
#include <iostream>
No virtual class/
class Animal {
public:
void eat() {
std::cout << "I'm eating generic food." << std::endl;
}
void shout() {
std::cout << "I'm shouting genericly." << std::endl;
}
};
class Cat : public Animal {
public:
void eat() {
std::cout << "I'm eating a rat." << std::endl;
}
void shout() {
std::cout << "I'm shouting with meow" << std::endl;
}
};
void distinguish(Animal* obj) {
obj->eat();
obj->shout();
}
class with virtual/
class Animal_V {
public:
virtual void eat() {
std::cout << "I'm eating generic food." << std::endl;
}
virtual void shout() {
std::cout << "I'm shouting genericly." << std::endl;
}
virtual ~Animal_V() = default;
};
class Cat_V : public Animal_V {
public:
void eat() override {
std::cout << "I'm eating a rat." << std::endl;
}
void shout() override {
std::cout << "I'm shouting with meow" << std::endl;
}
~Cat_V() override = default;
};
void distinguish(Animal_V* obj) {
obj->eat();
obj->shout();
}
int main(int argc, char* argv[]) {
auto *animal = new Animal;
auto *cat = new Cat;
std::cout << "------------------Normal use------------------" << std::endl;
animal->eat();
animal->shout();
cat->eat();
cat->shout();
std::cout << "------------------Use same interface without vitual------------------" << std::endl;
distinguish(animal);
distinguish(cat);
std::cout << "==================Divider==================" << std::endl;
auto *animal_V = new Animal_V;
auto *cat_V = new Cat_V;
std::cout << "------------------Normal use------------------" << std::endl;
animal_V->eat();
animal_V->shout();
cat_V->eat();
cat_V->shout();
std::cout << "------------------Use same interface with vitual------------------" << std::endl;
distinguish(animal_V);
distinguish(cat_V);
//由于sizeof(your class)会涉及到内存对齐,所以得到的字节数可能不是你想的数字,比如int a,b;和int a;可能都是占8个字节。
std::cout << "------------------Compare sizeof------------------" << std::endl;
std::cout << "Animal'size is: " << sizeof(Animal) << std::endl;
std::cout << "Cat'size is: " << sizeof(Cat) << std::endl;
std::cout << "Animal_V'size is: " << sizeof(Animal_V) << std::endl;
std::cout << "Cat_V'size is: " << sizeof(Cat_V) << std::endl;
std::cout << "------------------The address of virtual function------------------" << std::endl;
std::cout << "Animal_V::eat -> " << (void*)(&Animal_V::eat) << std::endl;
std::cout << "Animal_V::shout -> " << (void*)(&Animal_V::shout) << std::endl;
std::cout << "Cat_V::eat -> " << (void*)(&Cat_V::eat) << std::endl;
std::cout << "Cat_V::shout -> " << (void*)(&Cat_V::shout) << std::endl;
std::cout << "------------------The address of vptr&vtbl------------------" << std::endl;
Animal_V obj;
std::cout << "VPTR's address:" << (long*) (&obj+0) << std::endl; //由于我的电脑是64位系统,指针为8个地址,所以使用long类型的指针获取虚指针以及下面的虚表
std::cout << "VTBL's address:" << (long*) (*(long*)(&obj+0)) << std::endl;
std::cout << "The entry address of the first virtual function: " << (void*) (*((long*)*(long*)(&obj+0)+0)) << std::endl;
std::cout << "The entry address of the second virtual function: " << (void*) (*((long*)*(long*)(&obj+0)+1)) << std::endl;
// long* vptr_addr = (long*) &obj;
// long* vtbl_addr = (long*) *vptr_addr;
// std::cout << "The entry address of the first virtual function: " << (long*) *(vtbl_addr + 0) << std::endl;
// std::cout << "The entry address of the second virtual function: " << (long*) *(vtbl_addr + 1) << std::endl;
std::cout << "------------------Invoke virtual function by pFun------------------" << std::endl;
typedef void(*pFun)(void);
pFun Fun = nullptr;
Fun = reinterpret_cast<pFun>(*((long*)*(long*)(&obj + 0) + 0));
Fun();
Fun = reinterpret_cast<pFun>(*((long*)*(long*)(&obj + 0) + 1));
Fun();
delete animal;
delete cat;
delete animal_V;
delete cat_V;
return 0;
}
程序的输出结果为:
------------------Normal use------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating a rat.
I'm shouting with meow
------------------Use same interface without vitual------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating generic food.
I'm shouting genericly.
==================Divider==================
------------------Normal use------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating a rat.
I'm shouting with meow
------------------Use same interface with vitual------------------
I'm eating generic food.
I'm shouting genericly.
I'm eating a rat.
I'm shouting with meow
------------------Compare sizeof------------------
Animal'size is: 1
Cat'size is: 1
Animal_V'size is: 8
Cat_V'size is: 8
------------------The address of virtual function------------------
Animal_V::eat -> 0x4018c6
Animal_V::shout -> 0x4018f2
Cat_V::eat -> 0x40191e
Cat_V::shout -> 0x40194a
------------------The address of vptr&vtbl------------------
VPTR's address:0x7ffcbb9e5bc8
VTBL's address:0x401e40
The entry address of the first virtual function: 0x4018c6
The entry address of the second virtual function: 0x4018f2
------------------Invoke virtual function by pFun------------------
I'm eating generic food.
I'm shouting genericly.
可以看出使用virtual后即使使用的是同一个接口,会根据对象的不同自动变换,对于各自的接口会在虚表中找到函数的入口地址,这种多态的方式叫“动态绑定”。
注意:多态的实现是通过指针和引用;而对象的转换只会造成对象切割,不能实现多态。