1. 关于类中的成员数据和成员函数:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA () {
}
~ClassA () {
}
void publicFunc () {
}
virtual void vPublicFunc () {
}
int publicV1;
int publicV2;
static int staticClassA;
private:
void privateFunc () {
}
virtual void vPrivateFunc () {
}
int privateV1;
int privateV2;
};
class ClassB : public ClassA {
public:
ClassB () {
}
void publicFunc () {
}
int publicV;
static int staticClassB;
private:
void privateFunc () {
}
int privateV;
};
int
main (int argc, char *argv[]) {
ClassA cA;
ClassB cB;
cout << "sizeof ClassA is : " << sizeof (ClassA) << endl;
cout << "sizeof ClassB is : " << sizeof (ClassB) << endl;
return 0;
}
结果(环境是MinGW Shell):
成员函数不是存在对象中的,sizeof ClassA的结果来看有点奇怪,应该是4个变量和1个静态变量——但是这样的话如何静态呢。
详细看看类的内部有什么:
用gdb 打印一下就能看到为什么ClassA 和ClassB 的sizeof 会得到这样的值了,首先都包涵了一个_vptr.ClassName 的指针,这货指向了一个虚表。
然后是5个变量,其中静态变量不在类中。
同样,很容易看出来,ClassB 中继承自ClassA,其中先存放基类的数据成员,然后自己的数据成员再按照一定顺序存放。
关于顺序,打印下cA 中变量的地址如下:
和p cA 指令得到的一个样,cA 的数据结构是:
privateV2 | high addr |
privateV1 | |
publicV2 | |
publicV1 | |
vptr | low addr |
其中成员函数在类外,一般编译器这样实现成员函数:
如果有类ClassA,其中有公有函数pFunc (void)。
编译时该函数会在类外定义,原本的调用语句
cA.pFunc ();
会变为_cA_pFunc (&cA) // 这里不同的编译器实现不一样,但是一定会传入一个this 指针
虽然成员函数都在类外,然而普通函数和虚函数还不一样(记得虚表指针么)。
至于虚函数,请看2。
2. 关于类开始的虚表指针和类的私有变量:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA (int pV1, int pV2) : pV1 (pV1), pV2 (pV2) {
}
virtual void vFunc () {
cout << "虚函数vFunc 被执行" << endl;
}
virtual void vFunc2 () {
cout << "虚函数vFunc2 被执行" << endl;
}
void printV () {
cout << "pV1 = " << pV1 << " | pV2 = " << pV2 << endl;
}
private:
int pV1, pV2;
};
int
main (int argc, char *argv[]) {
ClassA cA (1, 2);
// 看看私有变量
cA.printV ();
// C++ 类的私有类型果真是”私有“的么?
((int *)&cA)[1] = 2, ((int *)&cA)[2] = 1;
// 再看看私有变量
cA.printV ();
// 传言第一个4byte 是一个指向虚表的指针,虚表顾名思义就是虚拟函数表
// 把指向的虚表每4byte 作为函数调用,看看会得到什么
for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
(*((void (*) ())((int *)*((int *)&cA))[i])) ();
}
getchar ();
return 0;
}
结果(环境是wxDev C++ 7.4.1.13):
注意顺序哦亲。
现在我们可以了解虚表是什么东东了,大约是下面的样子
类ClassA 虚表 虚函数
(high addr) pV2 NULL ①
pV1 *pFunc2 -----------------> vFunc2
(low addr) *vptr ---------------------------> *pFunc1 -----------------> vFunc:
① 在G++ 中,这个值为NULL 代表了只有一个虚表,为1代表还有下一个虚表
既然我们了解虚表了,我们能对虚表指针做些手脚么?
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA () {
}
virtual void vFunc () {
cout << "我是ClassA 中的虚函数" << endl;
}
};
class ClassB {
public:
ClassB () {
}
virtual void vFunc () {
cout << "我是ClassB 中的虚函数" << endl;
}
} ;
int
main (int argc, char *argv[]) {
ClassA *cA = new ClassA;
ClassB *cB = new ClassB;
cA->vFunc ();
*((int *)&cA) = *((int *)&cB);
cA->vFunc (); // 现在调用的还是原先的虚函数么?
delete cA;
delete cB;
getchar ();
return 0;
}
结果(环境wxDev C++ 7.4.1.13):
虚表指针被替换了,指向了ClassB 的虚表。
上面只是测试了public 的虚函数,那么private 的虚函数呢?
我们把2 中第一段代码稍稍修改:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA (int pV1, int pV2) : pV1 (pV1), pV2 (pV2) {
}
private:
virtual void vFunc () {
cout << "虚函数vFunc 被执行" << endl;
}
virtual void vFunc2 () {
cout << "虚函数vFunc2 被执行" << endl;
}
void printV () {
cout << "pV1 = " << pV1 << " | pV2 = " << pV2 << endl;
}
private:
int pV1, pV2;
};
int
main (int argc, char *argv[]) {
ClassA cA (1, 2);
for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
(*((void (*) ())((int *)*((int *)&cA))[i])) ();
}
getchar ();
return 0;
}
结果(环境wxDev C++ 7.4.1.13):
既然私有虚函数也在虚表之内,那么虚析构函数呢?
我们修改下上面的代码:
#include <iostream>
using namespace std;
class ClassA {
public:
ClassA () {
}
virtual ~ClassA () {
cout << "虚析构函数被执行" << endl;
}
virtual void vFunc () {
cout << "虚函数vFunc 被执行" << endl;
}
};
int
main (int argc, char *argv[]) {
ClassA cA ;
// 把指向的虚表每4byte 作为函数调用,看看会得到什么
for (int i = 0; ((int *)*((int *)&cA))[i]; ++i) {
(*((void (*) ())((int *)*((int *)&cA))[i])) ();
}
getchar ();
return 0;
}
结果(环境wxDev C++ 7.4.1.13):
至于为什么虚析构函数被调用了2次,我会试图弄清楚。②
虚函数无论访问权限如何,都存在于虚表之中,所以你可以通过虚表来调用私有虚函数。
C++ 的类并不能保证数据的封装性,就像上面调用私有虚函数或者直接修改私有变量一样。
②我开了个帖子,得到一个比较靠谱的回答如下:
第一个“虚析构函数”是vtbl第一个项目的函数调用产生的,这就是多出来的那一个。据《深度探索C++对象模型》一书介绍,vtbl第一个项目通常是一个type_info对象的指针,但g++的类对象内存布局是否真这样,偶没具体研究过g++的内存布局,也没找到其它介绍g++内存布局的资料,无从考究这个多出来的“虚析构函数”是如何产生的。
3. 如果虚函数被继承……
2中我们了解了虚表,但是如果再加上继承,虚表会怎么变化呢?
单继承:
#include <iostream>
using namespace std;
// 宏的参数*必须*是对象的指针
#define CALL_V_FUNCS(P_OBJECT)\
cout << "下面是"<< #P_OBJECT\
<< " 对象中虚表中函数的调用" << endl;\
for (int i = 0; ((int *)*((int *)(P_OBJECT)))[i]; ++i) {\
(*((void (*) ())((int *)*((int *)(P_OBJECT)))[i])) ();\
}
class ClassA {
public:
ClassA () {
}
virtual void vFunc () {
cout << "我是ClassA 中的虚函数vFunc" << endl;
}
virtual void vFunc1 () {
cout << "我是ClassA 中的虚函数vFunc1" << endl;
}
};
class ClassB : public ClassA {
public:
ClassB () {
}
virtual void vFunc2 () {
cout << "我是ClassB 中的虚函数vFunc2" << endl;
}
virtual void vFunc3 () {
cout << "我是ClassB 中的虚函数vFunc3" << endl;
}
} ;
int
main (int argc, char *argv[]) {
ClassA *cA = new ClassA;
ClassB *cB = new ClassB;
CALL_V_FUNCS (cA);
CALL_V_FUNCS (cB);
delete cA;
delete cB;
getchar ();
return 0;
}
结果(环境wxDev C++ 7.4.1.13):
可以看出,ClassB 中的虚表还是一个(我的上面代码只能读单虚表的情况),ClassA 的虚函数指针在虚表前2个位置,而后是ClassB 自身的2个虚函数指针。
如果再加上虚函数的覆盖呢?‘
这段代码几乎和上面的一段一模一样,只不过ClassB 中的虚函数覆盖了ClassA 中的一个:
#include <iostream>
using namespace std;
// 宏的参数*必须*是对象的指针
#define CALL_V_FUNCS(P_OBJECT)\
cout << "下面是"<< #P_OBJECT\
<< " 对象中虚表中函数的调用" << endl;\
for (int i = 0; ((int *)*((int *)(P_OBJECT)))[i]; ++i) {\
(*((void (*) ())((int *)*((int *)(P_OBJECT)))[i])) ();\
}
class ClassA {
public:
ClassA () {
}
virtual void vFunc () {
cout << "我是ClassA 中的虚函数vFunc" << endl;
}
virtual void vFunc1 () {
cout << "我是ClassA 中的虚函数vFunc1" << endl;
}
};
class ClassB : public ClassA {
public:
ClassB () {
}
virtual void vFunc () {
cout << "我是ClassB 中的虚函数vFunc" << endl;
}
virtual void vFunc3 () {
cout << "我是ClassB 中的虚函数vFunc3" << endl;
}
} ;
int
main (int argc, char *argv[]) {
ClassA *cA = new ClassA;
ClassB *cB = new ClassB;
CALL_V_FUNCS (cA);
CALL_V_FUNCS (cB);
delete cA;
delete cB;
getchar ();
return 0;
}
结果(环境wxDev C++ 7.4.1.13):
可以看出,ClassB 的虚函数vFunc 的指针覆盖了ClassA 中vFunc 的指针在虚表中的位置,从而实现了代码的重用。
如果是多继承呢:
关于虚表,其实有些地方都是要看编译器具体实现的。多继承的虚表太依赖于编译器实现(传言VS和GCC就不一样),因此无法写出统一的代码。
然而多继承能够确定的就是:
A. 子类会继承父类的虚表,假设有3个有虚表的父类,子类就会有3个虚表。
B. 子类的虚函数被放在第一个父类的虚表中,规则和上面演示的单继承一致。
C. 如果多继承的过程中发生了虚函数的覆盖,那么规则也和上文单继承虚函数的覆盖规则的一致。