继承对象模型:
继承对象模型初探:
结构体与类等价的示例:
可以看到mi、mj、mk被改变了,这三个值在外界本来是访问不到的,但是现在通过一个指针却改变了。
这说明本例中结构体的内存分布和继承类对象的内存分布是一样的。
父类的成员排在前面,继承类新添加的成员排在后面。
多态对象模型:
多态本质分析小实验:
运行结果如下:
可以看到定义了虚函数后,类的大小增加了4个字节,这说明确实多了一个虚函数表指针。
测试虚函数表指针的位置:
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Demo 7 { 8 protected: 9 int mi; 10 int mj; 11 public: 12 virtual void print() 13 { 14 cout << "mi = " << mi << ", " 15 << "mj = " << mj << endl; 16 } 17 }; 18 19 class Derived : public Demo 20 { 21 int mk; 22 public: 23 Derived(int i, int j, int k) 24 { 25 mi = i; 26 mj = j; 27 mk = k; 28 } 29 30 void print() 31 { 32 cout << "mi = " << mi << ", " 33 << "mj = " << mj << ", " 34 << "mk = " << mk << endl; 35 } 36 }; 37 38 struct Test 39 { 40 void* p; 41 int mi; 42 int mj; 43 int mk; 44 }; 45 46 int main() 47 { 48 cout << "sizeof(Demo) = " << sizeof(Demo) << endl; 49 cout << "sizeof(Derived) = " << sizeof(Derived) << endl; 50 51 Derived d(1, 2, 3); 52 Test* p = reinterpret_cast<Test*>(&d); 53 54 cout << "Before changing ..." << endl; 55 56 d.print(); 57 58 p->mi = 10; 59 p->mj = 20; 60 p->mk = 30; 61 62 cout << "After changing ..." << endl; 63 64 d.print(); 65 66 return 0; 67 }
运行结果:
打印结果正确,这说明虚函数表的指针确实在开头的位置。要不然,我们重解释内存后,在结构体中修改的mi、mj、mk和类对象中的mi、mj、mk是不对应的。
用C写面向对象,并实现多态:
1 #ifndef _51_2_H_ 2 #define _51_2_H_ 3 4 typedef void Demo; 5 typedef void Derived; 6 7 Demo* Demo_Create(int i, int j); 8 int Demo_GetI(Demo* pThis); 9 int Demo_GetJ(Demo* pThis); 10 int Demo_Add(Demo* pThis, int value); 11 void Demo_Free(Demo* pThis); 12 13 Derived* Derived_Create(int i, int j, int k); 14 int Derived_GetK(Derived* pThis); 15 int Derived_Add(Derived* pThis, int value); 16 17 #endif
1 #include "51-2.h" 2 #include "malloc.h" 3 4 static int Demo_Virtual_Add(Demo* pThis, int value); 5 static int Derived_Virtual_Add(Demo* pThis, int value); 6 7 struct VTable // 2. 定义虚函数表数据结构 8 { 9 int (*pAdd)(void*, int); // 3. 虚函数表里面存储什么??? 10 }; 11 12 struct ClassDemo 13 { 14 struct VTable* vptr; // 1. 定义虚函数表指针 ==》 虚函数表指针类型??? 15 int mi; 16 int mj; 17 }; 18 19 struct ClassDerived 20 { 21 struct ClassDemo d; 22 int mk; 23 }; 24 25 static struct VTable g_Demo_vtbl = 26 { 27 Demo_Virtual_Add 28 }; 29 30 static struct VTable g_Derived_vtbl = 31 { 32 Derived_Virtual_Add 33 }; 34 35 Demo* Demo_Create(int i, int j) 36 { 37 struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo)); 38 39 if( ret != NULL ) 40 { 41 ret->vptr = &g_Demo_vtbl; // 4. 关联对象和虚函数表 42 ret->mi = i; 43 ret->mj = j; 44 } 45 46 return ret; 47 } 48 49 int Demo_GetI(Demo* pThis) 50 { 51 struct ClassDemo* obj = (struct ClassDemo*)pThis; 52 53 return obj->mi; 54 } 55 56 int Demo_GetJ(Demo* pThis) 57 { 58 struct ClassDemo* obj = (struct ClassDemo*)pThis; 59 60 return obj->mj; 61 } 62 63 // 6. 定义虚函数表中指针所指向的具体函数 64 static int Demo_Virtual_Add(Demo* pThis, int value) 65 { 66 struct ClassDemo* obj = (struct ClassDemo*)pThis; 67 68 return obj->mi + obj->mj + value; 69 } 70 71 72 // 5. 分析具体的虚函数!!!! 73 int Demo_Add(Demo* pThis, int value) 74 { 75 76 struct ClassDemo* obj = (struct ClassDemo*)pThis; 77 78 return obj->vptr->pAdd(pThis, value); 79 } 80 81 void Demo_Free(Demo* pThis) 82 { 83 free(pThis); 84 } 85 86 Derived* Derived_Create(int i, int j, int k) 87 { 88 struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived)); 89 90 if( ret != NULL ) 91 { 92 ret->d.vptr = &g_Derived_vtbl; 93 ret->d.mi = i; 94 ret->d.mj = j; 95 ret->mk = k; 96 } 97 98 return ret; 99 } 100 101 int Derived_GetK(Derived* pThis) 102 { 103 struct ClassDerived* obj = (struct ClassDerived*)pThis; 104 105 return obj->mk; 106 } 107 108 static int Derived_Virtual_Add(Demo* pThis, int value) 109 { 110 struct ClassDerived* obj = (struct ClassDerived*)pThis; 111 112 return obj->mk + value; 113 } 114 115 int Derived_Add(Derived* pThis, int value) 116 { 117 struct ClassDerived* obj = (struct ClassDerived*)pThis; 118 119 return obj->d.vptr->pAdd(pThis, value); 120 }
1 #include "stdio.h" 2 #include "51-2.h" 3 4 void run(Demo* p, int v) 5 { 6 int r = Demo_Add(p, v); 7 8 printf("r = %d\n", r); 9 } 10 11 int main() 12 { 13 Demo* pb = Demo_Create(1, 2); 14 Derived* pd = Derived_Create(1, 22, 333); 15 16 printf("pb->add(3) = %d\n", Demo_Add(pb, 3)); 17 printf("pd->add(3) = %d\n", Derived_Add(pd, 3)); 18 19 run(pb, 3); 20 run(pd, 3); 21 22 Demo_Free(pb); 23 Demo_Free(pd); 24 25 return 0; 26 }
我们定义了虚函数表的结构体类型,并且定义了一个全局的虚函数表类型的变量。
我们在ClassDemo结构体中塞入一个“虚函数表”指针vtbl。
我们模拟编译器的行为,在生成对象时,将虚函数的地址写入虚函数表变量中,并将虚函数表变量的地址挂接到ClassDemo结构体中的虚函数表指针上。
运行结果如下:
小结:
补充说明:
面向对象程序最关键的地方在于必须能够表现三大特性:封装,继承,多态!
封装指的是类中的敏感数据在外界是不能访问的;继承指的是可以对已经存在的类
进行代码复用,并使得类之间存在父子关系;多态指的是相同的调用语句可以产生
不同的调用结果。因此,如果希望用 C 语言完成面向对象的程序,那么肯定的,
必须实现这三个特性;否则,最多只算得上基于对象的程序(程序中能够看到对象
的影子,但是不完全具备面向对象的 3 大特性)。
课程中通过 void* 指针保证具体的结构体成员是不能在外界被访问的,以
此模拟 C++ 中 private 和 protected。因此,在头文件中定义了如下的语句:
typedef void Demo;
typedef void Derived;
Demo 和 Derived 的本质依旧是 void, 所以,用 Demo* 指针和 Derived* 指针
指向具体的对象时,无法访问对象中的成员变量,这样就达到了“外界无法访问类
中私有成员”的封装效果!
继承的本质是父类成员与子类成员的叠加,所以在用 C 语言写面向对象程
序的时候,可以直接考虑结构体成员的叠加即可。课程中的实现直接将 struct
ClassDemo d 作为 struct ClassDerived 的第一个成员,以此表现两个自定义数
据类型间的继承关系。因为 struct ClassDerived 变量的实际内存分布就是由
struct ClassDemo 的成员以及 struct ClassDerived 中新定义的成员组成的,这
样就直接实现了继承的本质,所以说 struct ClassDerived 继承自 struct
ClassDemo。
下一步要实现的就是多态了!多态在 C++ 中的实现本质是通过虚函数表完
成的,而虚函数表是编译器自主产生和维护的数据结构。因此,接下来要解决的问
题就是如何在 C 语言中自定义虚函数表?课程中认为通过结构体变量模拟 C++
中的虚函数表是比较理想的一种选择,所以有了下面的代码:
struct VTable
{
int (*pAdd)(void*, int);
};
必须要注意的是,课程中由于复制粘贴的缘故,误将 pAdd 指针的类型定义成了
int (*)(Derived*, int) , 这从 C 语言的角度算不上错误,因为 Derived* 的本质就
是 void* , 所以编译运行都没有问题。但是,从面向对象的角度,这里可以说是
一种语义上的错误!因为 pAdd 必须可以指向父类中定义的 Add 函数版本,也可
以指向子类中定义的 Add 函数版本,所以说用 Derived* 作为第一个参数表示实
际对象并不合适,应该直接使用 void* 。
有了类型后就可以定义实际的虚函数表了,在 C 语言中用具有文件作用域
的全局变量表示实际的虚函数表是最合适的,因此有了下面的代码:
// 父类对象使用的虚函数表
static struct VTable g_Demo_vtbl =
{
Demo_Virtual_Add
};
// 子类对象使用的虚函数表
static struct VTable g_Derived_vtbl =
{
Derived_Virtual_Add
};
每个对象中都拥有一个指向虚函数表的指针,而所有父类对象都指向
g_Demo_vtbl,所以所有子类对象都指向 g_Derived_vtbl。当一切就绪后,实际
调用虚函数的过程就是通过虚函数表中的对应指针来完成的。