欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++对象在继承情况下的内存布局

程序员文章站 2022-05-03 22:34:45
1,C++ 中继承是非常重要的一个特性,本节课研究在继承的情形下,C++ 的对象模 型又有什么不同; 2,继承对象模型(最简单的情况下): 1,在 C++ 编译器的内部类可以理解为结构体; 2,子类是由父类成员叠加子类新成员得到的; 1,代码示例: 1 class Derived : public ......

1,c++ 中继承是非常重要的一个特性,本节课研究在继承的情形下,c++ 的对象模 型又有什么不同;  

 

2,继承对象模型(最简单的情况下):

    1,在 c++ 编译器的内部类可以理解为结构体;

    2,子类是由父类成员叠加子类新成员得到的;

       1,代码示例:

1 class derived : public demo

2 {

3 int mk;

4 }; 

       2,对象排布:

C++对象在继承情况下的内存布局 

           1,在对象模型中,先排布父类对象模型,再排布子类对象模型,见 本文3中内容;

   

3,继承对象模型初探编程实验:

 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;  // 为了证明 c++ 编译器真的会在对象中塞入一个指针成员变量,且指针放在最开始的字节处;    
41     int mi;
42     int mj;
43     int mk;
44 };
45 
46 int main()
47 {
48     cout << "sizeof(demo) = " << sizeof(demo) << endl; // 8 bytes         
49     cout << "sizeof(derived) = " << sizeof(derived) << endl;  // 12 bytes
50     
51     derived d(1, 2, 3);
52     test* p = reinterpret_cast<test*>(&d);
53     
54     cout << "before changing ..." << endl;
55     
56     d.print();  // mi = 1, mj = 2, mk = 3;
57 
58     /* 通过 p 对象改变成员变量的值,这里加了 p 指针后任然能够成功的访问; */    
59     p->mi = 10;
60     p->mj = 20;
61     p->mk = 30;
62     
63     cout << "after changing ..." << endl;
64     
65     d.print();  // mi = 10, mj = 20, mk = 30;在外界访问不到的保护成员变量的值被改变了,改变是因为 d 对象的内存分布 test 结构体的(此时类中未有虚函数,test 中未有 空指针),因此可以用 p 指针改变 d 对象当中成员变量的值;
66     
67     return 0;
68 }

 

   

4,多态对象模型:

    1,c++ 多态的实现原理:

       1,当类中声明虚函数时,编译器会在类中生成一个虚函数表;

       2,虚函数表是一个存储成员函数地址的数据结构;

           1,存储虚函数成员地址的数据结构;

       3,虚函数表是由编译器自动生成与维护的;

       4,virtual 成员函数会被编译器放入虚函数表中;

           1,这个表是给对象使用的;

           2,对象在创建时,在内部有一个虚函数表指针,这个指针指向虚函数表;

       5,存在虚函数时,每个对象中都有一个指向虚函数表的指针;

    2,框图展示:

  1,框架一

C++对象在继承情况下的内存布局 

         1,编译父类时,编译器发现了 virtual 成员函数,因此编译器创建了一个虚函数表,并且将虚函数的地址放到了虚函数表里面;

         2,编译子类时,继承自 demo,编译器发现重写了 add 函数,因此必须是虚函数,于是编译器就为子类也生成一张虚函数表,并且也会在虚函数表中放入重写过后的 add 虚函数的地址;

  2,框架二

