原文链接

在多继承的情况下,用static_cast去upcast子对象指针到父对象指针时候,This 指针会自动偏移到正确的那路父对象指针。 条件是static_cast时候,子对象的指针不可以是(void *)必须是(static_cast *). 原因是static_cast是编译时候处理的。

单继承(Single Inheritance,SI)对象模型

考虑下面的这个类:

[cpp] view plain copy
  1. class A
  2. {
  3. public:
  4. int m_a;
  5. int m_b;
  6. };
  7. A a;

class object a在内存中的布局如下所示:

两个member data按照顺序排列,class object a的地址就是该内存空间的首地址。现在我们增加一个类:

[cpp] view plain copy
  1. class B : public A
  2. {
  3. public:
  4. int m_c;
  5. };
  6. B b;
  7. A* a = &b; // upcast
  8. B* b2 = static_cast<B*>(a); // downcast

class object b在内存中的布局如下图:

先是A类的subobject内存布局,然后是B类的data member。C++标准保证“出现在派生类中的基类subobject保留其原样性不变”。因此无论class A的布局如何,都会完整地存在于class B的内存模型中,这主要考虑和C的兼容性。但有以下几点需要注意(请不要被下面3条所述细节困扰,如果实在不太清楚,可以略过,我们的重点在于SI的基础知识):

1)class A的因内存alignment而产生的padding bytes也必须出现在B的class A subobject中,这确保了基类subobject的严格原样性。

2)对于具有virtual function的类体系,vptr的放置根据不同编译器会有两种方式:头部和尾部。对于放在头部的编译器,如果这里给B类增加一个virtual destructor,从而让A无virtual机制而让B有virtual机制,class object b的头部就是vptr而不是class A subobject了。但这不会影响指针的相同性。

3)如果B是virtual继承于A,则事情另有变数。用Stanley B. Lippman的话说“任何规则一旦遇到virtual base class,就没辙了”。这里我们不讨论这个题外话。

&b、a和b2所指向的都是b的首地址。因此,在SI模型下,对象内存采用重叠的模型,基类和任何的派生类的指针,都指向该对象的首地址,因此这些指针的地址值都是一样的——所有基类subobject都共享相同首地址。

也就是说,在一个继承体系内,不论你用什么样的方式对一个指向了某对象的指针进行downcast或upcast,指针的地址值都是一样的。再加一层体系如下:

[cpp] view plain copy
  1. class C : public B
  2. {
  3. public:
  4. int m_d;
  5. };

A、B、C这3个在同一体系下的类的指针无论怎样进行相互casting,得到的地址都一样。

多继承(Multiple Inheritance,MI)对象模型

MI机制是C++这门语言的特性之一,同时,也是复杂度的罪魁祸首之一!因为,在这里,编译器又背着我们做了一些事情,这也是C++饱受批评的主要原因。

请考虑下面的程序:

[cpp] view plain copy
  1. class A
  2. {
  3. public:
  4. int m_a;
  5. };
  6. class B
  7. {
  8. public:
  9. int m_b;
  10. };
  11. class C
  12. {
  13. public:
  14. int m_c;
  15. };
  16. class D : public A, public B, public C
  17. {
  18. public:
  19. int m_d;
  20. };
  21. A* a;
  22. B* b;
  23. C* c;
  24. D d;
  25. a = &d;
  26. b = &d;
  27. c = &d;

类关系如图所示:

这是一个最简单的MI体系,D继承自三个base class。我们再来看看它的内存模型:

可以看到,和SI不同的是,MI采用了非重叠模型——每个base class subobject都有自己的首地址。这里,A、B和C subobject各自占据它们自己的首地址,唯一的例外就是D object——也就是这个模型的拥有者,它的首地址和class A subobject是相同的。因此,我们说:

assert( a == &d );

assert( b != &d );

assert( c != &d );

“哎!等等!”,我听到了你在打断我,“我们在上面的程序中已经写明

b = &d;

c = &d;

这里为什么你会这么写:

assert( b != &d );

assert( c != &d );

你确定断言不会crash吗?”。如果你这么问我,我很高兴,这表明你在跟着我。下面是我通过试验得到的数据:

