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

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

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

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

1,代码示例:

class Derived : public Demo

{

int mk;

};

2,对象排布:

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

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

 #include <iostream>
#include <string> using namespace std; class Demo
{
protected:
int mi;
int mj;
public:
virtual void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << endl;
}
}; class Derived : public Demo
{
int mk;
public:
Derived(int i, int j, int k)
{
mi = i;
mj = j;
mk = k;
} void print()
{
cout << "mi = " << mi << ", "
<< "mj = " << mj << ", "
<< "mk = " << mk << endl;
}
}; struct Test
{
void* p; // 为了证明 C++ 编译器真的会在对象中塞入一个指针成员变量,且指针放在最开始的字节处;
int mi;
int mj;
int mk;
}; int main()
{
cout << "sizeof(Demo) = " << sizeof(Demo) << endl; // 8 bytes
cout << "sizeof(Derived) = " << sizeof(Derived) << endl; // 12 bytes Derived d(, , );
Test* p = reinterpret_cast<Test*>(&d); cout << "Before changing ..." << endl; d.print(); // mi = 1, mj = 2, mk = 3; /* 通过 p 对象改变成员变量的值,这里加了 p 指针后任然能够成功的访问; */
p->mi = ;
p->mj = ;
p->mk = ; cout << "After changing ..." << endl; d.print(); // mi = 10, mj = 20, mk = 30;在外界访问不到的保护成员变量的值被改变了,改变是因为 d 对象的内存分布 Test 结构体的(此时类中未有虚函数,Test 中未有 空指针),因此可以用 p 指针改变 d 对象当中成员变量的值; return ;
}

4,多态对象模型:

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

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

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

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

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

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

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

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

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

2,框图展示:

  1,框架一

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

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

  2,框架二

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

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

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

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

  3,框架三

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

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

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

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

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

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

1,51-2.h 文件:

 #ifndef _51_2_H_
#define _51_2_H_ typedef void Demo;
typedef void Derived; // C 语言实现继承用 C++ 中的方法,即叠加; /* 父类中继承的成员函数 */
Demo* Demo_Create(int i, int j);
int Demo_GetI(Demo* pThis);
int Demo_GetJ(Demo* pThis);
int Demo_Add(Demo* pThis, int value); // 虚函数
void Demo_Free(Demo* pThis); /* 子类中新定义的成员函数 */
Derived* Derived_Create(int i, int j, int k); // 构造函数;
int Derived_GetK(Derived* pThis);
int Derived_Add(Derived* pThis, int value); // 虚函数 #endif

 2,51-2.c 文件:

 #include "51-2.h"
#include "malloc.h" static int Demo_Virtual_Add(Demo* pThis, int value); // 父类,先在这里声明,实现见第六步;
static int Derived_Virtual_Add(Demo* pThis, int value); // 子类 3,声明子类虚函数,实现见下面 struct VTable // 2. 定义虚函数表数据结构(用结构体表示虚函数表的数据结构,其用来创建虚函数表,见 static struct VTable g_Demo_vtbl)
{
int (*pAdd)(void*, int); // 3. 虚函数表里面存储什么?
}; /* 父类成员函数 */
struct ClassDemo
{
struct VTable* vptr; // 1. 定义虚函数表指针 ==》 虚函数表指针类型是什么,见第二步定义;
int mi;
int mj;
}; /* 子类成员函数 */
struct ClassDerived
{
struct ClassDemo d; // 父类的成员变量叠加上子类的成员变量,最开始的部分为父类;
int mk;
}; /* 父类,创建一个全局的虚函数表变量,通过 static 关键字将虚函数表隐藏在当前的文件中,外界不可访问 */
static struct VTable g_Demo_vtbl =
{
Demo_Virtual_Add // 7,用真正意义上的虚函数来初始化虚函数表指针;
}; /* 子类 2 放子类真正意义上的虚函数 */
static struct VTable g_Derived_vtbl = // static 关键字是对虚函数表这个变量隐藏在当前文件当中,完结不可访问。
{
Derived_Virtual_Add
}; /* 父类构造函数 */
Demo* Demo_Create(int i, int j)
{
struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo)); if( ret != NULL )
{
ret->vptr = &g_Demo_vtbl; // 4. 关联对象和虚函数表
ret->mi = i;
ret->mj = j;
} return ret;
} /* 父类成员函数 */
int Demo_GetI(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis; return obj->mi;
} /* 父类成员函数 */
int Demo_GetJ(Demo* pThis)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis; return obj->mj;
} // 6. 定义虚函数表中指针所指向的具体函数
static int Demo_Virtual_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis; return obj->mi + obj->mj + value;
} /* 这个函数功能和上个函数功能并没有重复,这个函数变成对外的用户所使用的函数接口 */
// 5. 分析具体的虚函数是什么?要定义一个全局意义上的真正的虚函数,并且这个虚函数只在当前文件中可以访问;
int Demo_Add(Demo* pThis, int value)
{
struct ClassDemo* obj = (struct ClassDemo*)pThis; /* 通过对象找到具体的虚函数表指针,然后再找到具体的 add() 函数,具体的 add() 函数地址保存在 pAdd 里面,在这里应该是 Demo_Virtual_Add()函数 */
return obj->vptr->pAdd(pThis, value);
} /* 父类析构函数 */
void Demo_Free(Demo* pThis)
{
free(pThis);
} /* 子类构造函数 */
Derived* Derived_Create(int i, int j, int k)
{
struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived)); if( ret != NULL )
{
ret->d.vptr = &g_Derived_vtbl; // 子类 1 ,首先关联虚函数表指针,指向子类虚函数表;
ret->d.mi = i; // 初始化父类成员变量,d 是子类中父类的结构体变量;
ret->d.mj = j;
ret->mk = k;
} return ret;
} /* 子类成员函数 */
int Derived_GetK(Derived* pThis)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis; return obj->mk;
} /* 子类成员函数 */
static int Derived_Virtual_Add(Demo* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis; return obj->mk + value;
} /* 子类成员函数 */
int Derived_Add(Derived* pThis, int value)
{
struct ClassDerived* obj = (struct ClassDerived*)pThis; return obj->d.vptr->pAdd(pThis, value);
}

  3,应用文件:

 #include "stdio.h"
