本篇文章由zg51747708曾广 原创,未经允许不可以转载
注:本文章内的程序代码全部是在Window 7 sp1 VS2015 Update3上测试
在学习C++中我总体感觉比较难理解的概念就是虚函数的理解,而且比较难想到他的内部实现。于是写下这篇博客,来帮助大家更深入的理解虚函数,纯虚函数,虚函数表。希望大家带着批判来阅读,如有错误请私聊我,谢谢!
一.虚函数与纯虚函数定义的解释
首先我在网上发现有些人的解释有明显错误的地方。比如说“虚函数是没有定义的成员函数”。这里要说明的是,这应该是对于纯虚函数而言。纯虚函数就是没有定义的。
1. 所以虚函数是有定义的。
虚函数定义的语法如下。
virtual void fun(){}
2. 虚函数的产生是为了能使得父类(基类)的指针,或者引用能够调用到子类(派生类)的同名函数(实现多态)。
比如下列程序(这个是用指针的例子,引用的我就写明了):
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class Moveable {
public:
virtual void move() {
cout << "你有腿哦!" << endl;
}
};
class Car : public Moveable {
public:
void eat() {}
void move() {
cout << "能送四个骚货哦!" << endl;
}
};
class Bus : public Moveable {
public:
void move() {
cout << "能送一堆骚货哦!" << endl;
}
};
void Move(Moveable * p) {
p->move();
}
int main() {
Bus a;
Car b;
Moveable c;
Move(&a);
Move(&b);
Move(&c);
system("pause");
return 0;
}
3.纯虚函数才没有定义。
纯虚函数定义的语法如下。
virtual void fun() = 0;
虚函数表就是实现虚函数,纯虚函数的内部方法。
---------------------------------------------------------------------------------------------------------------------------------
一.虚函数表的真实样子
首先我们要知道虚函数表在内存中真正存储的形式。可以用一下方式来测试:
#include <iostream>
#include <stdlib.h>
using namespace std;
class A {
public:
virtual void a() {
}
};
int main() {
cout << sizeof(A) << endl;
system("pause");
return 0;
}
输出的结果是:4
注:程序内存包含,代码段,数据段,堆段,栈段
这里要说明的是,sizeof中并不包括成员函数所占有的空间,因为函数存在于代码段,sizeof是检测的对象占有的内存空间而不包括函数。(最简单的检测方法是,你再在类中加一个函数,再看看sizeof返回的值,是否增加了。)
那么问题来了,这个4个字节是存放的什么呢?答案就是虚函数表的内存地址。
那么如果类中有数据成员,这个指针会在哪个位置呢?可以这样分析,如果把这个指针放在后面。因为数据成员数量的不确定,那么寻找这个指针将是比较费时间,而且相对而言难以实现的。而且在多重继承或者更复杂的情况下,能够访问速度最大化。这样的分析是正确的,虚函数表的内存地址就是存放在对象地址的开头。
-------------------------------------------------------------------------------------
到这里总结了两点重要的,1.存储的是虚函数表的内存地址,2.地址在对象内存区域的开头。
-------------------------------------------------------------------------------------
那么对应的虚函数的入口地址,在表中又是如何排列的呢?让我们使用函数指针来测试虚函数在虚函数表中的位置吧。
1. 再没有继承关系时
//编译环境Windows 7 VS2015 Update3
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class A {
public:
virtual void a(void) {
cout << "A::a 函数发骚了" << endl;
}
virtual void b(void) {
cout << "A::b 函数发骚了" << endl;
}
virtual void c(void) {
cout << "A::c 函数发骚了" << endl;
}
};
int main() {
void (*fun)(void);//函数指针,其数据类型是void (*)(void)
A A_demo;
cout << &A_demo << endl;//输出的是A_demo对象的首地址
//(&A_demo)对象首地址
//*((int *)&A_demo)虚函数表首地址
//(*((int *)(*((int *)&A_demo))))虚函数表的第一个函数地址。
fun = (void(*)(void))(*((int *)(*((int *)&A_demo))));
fun();
fun = (void(*)(void))(*((int *)(*((int *)&A_demo)) + 1));
fun();
fun = (void(*)(void))(*((int *)(*((int *)&A_demo)) + 2));
fun();
system("pause");
return 0;
}
这里能够看出虚函数表内的排列是按照public下,函数的定义顺序来的。
也就是
2. 在存在继承关系而且有同名函数时。
class A {
public:
virtual void a(void) {
cout << "A::a 函数发骚了" << endl;
}
virtual void b(void) {
cout << "A::b 函数发骚了" << endl;
}
virtual void c(void) {
cout << "A::c 函数发骚了" << endl;
}
};
class B : public A {
public:
void a(void) {
cout << "B::a 函数发骚了" << endl;
}
};
虚函数表内的B::a函数地址覆盖了本属于A::a的位置。
3. 存在继承关系,都是虚函数,但是没有同名时。
//编译环境Windows 7 VS2015 Update3
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class A {
public:
virtual void a(void) {
cout << "A::a 函数发骚了" << endl;
}
virtual void b(void) {
cout << "A::b 函数发骚了" << endl;
}
virtual void c(void) {
cout << "A::c 函数发骚了" << endl;
}
};
class B : public A {
public:
virtual void ab(void) {
cout << "B::a 函数发骚了" << endl;
}
virtual void bb(void) {
cout << "B::b 函数发骚了" << endl;
}
virtual void cb(void) {
cout << "B::c 函数发骚了" << endl;
}
};
int main() {
void (*fun)(void);//函数指针,其数据类型是void (*)(void)
B B_demo;
A * pA_demo = &B_demo;
cout << pA_demo << "," << &B_demo << endl;//输出的是A_demo对象的首地址
cout << "pA_demo指向的对象的虚函数表首地址: " << (int *)*((int *)pA_demo) << endl;
/*cout << "&B_demo指向的对象的虚函数表首地址: " << (int *)*((int *)&B_demo) + 1 << endl;*/
//(&A_demo)对象首地址
//*((int *)&A_demo)虚函数表首地址
//(*((int *)(*((int *)&A_demo))))虚函数表的第一个函数地址。
cout << "派生类的虚函数表" << endl;
fun = (void(*)(void))(*((int *)(*((int *)&B_demo))));
cout << (*((int *)(*((int *)pA_demo)))) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)&B_demo)) + 1));
cout << (*((int *)(*((int *)pA_demo)) + 1)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)&B_demo)) + 2));
cout << (*((int *)(*((int *)pA_demo)) + 2)) << endl;
fun();
//-----------------------------------------------------------------
cout << "基类的虚函数表" << endl;
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 3));
cout << (*((int *)(*((int *)pA_demo)) + 3)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 4));
cout << (*((int *)(*((int *)pA_demo)) + 4)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 5));
cout << (*((int *)(*((int *)pA_demo)) + 5)) << endl;
fun();
system("pause");
return 0;
}
这种情况下:在原有的虚函数表上,前天三个入口是基类的虚函数入口地址。
后三个入口是派生类的虚函数入口地址。
1. 多重继承,但是派生类中有虚函数重载的情况。
//编译环境Windows 7 VS2015 Update3
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class A {
public:
virtual void a(void) {
cout << "A::a 函数发骚了" << endl;
}
virtual void b(void) {
cout << "A::b 函数发骚了" << endl;
}
virtual void c(void) {
cout << "A::c 函数发骚了" << endl;
}
};
class B {
public:
virtual void a(void) {
cout << "B::a 函数发骚了" << endl;
}
virtual void b(void) {
cout << "B::b 函数发骚了" << endl;
}
virtual void c(void) {
cout << "B::c 函数发骚了" << endl;
}
};
class C :public A,public B{
public:
virtual void ac(void) {
cout << "C::a 函数发骚了" << endl;
}
virtual void bc(void) {
cout << "C::b 函数发骚了" << endl;
}
virtual void cc(void) {
cout << "C::c 函数发骚了" << endl;
}
};
int main() {
void(*fun)(void);//函数指针,其数据类型是void (*)(void)
cout << sizeof(long long int) << endl;
C C_demo;
A * pA_demo = &C_demo;
cout << pA_demo << "," << &C_demo << endl;//输出的是A_demo对象的首地址
cout << "pA_demo指向的对象的虚函数表首地址: " << (int *)*((int *)pA_demo) << endl;
/*cout << "&B_demo指向的对象的虚函数表首地址: " << (int *)*((int *)&B_demo) + 1 << endl;*/
//(&A_demo)对象首地址
//*((int *)&A_demo)虚函数表首地址
//(*((int *)(*((int *)&A_demo))))虚函数表的第一个函数地址。
cout << "派生类的虚函数表" << endl;
fun = (void(*)(void))(*((int *)(*((int *)pA_demo))));
cout << (*((int *)(*((int *)pA_demo)))) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 1));
cout << (*((int *)(*((int *)pA_demo)) + 1)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 2));
cout << (*((int *)(*((int *)pA_demo)) + 2)) << endl;
fun();
//-----------------------------------------------------------------
cout << endl;
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 3));
cout << (*((int *)(*((int *)pA_demo)) + 3)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 4));
cout << (*((int *)(*((int *)pA_demo)) + 4)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 5));
cout << (*((int *)(*((int *)pA_demo)) + 5)) << endl;
fun();
cout << endl;
fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 0));
cout << (*((int *)(*((int *)pA_demo + 1)) + 0)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 1));
cout << (*((int *)(*((int *)pA_demo + 1)) + 1)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 2));
cout << (*((int *)(*((int *)pA_demo + 1)) + 2)) << endl;
fun();
cout << endl;
/*fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 3));
cout << (*((int *)(*((int *)pA_demo + 1)) + 3)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 4));
cout << (*((int *)(*((int *)pA_demo + 1)) + 4)) << endl;
fun();
fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 5));
cout << (*((int *)(*((int *)pA_demo + 1)) + 5)) << endl;
fun();*/
system("pause");
return 0;
}
这里这种C继承了A和B的情况下,按照之前的测试方法测试了很久,在虚函数表中一直找不到B类的函数入口,我觉得既然继承了,就应该存在吧,因为毕竟使用了virtual就应该在虚函数表里(这就证明了B类的函数入口应该是在数据区),而且A和B并不存在继承关系,因为我可以通过C_demo.B::a()这个还是能调用到类B的成员函数。
突然,想到会不会有两张虚函数表呢?于是在对象的起始地址那里向后偏移了4个字节。发现,这里也是一张虚函数表。
原来一个对象会建立两张虚函数表,现在这种情况的排列形式如下图。
那为什么A和C的会在一张表里面,而不是和B在一起呢?,继续探知发现,是和C继承A和B时的顺序有关。
改成这样:class C :publicB,public A {
并且,基类指针改成B的B * pA_demo =&C_demo;(或者直接使用&C_demo是一样的,同一地址)
结果就变成下面的图的样子。
① 问题又来了。如果还是上面那种多继承,如果A和B类的函数名不相同呢?
没错,还是和上面一样的结果,这里就证明了,两个基类的函数名称相同并不会对编译产生影响。
②还有一个问题,上面多继承A和B的情况下,内存中出现了两张表,那么虚函数表的个数是和基类个数有关吗?
果然,在单层的多继承的情况下,虚函数表的个数是和基类的个数相等的。
③那么在多重继承下呢,虚函数表个数会和基类个数有什么关系呢?继续编程测试。(当然这里测试时,不会使用同名,因为同名会实现派生类的函数入口覆盖了虚函数表中基类的同名虚函数入口)这个就是C++里的覆盖,隐藏就是派生类的函数与基类同名,于是基类就隐藏了。需要使用::号去访问。
测试结果如下图,
结果显示只有一张虚函数表,而且是以多重继承的继承顺序排列下来。也就是A->B->C.用语言来描述就是在单纯的多重继承中,虚函数表的个数只有一个。并按照继承的顺序成员函数以线性排列在虚函数表中。
1. 如果是多继承中派生类与两个基类中的一个有重载的情况呢?
测试代码如下:
//编译环境Windows 7 VS2015 Update3 #include <iostream> #include <stdlib.h> #include <string> using namespace std; class A { public: virtual void a(void) { cout << "A::a 函数发骚了" << endl; } virtual void b(void) { cout << "A::b 函数发骚了" << endl; } virtual void c(void) { cout << "A::c 函数发骚了" << endl; } }; class B { public: virtual void ab(void) { cout << "B::a 函数发骚了" << endl; } virtual void bb(void) { cout << "B::b 函数发骚了" << endl; } virtual void cb(void) { cout << "B::c 函数发骚了" << endl; } }; class C :public A, public B { public: virtual void a(void) { cout << "C::a 函数发骚了" << endl; } virtual void b(void) { cout << "C::b 函数发骚了" << endl; } virtual void c(void) { cout << "C::c 函数发骚了" << endl; } }; int main() { void(*fun)(void);//函数指针,其数据类型是void (*)(void) cout << sizeof(long long int) << endl; C C_demo; A * pA_demo = &C_demo; cout << pA_demo << "," << &C_demo << endl;//输出的是A_demo对象的首地址 cout << "pA_demo指向的对象的虚函数表首地址: " << (int *)*((int *)pA_demo) << endl; /*cout << "&B_demo指向的对象的虚函数表首地址: " << (int *)*((int *)&B_demo) + 1 << endl;*/ //(&A_demo)对象首地址 //*((int *)&A_demo)虚函数表首地址 //(*((int *)(*((int *)&A_demo))))虚函数表的第一个函数地址。 cout << "派生类的虚函数表" << endl; fun = (void(*)(void))(*((int *)(*((int *)pA_demo)))); cout << (*((int *)(*((int *)pA_demo)))) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 1)); cout << (*((int *)(*((int *)pA_demo)) + 1)) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 2)); cout << (*((int *)(*((int *)pA_demo)) + 2)) << endl; fun(); //----------------------------------------------------------------- /*cout << endl; fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 3)); cout << (*((int *)(*((int *)pA_demo)) + 3)) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 4)); cout << (*((int *)(*((int *)pA_demo)) + 4)) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 5)); cout << (*((int *)(*((int *)pA_demo)) + 5)) << endl; fun();*/ cout << endl; fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 0)); cout << (*((int *)(*((int *)pA_demo + 1)) + 0)) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 1)); cout << (*((int *)(*((int *)pA_demo + 1)) + 1)) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 2)); cout << (*((int *)(*((int *)pA_demo + 1)) + 2)) << endl; fun(); cout << endl; /*fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 3)); cout << (*((int *)(*((int *)pA_demo + 1)) + 3)) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 4)); cout << (*((int *)(*((int *)pA_demo + 1)) + 4)) << endl; fun(); fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 5)); cout << (*((int *)(*((int *)pA_demo + 1)) + 5)) << endl; fun();*/ system("pause"); return 0; }
从结果看出,这里A的虚函数的入口被在C类中重载的对应函数将入口覆盖了。第二张表中是B的函数入口。
1. 如果是多继承中派生类与两个基类都有重载的情况呢?u
程序代码更改很简单,直接把B类的函数名字改成和A类相同的。
结果是,A和B这两个基类给派生类的虚函数表,都被派生类C的重载函数覆盖,这个结果表明,不管有多少个基类存在虚函数在派生来中重载,相互之间是没有任何影响的。
到这里基本上所有情况我都涉及到了。如有漏情况,或者错误,请私聊我,谢谢,本人是菜鸟。可能代码写的并不好,而且博客书写也也不好,请谅解。
博客撰写者:曾广
2017/02/24