这就是问题的关键所在——编译器背着我们做了一件事情:this指针调整!在MI的世界里,this指针调整非常频繁,而这种调整,主要发生在 派生类对象 和“第二个以及后续的基类对象”(像咒语一样)之间的转换。在上面的例子里,“第二个以及后续的基类”就是类B和C。这个转换就是

b = &d; // upcast

c = &d; // upcast

this指针就是在这个时候被compiler调整的。b和c分别指向了正确的,属于它们各自的subobject的地址。同理,当我们将b和c转换成d指针的时候,this指针也会调整

D* d2 = static_cast<D*>(b); // downcast

D* d3 = static_cast<D*>(c); // downcast

结果是:

assert( d2 == &d );

assert( d3 == &d );

指针又被调整了回来。而这在SI的世界中是不会发生的(重叠模型)。

为什么要调整this指针呢?this指针调整的原因在于MI采用了非重叠的内存模型,而之所以采用这种模型,是为了保证各基类体系的完整性和独立性,保证virtual机制能够得以在MI的不同体系之间顺利运行(这通过每个subobject各自的vptr进行)。关于MI以及它的this指针调整,可以说的东西足够写成一本书(本文只是冰山一角),这里当然不行!关于MI的任何理论问题,你都可以在《Inside The C++ Object Model》一书中找到。

但是,如果你把上面我们讨论的理论都弄明白了,就足够理解下面的部分,以及一般的MI问题了。

问题分析

在掌握了SI和MI各自的基本知识之后,我们现在可以把之前的问题弹出堆栈!我们暂时离开实验室,来分析一下这个现实生活中的问题。Actor的继承体系如下所示:

老办法,我们分析一下它的内存模型:

该体系是一个SI和MI的混合体。可以把Actor看成是左右两个体系的MI类。Super hierarchy和EventedSprite这个SI作为第一个base class,ConcurrentJob和Path_Finder的这个SI看做是第二个base class。因此,

class Actor : public EventedSprite, public Path_Finder {...}

有关系:

Actor actor;

EventedSprite* spr = &actor; // 1

Path_Finder* path = &actor; // 2

assert( spr == &actor );

assert( path != &actor );

因为步骤2进行了this指针调整——这很清楚。好了,我们来看我们出问题的程序:

[cpp] view plain copy
  1. My_Task* myTask = new My_Task;
  2. Actor* actor = new Actor;
  3. myTask->setJob( actor );
  4. System_Task_Manager_Or_Something->addToOperationQueue( myTask );

我们将actor交给了My_Task::setJob这个方法,该方法的形参是类型void*——它可以接受任何指针类型,这没有什么问题——我们只需要存储这个地址,在需要使用的时候用就是了。我们再看My_Task::run:

[cpp] view plain copy
  1. virtual void run()
  2. {
  3. ConcurrentJob* job = static_cast<ConcurrentJob*>( m_job );
  4. job->run();
  5. }

m_job就是刚才被存储的Actor*——这个地址没问题。但,m_job的类型是void*——没有任何类型信息!我们应该对

ConcurrentJob* job = static_cast<ConcurrentJob*>( m_job );

有什么期待呢?我们期待compiler会为我们调整this指针!因为ConcurrentJob是第二个基类体系,还记得 “第二个以及后续的基类”咒语吗?一个void*的指针,我们用编译期casting  operator

static_cast

进行转换,是不会有任何地址上的变化的!Actor*的地址就这么直接赋予了ConcurrentJob*。this指针没有被调整!这个指针没有指向正确的subobject!导致了后面的严重错误!

一个快速的解决方案就是把m_job先变成Actor*,然后再转换。不过无论如何,只要能够给compiler足够的类型信息量,它就能做对事情——但前提是,你要先做对事情。