#include "51-2.h" void run(Demo* p, int v)
{
int r = Demo_Add(p, v); // DEmo_Add(p, 3); 没有实现多态的时候,C++ 编译器这样做更安全; printf("r = %d\n", r);
} int main()
{
Demo* pb = Demo_Create(, );
Derived* pd = Derived_Create(, , ); printf("pb->add(3) = %d\n", Demo_Add(pb, )); //
printf("pd->add(3) = %d\n", Derived_Add(pd, )); // run(pb, ); // 没有实现多态的时候,打印 6;实现多态后,打印 6;
run(pd, ); // 没有实现多态的时候,打印 26;实现多态后,打印 336; Demo_Free(pb);
Demo_Free(pd); // 子类可以继承父类的析构函数,所以可以通过父类的析构函数来析构子类对象; return ;
}

4,步骤:

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

2,后实现多态;

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

1,子类继承:

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

2,子类构造函数:

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

3,多态实现:

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

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

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

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

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

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

6,小结:

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

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

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

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

C++对象在继承情况下的内存布局的更多相关文章

  1. C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域

    面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:假设不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这样的类作用域的层次嵌套使 ...

  2. C++学习笔记----4.4 继承情况下的类作用域嵌套

    引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员 ...

  3. C++使用继承时子对象的内存布局

    C++使用继承时子对象的内存布局 // */ // ]]>   C++使用继承时子对象的内存布局 Table of Contents 1 示例程序 2 对象的内存布局 1 示例程序 class ...

  4. STL容器存储的内容动态分配情况下的内存管理

    主要分两种情况:存储的内容是指针:存储的内容是实际对象. 看以下两段代码, typedef pair<VirObjTYPE, std::list<CheckID>*> VirO ...

  5. 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。

    本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...

  6. C++ 各种继承方式的类内存布局

    body, table{font-family: 微软雅黑; font-size: 10pt} table{border-collapse: collapse; border: solid gray; ...

  7. C++单继承、多继承情况下的虚函数表分析

    C++的三大特性之一的多态是基于虚函数实现的,而大部分编译器是采用虚函数表来实现虚函数,虚函数表(VTAB)存在于可执行文件的只读数据段中,指向VTAB的虚表指针(VPTR)是包含在类的每一个实例当中 ...

  8. 什么情况下JVM内存中的一个对象会被垃圾回收?

    新生代满了会触发 Young GC,老年代满了会触发 Old GC.GC时会回收对象,那么具体是什么样的对象会被垃圾回收器回收呢? 可达性分析算法,判断是否被 GC Roots 引用 判断引用类型:强 ...

  9. 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用

    欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...

随机推荐

  1. Linux 打印简单日志(一)

    简单日志输出: #include<stdio.h> #include<string.h> #include<stdlib.h> void write(char* f ...

  2. Selenium 环境安装

    前言: 本人在学习Selenium时,用的版本是Python3.6+Selenium3,后续写的所有学习资料都是基于这套环境.在安装Selenium3前,请确保本机已安装好了Python3,如未安装可 ...

  3. win10下MYSQL 8.0.16的下载、安装以及配置

    win10系统MySQL 8.0的下载安装超详细教程 https://blog.csdn.net/qq_34444097/article/details/82315587 下载安装配置链接:https ...

  4. Luogu P4708 画画 (Burnside引理、组合计数)

    题目链接 https://www.luogu.org/problem/P4708 题解 看上去Luogu P4706-4709是Sdchr神仙出的一场比赛,一道水题和三道很有趣的题终于全过了纪念QAQ ...

  5. AcWing:246. 区间最大公约数(线段树 + 增量数组(树状数组) + 差分序列)

    给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一: 1.“C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d. 2.“Q l r”,表示询问 A[l],A[l ...

  6. openvas 安装

    NMAP apt-get update & apt-get upgrade kali的更新命令 https://www.fujieace.com/kali-linux/update-sourc ...

  7. python学习之路(16)

    Python内建的filter()函数用于过滤序列. 和map()类似,filter()也接收一个函数和一个序列.和map()不同的时,filter()把传入的函数依次作用于每个元素,然后根据返回值是 ...

  8. git 指定自己的sshkey

    在服务器上生成ssh-key以后,需要把公钥放在github上,但是,这个公钥只能放在一个账户里,如果放在第二个账户里,就会提示这个key已经被用了,这是前提 一个可能的场景是这样的: 你们公司有好几 ...

  9. webpack学习之路--demo1

    1.不使用框架建立webpack项目时 (1).npm init -y 生成package.json文件 (2).npm install --save-dev webpack 在当前项目下安装webp ...

  10. ES6 变量的结构赋值用途(实用技巧)

    1.交换变量的值 let x=1; let y=2; [x,y]=[y,x];//x=2,y=1 2.从函数返回多个值 函数只能返回一个值,如果要返回多个值,只能将它们放在数组或者对象里返回,有了解构 ...