开始本篇文章之前,我们现在先来看看一个具体问题 :
class CObject{public: virtual void Serialize() { cout << "CObject::Serialize() "; }};
class CDocument : public CObject{public: int m_data1; void func1() { cout << "CDocument::func1()" << endl; Serialize(); } virtual void Serialize() { cout << "CDocument::Serialize() "; } void func2() { cout << "CDocument::func2()" << endl; test(); } void test() { cout << "CDocument::test() "; }};
class CMyDoc : public CDocument{public: int m_data2; virtual void Serialize() { cout << "CMyDoc::Serialize() "; } void test() { cout << "CMyDoc::test() "; }}; void main(){ cout << sizeof(CObject) << endl; cout << sizeof(CDocument) << endl; cout << sizeof(CMyDoc) << endl;
CMyDoc mydoc; CMyDoc* pmydoc = new CMyDoc; CDocument* pdoc = new CMyDoc;
cout << "#1 testing" <<endl; mydoc.func1(); mydoc.func2(); mydoc.test();
cout << "#2 testing" <<endl; pmydoc->func1(); pmydoc->func2(); pmydoc->test();
cout << "#3 testing" <<endl; pdoc->func1(); pdoc->func2(); pdoc->test();
cout << "#4 testing" <<endl; ((CDocument*)(&mydoc))->func1(); ((CDocument*)(&mydoc))->func2(); ((CDocument*)(&mydoc))->test();
cout << "#5 testing" <<endl; ((CDocument)mydoc).func1(); ((CDocument)mydoc).func2(); ((CDocument)mydoc).test(); }如果题目的输出结果会使你头疼,你就有必要继续了解C++对象模型的内部细节和不同类型函数的调用本质。
这些知识点正是我想要讲述的内容。
首先让我们来了解一下包含有虚函数的类对象在内存中的结构:(在这里我只分析单继承的情况。至于多继承,请参考《Inside The C++ Object Model》在Class1对象的内存区块中,包含一个指向虚函数表的指针以及对象的各个数据成员。至于Class1类的普通成员函数,他们在编译时被编译器更改名称,并增加了一个参数(this指针),因而可以处理调用者(C++对象)中的成员变量。所以在Class1对象的内存区块中看不到和成员函数相关的任何东西。虚函数表的内容是依据类中的虚函数声明次序,一一填入函数指针。派生类会继承基类的虚函数表(以及所有其他可以继承的成员),当我们在派生类中改写虚函数时,虚函数表就受了影响:表中元素所指的函数地址将不再是基类的函数地址,而是派生类的函数地址。
接下来,需要了解虚成员函数和普通成员函数不同的绑定机制: 虚成员函数采用动态绑定机制,也叫做运行时绑定。在执行期,依据调用者实际代表(或者说调用地址实际指向)的类型进行动态绑定。普通的成员函数采用静态绑定机制,也叫做编译时绑定。在编译时,依据调用者的声明类型进行静态绑定。有了这些基本概念之后,我们来分析上述问题的答案:1)三个sizeof输出:CObject的内存结构只有一个vptr; CDocument的内存结构包含一个vptr和数据成员m_data1; CMyDoc的内存结构包含一个vptr以及两个数据成员:继承来的m_data1和新定义的m_data2。所有的数据成员都是int型,在32位机上,指针占据的空间和int类型一样都是32个bit位---4个字节。sizeof输出的是字节数: 4812 2)4个testing部分的输出: 首先我们先要弄清楚调用者的声明类型和实际代表类型: //声明类型:CMyDoc //实际类型:CMyDoc cout << "#1 testing" <<endl; mydoc.func1(); mydoc.func2(); mydoc.test();
//声明类型:CMyDoc //实际类型:CMyDoc cout << "#2 testing" <<endl; pmydoc->func1(); pmydoc->func2(); pmydoc->test();
//声明类型:CDocument //实际类型:CMyDoc cout << "#3 testing" <<endl; pdoc->func1(); pdoc->func2(); pdoc->test();
//经过类型转换之后 //声明类型变为:CDocument //实际类型仍是:CMyDoc cout << "#4 testing" <<endl; ((CDocument*)(&mydoc))->func1(); ((CDocument*)(&mydoc))->func2(); ((CDocument*)(&mydoc))->test();
//经过对象切割之后 //相当于编译器以所谓的拷贝构造函数把CDocument对象内容复制了一份, //使得在调用时mydoc完全变成了Cocument对象,包括vtable中的函数指针 cout << "#5 testing" <<endl; ((CDocument)mydoc).func1(); ((CDocument)mydoc).func2(); ((CDocument)mydoc).test(); 然后依据虚函数和普通函数不同的绑定机制去分析。其中需要注意:CMyDoc自己没有func1和func2函数,而它继承了CDocument的所有成员。所以所有关于这两个函数的调用都是对CDocument::func1()和CDocument::func2的调用。不同之处在于:func1中调用的虚函数需要在运行时依据调用者的实际代表类型进行动态绑定;func2中调用的普通成员函数----test在func2被编译的时候就需要具体绑定,此时只能从func2所在的类型----CDocument中开始查找,即使真正的声明类型CMyDoc中重写了test函数。因为当func2被编译的时候CMyDoc中的内容是不可见的。由此,可以得到该部分的输出结果为: #1 testingCDocument::func1()CMyDoc::Serialize()CDocument::func2()CDocument::test()CMyDoc::test()
#2 testingCDocument::func1()CMyDoc::Serialize()CDocument::func2()CDocument::test()CMyDoc::test()
#3 testingCDocument::func1()CMyDoc::Serialize()CDocument::func2()CDocument::test()CDocument::test()
#4 testingCDocument::func1()CMyDoc::Serialize()CDocument::func2()CDocument::test()CDocument::test()
#5 testingCDocument::func1()CDocument::Serialize()CDocument::func2()CDocument::test()CDocument::test()