compler moves this pointer while Upcasting derived ojbect pointe to parent pointe by static_cast相关推荐

  1. static_cast、dynamic_cast、reinterpret_cast、const_cast[转]

    C-style cast举例: int i; double d; i = (int) d; 上面的代码就是本来为double类型的d,通过(int)d将其转换成整形值,并将该值赋给整形变量i (注意d ...

  2. COMP 3023代写、代写COMP 3023、代做 C++ - Assignment、 代编码C++ - Assignment

    COMP 3023代写.代写COMP 3023.代做 C++ - Assignment. 代编码C++ - Assignment Revision 1 COMP 3023 Software Devel ...

  3. COMP 3023 国外作业代写、C++ - Assignment代写、代做留学生Software Development程序作业、代做C/C++作业...

    COMP 3023 国外作业代写.C++ - Assignment代写.代做留学生Software Development程序作业.代做C/C++作业 Revision 1 COMP 3023 Sof ...

  4. C++ 强制类型转换操作符(static_cast、dynamic_cast、const_cast和reinterpret_cast)

    C++中的四种操作符形式类型转换 1.static_cast (静态类型转换) 主要使用场景:适用于将void*转换为其他的指针 int a = 100; void* pv = &a; //i ...

  5. 网易云课堂_C++程序设计入门(上)_第6单元:丹枫虽老犹多态–继承与多态

    第01节:继承 第02节:构造函数和析构函数 第03节:函数重定义 第04节:多态和虚函数 第05节:访问控制 (可见性控制) 第06节:抽象类与纯虚函数 第07节:动态类型转换 第01节:继承 回顾 ...

  6. C,C++面试题之2

    华为面试题及答案 1.局部变量能否和全局变量重名 答:能,局部会屏蔽全局.要用全局变量,需要使用"::" 局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量, ...

  7. iOS interview questions and Answers

    http://gksanthoshbe.blogspot.com/2013/03/ios-interview-questions-and-answers.html 1-How would you cr ...

  8. When should static_cast, dynamic_cast and reinterpret_cast be used?

    这是我偶然在 http://stackoverflow.com/questions/ 网页上发现的一个问题(类似博客园的博问),问题主要是关于询问应该怎样使用,以及何时使用C++里面的这几种类型转换操 ...

  9. pybind11介绍

          pybind11是一个轻量级的仅头文件库,主要用于创建现有C++代码的Python绑定,它的源码在https://github.com/pybind/pybind11,license为BS ...

最新文章

  1. Go 学习笔记(11)— 切片定义、切片初始化、数组和切片差异、字符串和切片转换、len()、cap()、空 nil 切片、append()、copy() 函数、删除切片元素
  2. http 403错误解决
  3. linux日常运维手册_Linux日常运维上传下载工具lrzsz
  4. 网络工具中的瑞士军刀——netcat工具简介
  5. raid5和raid6对比
  6. jQuery获取Table某列的值
  7. Vue学习(常用实例、脚手架搭建)-学习笔记
  8. javafx隐藏_JavaFX技巧14:StackPane子项-隐藏但不消失
  9. atom插件安装方法
  10. Web Audio API 入门1
  11. linux 进程 转存储,Linux memory management——(进程虚存空间的管理)(转)
  12. 打印机修复工具_Windows10更新后打印机崩溃?速度安装修复补丁
  13. python流程图自动生成_python自动化办公 自动生成PPT通报
  14. 解决办法:java.lang.UnsatisfiedLinkError: org.opencv.core.Mat.n_eye(III)J
  15. 绝地求生最新服务器维护,绝地求生更新维护公告最新:3月10日吃鸡停机维护多久 几点开始?...
  16. C语言 “百鸡问题”最优解
  17. python爬虫登录正方教务管理系统获取成绩数据_「武汉理工大学教务处管理系统」Python爬虫初学(4)登陆武汉理工大学教务处并转到成绩管理 - seo实验室...
  18. 无线网服务器断开怎么回事,无线网自动断开怎么回事
  19. word2vec的经验总结
  20. 注册时验证用户名和密码是否合法

热门文章

  1. python3.6 pyqt4_当我使用Python3.6时,没有名为PyQt4的模块matplotlib.pyp
  2. 基于多智能体深度强化学习的空地协同通信系统轨迹设计与访问控制
  3. 权威认可 | 通付盾再次入选工信部“CAPPVD安全漏洞库技术支撑单位”!
  4. 支付宝支付 - 电脑网站支付
  5. 如何用计算机制作公式,计算机制作报表时,计算公式定义可采用人工输入也可采用参照输入。...
  6. python 状态机设计(聊聊transitions)
  7. dir /s真是个神奇的存在
  8. 【洛谷】P2196 挖地雷
  9. MySQL8.0_JDBC笔记
  10. 重庆北大青鸟解放碑校区J12班 chickenNice队【游戏账号交易平台】