C++对象在继承情况下的内存布局 

          1,当创建父类对象的时候,会为 demo 对象自动的塞入一个指针 vptr,也 就是如果类中有虚函数的话,在最终生成类对象的时候,会被编译器强         制赛一个指针成员变量,这个指针成员变量对于程序员是不可见的,但是它确确实实的会存在对象当中,这个指针成员变量指向了虚函数表;

         2,当创建子类对象的时候,会为 derived 对象自动的塞入一个指针 vptr,其是一个虚函数表指针,最终会指向创建的虚函数表;

         3,通过 p 指针来调用虚函数 add(),编译器就会判断,当前调用的 add() 函数是不是虚函数,如果是虚函数,编译器肯定可以知道这个虚函数地址位于虚函数表里面,编译器根据 p 指向的实际对象通过强行塞入的指针来查找虚函数表,然后在虚函数表里面取得具体的 add() 函数地址,然后通过这个地址来调用,这样子就实现了多态;

         4,当通过指针调用的函数不是虚函数,这时就不会查找虚函数表了,此时就能够直接确定函数地址;

  3,框架三

 C++对象在继承情况下的内存布局

         1,红色箭头代表寻址操作,即代表确定最后 add() 地址的操作;

         2,通过 p 指针找到具体的对象,然后通过具体的对象找到这个虚函数表指针,之后通过虚函数表指针找到虚函数表,在虚函数表里面通过查找找到最后的函数地址;

            3,多态发生的情形下,调用一个函数要经历三次寻址,这个调用效率不会高,即虚函数的调用效率低于普通的成员函数,c++ 中的多态是通过牺牲效率得到的;

            4,所以在写 c++ 面向对象程序的时候,要考虑一个成员函数有没有必要成为虚函数,因为每当我们定义一个虚函数,就会牺牲一定的效率,而 c++ 因为继承了 c 语言的特性,所以天生就要高效,既要高效,又要实现多态,这就交给了程序员了;

            5,虚函数中的指针指向具体对象,具体对象指针指向虚函数表,虚函数表中的指针指向具体的虚函数实现函数;

      

5,多态本质分析编程实验(用 c 实现多态):

    1,51-2.h 文件:

 1 #ifndef _51_2_h_
 2 #define _51_2_h_
 3 
 4 typedef void demo;
 5 typedef void derived;  // c 语言实现继承用 c++ 中的方法,即叠加;
 6 
 7 /* 父类中继承的成员函数 */
 8 demo* demo_create(int i, int j);
 9 int demo_geti(demo* pthis);
10 int demo_getj(demo* pthis);
11 int demo_add(demo* pthis, int value);  // 虚函数
12 void demo_free(demo* pthis);
13 
14 /* 子类中新定义的成员函数 */
15 derived* derived_create(int i, int j, int k);  // 构造函数;
16 int derived_getk(derived* pthis);
17 int derived_add(derived* pthis, int value);  // 虚函数
18 
19 #endif 

     2,51-2.c 文件:

  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);  // 子类 3,声明子类虚函数,实现见下面
  6 
  7 struct vtable     // 2. 定义虚函数表数据结构(用结构体表示虚函数表的数据结构,其用来创建虚函数表,见 static struct vtable g_demo_vtbl)
  8 {
  9     int (*padd)(void*, int);   // 3. 虚函数表里面存储什么?
 10 };
 11 
 12 /* 父类成员函数 */
 13 struct classdemo
 14 {
 15     struct vtable* vptr;     // 1. 定义虚函数表指针  ==》 虚函数表指针类型是什么,见第二步定义;
 16     int mi;
 17     int mj;
 18 };
 19 
 20 /* 子类成员函数 */
 21 struct classderived
 22 {
 23     struct classdemo d;  // 父类的成员变量叠加上子类的成员变量,最开始的部分为父类;
 24     int mk;
 25 };
 26 
 27 /* 父类,创建一个全局的虚函数表变量,通过 static 关键字将虚函数表隐藏在当前的文件中,外界不可访问 */
 28 static struct vtable g_demo_vtbl = 
 29 {
 30     demo_virtual_add  // 7,用真正意义上的虚函数来初始化虚函数表指针;
 31 };
 32 
 33 /* 子类 2 放子类真正意义上的虚函数 */
 34 static struct vtable g_derived_vtbl =   // static 关键字是对虚函数表这个变量隐藏在当前文件当中,完结不可访问。
 35 {
 36     derived_virtual_add
 37 };
 38 
 39 /* 父类构造函数 */
 40 demo* demo_create(int i, int j)
 41 {
 42     struct classdemo* ret = (struct classdemo*)malloc(sizeof(struct classdemo)); 
 43 
 44     if( ret != null )
 45     {
 46         ret->vptr = &g_demo_vtbl;   // 4. 关联对象和虚函数表
 47         ret->mi = i;
 48         ret->mj = j;
 49     }
 50     
 51     return ret;
 52 }
 53 
 54 /* 父类成员函数 */
 55 int demo_geti(demo* pthis)
 56 {
 57      struct classdemo* obj = (struct classdemo*)pthis;    
 58 
 59      return obj->mi;
 60 }
 61 
 62 /* 父类成员函数 */
 63 int demo_getj(demo* pthis)
 64 {
 65     struct classdemo* obj = (struct classdemo*)pthis;
 66 
 67     return obj->mj;
 68 }
 69 
 70 // 6. 定义虚函数表中指针所指向的具体函数
 71 static int demo_virtual_add(demo* pthis, int value)
 72 {
 73     struct classdemo* obj = (struct classdemo*)pthis;
 74     
 75     return obj->mi + obj->mj + value;
 76 }
 77 
 78 /* 这个函数功能和上个函数功能并没有重复,这个函数变成对外的用户所使用的函数接口 */
 79 // 5. 分析具体的虚函数是什么?要定义一个全局意义上的真正的虚函数,并且这个虚函数只在当前文件中可以访问;
 80 int demo_add(demo* pthis, int value)
 81 {
 82     struct classdemo* obj = (struct classdemo*)pthis;
 83 
 84     /* 通过对象找到具体的虚函数表指针,然后再找到具体的 add() 函数,具体的 add() 函数地址保存在 padd 里面,在这里应该是 demo_virtual_add()函数 */
 85     return obj->vptr->padd(pthis, value);
 86 }
 87 
 88 /* 父类析构函数 */
 89 void demo_free(demo* pthis)
 90 {
 91     free(pthis);
 92 }
 93 
 94 /* 子类构造函数 */
 95 derived* derived_create(int i, int j, int k)
 96 {
 97     struct classderived* ret = (struct classderived*)malloc(sizeof(struct classderived));
 98     
 99     if( ret != null )
100     {
101         ret->d.vptr = &g_derived_vtbl;  // 子类 1 ,首先关联虚函数表指针,指向子类虚函数表;
102         ret->d.mi = i;  // 初始化父类成员变量,d 是子类中父类的结构体变量;
103         ret->d.mj = j;
104         ret->mk = k;
105     }
106     
107     return ret;
108 }
109 
110 /* 子类成员函数 */
111 int derived_getk(derived* pthis)
112 {
113     struct classderived* obj = (struct classderived*)pthis;
114     
115     return obj->mk;
116 }
117 
118 /* 子类成员函数 */
119 static int derived_virtual_add(demo* pthis, int value)
120 {
121     struct classderived* obj = (struct classderived*)pthis; 
122 
123     return obj->mk + value;
124 }
125 
126 /* 子类成员函数 */
127 int derived_add(derived* pthis, int value)
128 {   
129     struct classderived* obj = (struct classderived*)pthis;
130    
131     return obj->d.vptr->padd(pthis, value);
132 }

     3,应用文件:

 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);  // demo_add(p, 3);  没有实现多态的时候,c++ 编译器这样做更安全;
 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));  // 6
