一 前言引入
虚函数在多态中可以说非常重要,因此关于虚函数的底层实现,我们有必要了解。首先我们来看以下代码。
class Animal {
public :
Animal() = default;
Animal(int x, double y) : x(x), y(y) {}
void run() {
cout << "I don't know how to run" << endl;
}
private :
int x;
double y;
};
class Dog : public Animal {
public :
Dog() : Animal() {};
Dog(int x, double y) : Animal(x, y) {}
void run() {
cout << "I can run with four legs" << endl;
}
};
int main() {
cout << "sizeof(Animal): " << sizeof(Animal) << endl;
cout << "sizeof(Dog): " << sizeof(Dog) << endl;
return 0;
}
如代码所示,定义了父类Animal,子类Dog公有继承自Animal类,现在我们输出两个类实例化出来的对象在内存中的大小,代码输出如下:
关于输出结果大小为16字节,以下有几点相关的说明:
- 这个答案显然是正确的,并且这个大小是成员变量在内存中的大小,不包含成员方法在内存中的大小
- 若各位看官不理解为什么对象大小不是4 + 8 = 12个字节,而是16个字节,建议大家上网搜索如何判断结构体的大小
- 对于某一个类,实例化对象时,若对每一个对象都为其成员变量和成员方法分配内存,这样显然是不好的,因为这些从同一个类实例化而来的对象中的成员方法都一样,为此在C++中,每一个对象分配的内存空间在中只有成员变量,而成员方法是另外开辟一片公共的内存空间,所有对象都可以访问并调用其中的方法。
现在我对原来的代码进行一些修改,将run()方法变为虚函数(修改较小,看官可自行操作,这里就不再粘贴代码了),再次运行代码,输出的结果会是什么呢?
我们会发现,此时大小变为24个字节,相对于之前的16个字节增加了8个字节,那为什么会这样呢?是因为引入了一个虚函数,对象所占内存大小就会增加8字节吗?如果真是这样,那在类中再定义一个虚函数,大小是不是会变成30呢?如果不是这样,那是别的什么原因呢?
首先前者猜想肯定是错误的,我们可以在类中多定义几个虚函数,之后输出大小,会发现增加虚函数个数后对象后的大小并没有变化。这样的话,就是说,类中定义虚拟函数后,相比没有定义虚函数,增加的大小恒为8字节。那着8字节会是什么呢?我们平时编程中什么东西的大小是8字节,然后放在此处有些依据呢? 答案就是“指针” ,在64位操作系统中,任意类型的指针变量的大小都是8字节。指针既然是一个地址,那是谁的地址呢?答案就是“虚函数表在内存中的地址”
若存在虚函数时,则对象在内存中头8个字节存储的就是虚函数表的地址,此虚函数表是和类挂钩的,一个类就对应一张虚函数表,换句话说,不管这个类实例化出多少个对象,这些对象在内存中头8个字节的内容都是一致的,都指向虚函数表在内存中的地址。
二 验证虚函数表的存在并调用
知晓虚函数表的存在,那么我们尝试着来验证虚函数表的存在。
- 首先我们来验证,Dog类实例化出来的所有对象,其首部的8个字节的内容是否相同,主要代码如下:
// 从地址q开始,打印向后n个字节的内容
void output_raw_data(void *q, int n) {
unsigned char *p = (unsigned char *)q;
for (int i = 0; i < n; i++) {
printf("%02x ", p[i]); //不足2位的前面补0
}
printf("\n");
}
int main() {
Dog a, b;
output_raw_data(&a, sizeof(Dog));
output_raw_data(&b, sizeof(Dog));
return 0;
}
代码输出如下:
观察输出我们发现对象a和对象b前8个字节的内容是一致的,符合预期猜想。
- 通过对象中的虚函数表地址调用虚函数
首先我们知道,虚函数表中存储的是虚函数在内存中的地址,也就是函数指针,我们假设函数指针的类型为func
,那么指向这个虚函数表的指针类型为func *
,而对象首部8字节中存储的就是虚函数表的地址,所以我们需要将对象的地址转换 为func **
。
这一点要是不好理解的话,可以参考字符串数组:每一个字符的类型是char
,多个字符组成字符串,指向字符串的指针类型为char *
,若干个字符串组合成字符串数组,所以指向字符串数组的指针类型为char **
,虽然两者之间不完全相同,但是作为类比解释还是可以的。关键代码如下:
// 定义函数指针
typedef void (* func)();
int main() {
Dog a, b;
cout << " call virtual function through virtual table: " << endl << endl;
((func **)(&a))[0][0]();
((func **)(&a))[0][1]();
return 0;
}
执行结果如下:
从截图可以看出,显然调用是成功的。
三 this指针做为成员方法的隐藏参数
现在我们修改一下虚函数,向函数中传入一个int类型的参数,并在函数中打印出来,我们试试这样到底行不行。
class Dog : public Animal {
public :
Dog() : Animal() {};
Dog(int x, double y) : Animal(x, y) {}
void run(int x) override {
cout << "this: " << this << endl;
cout << "x: " << x << endl;
cout << "I can run with four legs" << endl;
}
void say() override {
cout << "Wang~ Wang~ Wang~" << endl;
}
};
typedef void (* func)(int);
int main() {
Dog a;
((func **)(&a))[0][0](145);
return 0;
}
如代码所示,run()函数中传入 int x 作为函数参数,在run()函数中输出了this指针和x的值,注意修改函数指针类型。代码输出如下:
我们发现,输出的x的值是错误的,如果我们再算一下 this
指针输出的值,我们会发现9 * 16 + 1 = 145,这不就是在调用函数时,我们传入的参数吗?这是怎么回事?
其实这就是因为,this参数是成员函数的隐藏参数,并且这个参数在参数列表的第一个位置,所以我们传入的参数,实际上是传给了this指针,这个this指针的值显然是错误的,如果我们通过该this值访问对象中的成员变量一定会报错。
为了函数正确执行,需要修改函数指针的类型,同时将this指针(对象的地址)传入即可。
对应代码如下:
class Animal {
public :
Animal() = default;
Animal(int x, double y) : x(x), y(y) {}
virtual void run(int x) {
cout << "I don't know how to run" << endl;
}
virtual void say() {
cout << "I don't know how to say" << endl;
}
private :
int x;
double y;
};
class Dog : public Animal {
public :
Dog() : Animal() {};
Dog(int x, double y) : Animal(x, y) {}
void run(int x) override {
cout << "this: " << this << endl;
cout << "x: " << x << endl;
cout << "I can run with four legs" << endl;
}
void say() override {
cout << "Wang~ Wang~ Wang~" << endl;
}
};
typedef void (* func)(void *, int);
int main() {
Dog a;
cout << "address of a: " << &a << endl;
((func **)(&a))[0][0](&a, 145);
return 0;
}
执行结果如下:
当然,在平时情况,我们通过变量本身(或引用)直接调用或通过指针间接调用 对象的成员函数,直接传参即可,没必要参数列表前面再加上对象的地址。因为编译器已经帮我们完成了这项工作,这也从另一个方面解释了为什么只有在成员方法 (类方法除外) 中可以使用this
关键字,this指针是函数的参数,函数内部当然就可以调用了。
加油,路漫漫其修远兮,吾将天天敲代码!!!