以下分析是基于VS2010的。以后会使用G++分析看看G++如何处理多态!
1 // polymorphic_test.cpp : 定义控制台应用程序的入口点。 2 // 3 4 /** 5 特别注意:实现C++多态,除了基类相关函数要声明 virtual关键字,还需要派生类的该函数签名和基类完全一致!两个条件缺一不可,否则: 6 (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual 7 关键字,基类的函数将被隐藏(注意别与重载混淆)。 8 (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 9 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。 10 */ 11 12 #include "stdafx.h" 13 #include<iostream> 14 using namespace std; 15 16 class A 17 { 18 public: 19 A():a(0){;} 20 void foo() { std::cout << "BaseMember\t"; } 21 virtual void vfun() { std::cout << "BaseVirtual\t"; } 22 //private: 23 int a; 24 }; 25 26 class B : public A 27 { 28 public: 29 B():b(1){;} 30 void foo() { std::cout << "DerivedMember\t"; } 31 void vfun() { std::cout << "DerivedVirtual\t"; } 32 //private: 33 int b; 34 }; 35 36 void test() 37 { 38 A a; 39 B b; 40 A *pa = NULL; 41 B *pb = NULL; 42 43 while(true){ 44 pa = &a; 45 sizeof(a); sizeof(b); 46 &a.a; &b.b; &b.a; 47 (int)&a.a ; (int)&b.b ; 48 int tt = &b - &a; 49 int t = (int)&b - (int)&a; 50 pa->foo(); // 非虚拟函数是编译器绑定,指针类型是什么,就调用该类型的成员函数!因此这里输出 BaseMember 51 pa->vfun(); // 运行期绑定,调用实际的类型函数!输出 BaseVirtual\t 52 53 pa = &b; 54 pa->foo(); // 非虚拟函数是编译器绑定,指针类型是什么,就调用该类型的成员函数!由于指针类型是A*,因此调用A的成员函数。 55 // 因此输出 BaseMember 56 pa->vfun(); // 运行期绑定,调用实际类型的成员函数。因此输出 DerivedVirtual\t 57 //return 0; 58 std::cout << std::endl; 59 60 61 pb = &b; 62 pb->foo(); pb->vfun(); // 这里输出 DerivedMember\t, DerivedVirtual\t .好理解。 63 64 pb = (B*)&a; 65 pb->foo(); // 这里注意:非虚拟函数编译期间绑定, pb的类型是B*, 因此调用B的成员函数。输出 DerivedMember\t 66 pb->vfun(); // 虚拟函数,调用实际类型的函数,现在pb指向的&a, 因此调用A的成员函数,输出 BaseVirtual\t 67 68 std::cout << "\n" << std::endl; 69 } 70 71 } 72 73 int _tmain(int argc, _TCHAR* argv[]) 74 { 75 test(); 76 return 0; 77 }
根据调试信息,观察到的对象内存地址!
执行pa=&a之后:
指向pa=&b之后:
根据内存地址,汇出的对象内存布局. (不太会用word,画的太乱了。抱歉!)
根据该内存布局,我们可以总结如下:
1. 派生类对象也有基类继承成员的独立拷贝,并非如《深入C++对象模型》所说的“派生类的继承自基类的成员是依附于基类对象“
2. 每个对象都在栈地址上分配(没有使用new)。对象间是从高地址到低地址分配,所以a对象的起始地址大于b对象的;而在对象内部数据成员之间,是从低地址到高地址开始分配,所以b.a地址要高于b.b地址。
3. 假如存在vptr,那么vptr在对象的最开始处分配(vptr是对象内存布局的第一个成员).
4. 程序运行过程中,假如ptr指向不同的对象。那么调用虚函数时,会通过vptr+offset找到虚函数的入口地址。(比如,pa指向&a时,那么pa->vfun(),就是通过a对象的vptr找到vfun的地址,所以是调用A::vfun() ;当pa = &pb执行后,pa此时指向&pb,然后据此得到b对象的vptr+offset调用B::vfun()).
5. 非虚函数,是编译期间绑定,指针实际类型是什么,就调用该类型的成员函数,与运行期所指向对象无关。因为,因为pa的类型是A*,所以pa->foo()总是调用A::foo(),同理pb->foo()总是调用B::foo().
程序运行结果:
时间: 2024-10-20 01:43:13