17     printf("pd->add(3) = %d\n", derived_add(pd, 3));  // 336
18     
19     run(pb, 3);  // 没有实现多态的时候,打印 6;实现多态后,打印 6;
20     run(pd, 3);  // 没有实现多态的时候,打印 26;实现多态后,打印 336;
21     
22     demo_free(pb);
23     demo_free(pd);  // 子类可以继承父类的析构函数,所以可以通过父类的析构函数来析构子类对象;
24     
25     return 0;
26 }

    4,步骤:

       1,先实现基本的子类继承和其成员函数基本功能;

       2,后实现多态;

    5,c 实现 c++ 中的多态(第三个视频这里不是很明白):

       1,子类继承:

           1,另外生成结构体,内容由子类叠加父类的结构体内容;

       2,子类构造函数:

           1,另外写,先在堆上面生成指向结构体的指针,子类调用父类的构造函数是不影响父类原来的构造函数的;

       3,多态实现:

           1,在对象的结构体中定义虚函数表指针(要考虑虚函数表指针类型);

           2,在虚函数结构体中定义虚函数表数据结构(就是定义一个空的结构体);

           3,在虚函数结构表中存放指向虚函数成员函数的指针;

           4,在构造函数中关联具体的对象和虚函数表;

           5,分析让那个函数称为真正的虚函数( static 修饰 );

           6,定义虚函数表指针所指向的具体函数。

   

6,小结:

    1,继承的本质就是父子间成员变量的叠加;

    2,c++ 中的多态是通过虚函数表实现的;

    3,虚函数表是由编译器自动生成与维护的;

    4,虚函数的调用效率低于普通成员函数;