Data 语义学(2)
四、"继承"与 Data Member
(1). 只有继承没有多态
先来看个例子
- #include <iostream>
- using namespace std;
- class A
- {
- public:
- //
- private:
- int val;
- char c1;
- char c2;
- char c3;
- };
- int main()
- {
- cout << sizeof(A) << endl;
- return ;
- }
输出的结果是 8 ,这个比较容易理解:
a. val 占用 4 bytes。
b. c1,c2,c3各占 1 byte,3 个 bytes。
c. 边界调整(alignment)---调整到 word 需要 1 byte
一共 8 个!那么在看下面这个例子:
- #include <iostream>
- using namespace std;
- class A
- {
- public:
- //
- private:
- int val;
- char c1;
- };
- class B: public A
- {
- public:
- //
- private:
- char c2;
- };
- class C: public B
- {
- public:
- //
- private:
- char c3;
- };
- int main()
- {
- cout << sizeof(C) << endl;
- return ;
- }
这下输出结果是多少呢?可以肯定的是,C中也含有 val、c1、c2 和 c3,那么输出结果是不是也是 8呢?当然不是,是 16 !为什么呢?解释如下:
C++ 规定出现在 derived class 中的 ”base class subobject“ 有其完整原样性。我们从下至上看看各个类在内存中的布局!
class A 中含有两个变量 val 和 c1,其中 val 占有 4 个 bytes,c1 占有 1 个byte加起来 5 个 bytes。再加上边界调整填补 3 bte,实际会占用 8 byte。
class B 继承了 class A 并增加了一个自己的 nonstatic data member c2,类型时 char,占有 1 byte,轻率的程序员会以为:c2 会 c1 放到一起构成 2 bytes,这样内存补齐只需补 2 bytes,class B的大小应该还是 8 bytes。然而,class B 的 c2 是被放在 class A 所填补的 3 bytes 的后面,此时 class B 的大小是 9 bytes ,在进行边界调整填补 3 bytes,实际会占用 12 bytes。同样 class C 的大小是 16 bytes ,class B 的大小 + c3 + 边界调整!下图会让你一目了然:
图 1
很多人会认为这种做法很愚蠢,在 class B 继承 A 的时候,将 c1 和 c2 放在一起,填补 2 bytes,在 class C 继承 class B 的时候,将 c1、c2 和 c3放到一起,填补 1 byte,这样 class A、class B、class C 的大小都是 8 bytes,比之前那样省很多空间!!!事实上是如此吗?这种做法会破坏C++中规定的 “出现在 derived class 中的 ”base class subobject“ 有其完整原样性”,会是一些代码导致致命的错误,不信你看:
加入我们声明以下一组指针:
B *pb;
A *pa1,*pa2;// 这两个指针可以用来指向 class A,B,C。
如果执行操作:
*pa1 = *pa2;
应该执行一个默认的“memberwise”复制操作(复制一个个的 members)对象是值得 class A 的那部分哦。如果 pa1指向一个 class B 或 class C ,那么就会将 pa2 所指的内容中 class A 的部分复制给 class B 或 class C 的 class A 的部分!如果将 A、B 和 C 的c1、c2 和 c3 捆版到一起,去填补空间,上述那些语义就没办法保留了!由于捆绑而导致的内存复制出现错误,请看下图,会更容易理解:
所以知道,C++设计者们为什么宁愿浪费空间而不那么做的原因了吧?
(2) 加上多态
加上多态之后会使很多操作起来更有方便性,使动作在运行期决定。这使得 class 更加完美,但我们必须维持付出代价:
a. 导入一个相关的 virtual table,用来存放他 class 所声明的每一个 virtual function 的地址。这个 table 元素的个数 = virtual function 的数目 + 1 或 2(用以支持 runtime identification)
b. 在每一个 class object 中导入一个 vptr ,提供执行期的连接,使每个 object 能够找到相应的 virtual table。
c. 加强 destructor,使它能够销毁那个被安插进去的 vptr。
那这个指针到底放在哪呢?看如下代码:
- class A
- {
- public:
- int num;
- virtual void fun(){};
- }
- class B
- {
- public:
- int num;
- int num1;
- virtual void fun(){}
- }
这时 class A 和 class B 的布局可能是这样:
也有可能是这样
如果我对 class A 和 class B 进行如下该写,那么 A 和 B 的布局又会是什么样子呢?
该写如下:
- class A
- {
- public:
- int num;
- virtual void fun(){};
- }
- class B: public A
- {
- public:
- int num1;
- virtual void fun(){}
- }
那么 class A 和 class B 的布局会如下:
图2 还有可能。。。另外一种就不画了,只是把vptr 移到 num 上去罢了!
从这个图中可以发现什么?对无论继承多少次 vptr 在所有的 base class 或者 derived class 中都只有一份!并且位置都相同,但是他们(vptr)指向的 virtual table 不相同哦!都指向内存中各自的 virual table 调用时分别从各自的 virtual table 中找 virtual functions 的地址进行调用!实现了多态的机制哦!
(3). 多重继承
还是先来看几个类的定义:
- class base1
- {
- public:
- // something
- protected:
- float fb1;
- };
- class dbase1: public base
- {
- public :
- // something
- protected:
- float fdb1;
- };
- class base2
- {
- public:
- // something
- protected:
- float fb2;
- };
- class Topclass :public dbase1,public base2
- {
- public:
- // something
- private:
- float muble;
- };
他们的关系图如下
图 3
他们在内存中的布局图如下(假设他们都有虚函数哈,给他们画了 vptr 指针):
图4
由他们的内存布局图可知,在class Topclass 中取 class base1 和 class dbase1 的时候可以直接取到,而取class base2就必需要做相应的转化了。请看下面的例子:
Toplass tc;
base1 *b1;
dbase1 *db;
base2 *b2;
如果是 b1 = &tc或 db = &tc都是和单继承一样的;但如果是 b2 = &tc的操作就需要相应的转化咯,如下:
pv = ((Topclass *)((char *) &tc)) + sizeof( dbase2);
而如果将 tc定义成 Topclass *tc;呢?那么相应的操作是否是 pv = ((Topclass *)((char *) tc)) + sizeof( dbase2);呢?只能说你这样对了一半,应该是这样:
pv = tc ? ((Topclass *)((char *) &tc)) + sizeof( dbase2) : 0;
因为 tc 有可能是空指针哦。至于 reference 就不会这样了,因为不可能有一个空的引用!
(4). 虚拟继承
//一下可能有误,回头看
之前我们就对虚拟继承的内存比较好奇,而且在我的博文中未讲,我们只知道会生成一个指针和一个 virtual base classs table 那这些东西到底是什么样子呢?今天将详细的展示给你们看:
- #include <iostream>
- using namespace std;
- class A
- {
- public:
- //
- virtual void foo(){};
- private:
- int val;
- };
- class B: public A
- {
- public:
- //virtual void foo(){}
- private:
- };
- int main()
- {
- cout << sizeof(A) << endl;
- cout << sizeof(B) << endl;
- return ;
- }
以上是一个一般继承的代码,程序输出的结果大家也知道是什么了,是 8 和 8,他们内存布局图也就是我们"(2)加上多态"图 2 所画的那样。那么我稍作如下修改:
- #include <iostream>
- using namespace std;
- class A
- {
- public:
- //
- virtual void foo(){};
- private:
- int val;
- };
- class B: virtual public A //这里将继承改成了虚拟继承
- {
- public:
- //virtual void foo(){}
- private:
- };
- int main()
- {
- cout << sizeof(A) << endl;
- cout << sizeof(B) << endl;
- return ;
- }
这个代码将输出什么呢?结果却变成了 8 和 12 啦,为什么呢?我画一下虚拟继承后的他们的内存布局:
图 5
你将这幅图和上面"(2)加上多态"那里画的图 2 对比一下,会发现这里会多了一个 _vptr_B 指针啦(我将上面的 _vptr 后面 加上了 _A 或 _B 以区别他们,这个没影响)。这也就是为什么上面第二个代码输出的结果是 8 和 12 了。那为什么要这么做呢?以下是我通过 vc 调试和书上所述的方式总结出来的:
我们之前都有说过 virtual base class 是虚基类它在 derived class 中访问并不能像其他,而是另外安插了一个指针,该指针指向一个 virtual base class table 这个表放的是 virtual base class 在 derived class 中的 offset !这个指针就是上面所说的 _vptr_B。(在我的 VC 下)这个 _vptr_B 有两个作用:第一、用来指向 class B 的 virtual function table 。第二、用来指向 class B 的 virtual base class。有人说一个指针怎么会指向两个表呢?请看下图:
图 6
看上图明白了吧?virtual function table 和 virtual base class table 首地址是同一个。两个表合到一起了,我们就叫 virtual function table ,表的首地址在分界处,当取正索引时,取的是 VFT slots ,当取负索引时,取的是 VBC offset。这样就可以方便的访问虚基类和实现多态了!VBC offset 是指virtual base class 在 derived class中的偏移量哦!---不过大家注意了,这只是 VC 下的一个用来共享虚基类的一种方法,别的编译器可能不是采用这种方法。但是都大同小异了,都是为了能够方便的在 derived class 中访问共享的 virtual base class!
Data 语义学(2)的更多相关文章
- Data 语义学(1)
一.Data Member 的绑定(The binding of Data Member) extern float x; class Point3d { public: Point3d( float ...
- C++对象模型(五):The Semantics of Data Data语义学
本文是<Inside the C++ Object Model>第三章的读书笔记.主要讨论C++ data member的内存布局.这里的data member 包含了class有虚函数时 ...
- 【C++对象模型】第三章 Data语义学
1. Data Member 的布局 同一个Access Section(private, public等)中,data member的顺序按照声明顺序排列,但是没有规定需要连续排序.同时编译器可能会 ...
- 【深入探索c++对象模型】data语义学二
单一继承中,base class 和derived class的对象都是从相同的地址开始,其间差异只在于derived class比较大,用以容纳自己的nonstatic members. 若vptr ...
- 【深度探索C++对象模型】data语义学
class X{}; class Y :public virtual X{}; class Z :public virtual X{}; class A :public Y, public Z{}; ...
- 深入探索C++对象模型(三)
Data 语义学 一个class的data members,一般而言,可以表现这个class在程序执行时的某种状态.Nonstatic data members放置的是"个别的class o ...
- Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)
前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transfor ...
- CYQ.Data、ASP.NET Aries 百家企业使用名单
如果您或您所在的公司正在使用此框架,请联系左侧的扣扣,告知我信息,我将为您添加链接: 以下内容为已反馈的用户,(收集始于:2016-08-08),仅展示99家: 序号 企业名称 企业网址 备注 1 山 ...
- 终于等到你:CYQ.Data V5系列 (ORM数据层)最新版本开源了
前言: 不要问我框架为什么从收费授权转到免费开源,人生没有那么多为什么,这些年我开源的东西并不少,虽然这个是最核心的,看淡了就也没什么了. 群里的网友:太平说: 记得一年前你开源另一个项目的时候我就说 ...
随机推荐
- FaceBook要在视频领域打败YouTube?
据<纽约时报>报道,FaceBook正在探索一项新的策略来直接把音乐视频嵌入到用户的News Feeds中.目前,具有代表性的视频网站有YouTube和Vimeo,它们可以在社交网络上分享 ...
- jQuery作用
jquery是前端里面比较总要的,是很强大的一个选择器. 表单: 1.$(":input") 查找所有的input元素 2.$("text") 匹配所有的 ...
- CTE-递归[2]
在此之前写过一个CTE的递归,取出了所有的子节点,基本上可以满足大多数的需求,这里我们来延伸一下:首先我们回顾下原来的场景 图片的上半部分递归查出某个节点的所有子节点,这个我们已经通过CTE实现了,可 ...
- oracle数据库读取操作系统的物理文件-转载,待完善
--源地址不详 --创建目录SQL> create directory dir_xls as '/home/oracle'; Directory created. --给用户授权SQL> ...
- javaScript 删除数组中指定元素
Array.prototype.indexOf = function(val) { for (var i = 0; i < this.length; i++) { if (this[i] == ...
- java 迭代器iterator
对于如ArrayList<E>类的数据,常用iterator遍历. ArrayList<String> list = new ArrayList<String>() ...
- 注解SpringMVC
<!--注解映射器 --> <bean class="org.springframework.web.servlet.mvc.method.annotation.Reque ...
- Sqoop import加载HBase过程中,遇到Permission denied: user=root, access=WRITE, inode="/user":hdfs:supergroup:drwxr-xr-x
在执行hbase sqoop抽取的时候,遇到了一个错误,如下图: 在执行程序的过程中,遇到权限问题很正常,也容易让人防不胜防,有问题就想办法解决,这个是关键. 解决办法如下: 第一步:su hdfs, ...
- hdoj 2047 简单递推
代码: #include <stdio.h>int main(){ int n,m,i; __int64 x[41]; x[1]=3; x[2]=8; for(i=3;i<=40;i ...
- POJ 1286 Necklaces of Beads (Burnside定理,有限制型)
题目链接:http://vjudge.net/problem/viewProblem.action?id=11117 就是利用每种等价情形算出置换节之后算组合数 #include <stdio. ...