@(Cplusplus)
相对于前一章的内容,这一章还是比较晦涩,不是内容难,而是这里面这几不同的编译器,不同版本的编译,对程序的理解和编译结果有差异,差异多了自然就记不住了,所以,这一章我认真的看了两遍,并且xmind画了思维导图。
导读
每一章都有一些篇幅是导读内容,用来介绍这章要讲的是什么。这章的导读比较有意思,不是说特别有趣,而是用程序写出来很容易验证,并且记住这段内容。
给出的代码是这样的:1
2
3
4class X{};
class Y:public virtual X{};
class Z:public virtual X{};
class A:public Y,public Z{};
在主函数中可以输出并验证一下:1
2
3
4
5
6
7
8X x;
Y y;
Z z;
A a;
cout<< sizeof x<<endl;
cout<<sizeof y<<endl;
cout<<sizeof z<<endl;
cout<<sizeof a<<endl;
我是在window
环境下测试的,两种编译器:Visual Studio
和Windows版本的gcc
— MinGW
。运算结果都是一样的:1
2
3
41
4
4
8
这个导读其实我比较关注三个问题:
- 空的类的对象大小到底是多少?答案是
1
,这个空间是编译器安插进去的一个字节的空间,用来区分这个类的不同的对象。(后面会有彩蛋) - 类
Y
和Z
的对象的大小是多少呢?同样,这两个类也没有数据成员,当前我的编译器给出的都是4,这个四个字节存放的是个什么鬼?我是编译的32位程序,地址也就是32位,这4个字节存放的是指针,因为是虚继承,所以,这个指针指向的是虚基类子对象的地址,(若是多个虚继承,就指向一个表格,表格存放所有虚基类子对象的地址或者是偏移量) - 对齐(Alignment)限制,32位的应用程序的对齐大小是4个字节,也就是说若一个类的对象的字节数若不是4的整数倍,需要补齐,这是为了方便处理器进行计算。
彩蛋呢?既然不是4的整数倍就要对齐,那么空类的对象为什么不补齐?我感觉书上给出的有点迷惑,书中原话是对于空类的对象:
编译器要安插一个
char
我理解的是这里不应该说是一个char
,应该是一个字节的空间,不知道是个什么东西,就是用来占据内存的一个空间,这样,空类类型的指针也可以指向内存中一个位置(就是这个一个字节,不存储任何数据),同时,同一个类的不同对象由于分配不同的一个字节的空间,也就能区分不同的对象了。我是这样理解的。
书上来信者的编译器给出的答案是1 8 8 12
,这里的8=1+4+3
,其中4是指向基类子对象的指针,3是对齐产生的,1是因为父类为空,编译器给了它一个字节的空间。是从父类继承而来的。其实我认为这样解释不妥,一个字节的空间实际是在创建对象的时候分配的,并不是类本身具有的,我认为这里的1个字节的空间也是因为类Y
和Z
是空类,所以编译器也给他们添加了一个字节的空间。其实作者在86页的解释也应该是证明这一点的。我身边没有这样的编译器,否则我应该做一个实验验证一下。
现代的编译器采用了一种新的策略:它将空的虚基类作看作是一个虚接口(virtual interface
),空的虚基类视为其派生类的一部分(最开始的部分),这样派生类里面指向虚基类子对象的指针就被视为派生类的第一个成员了,而且这是一个数据成员,以前则是把虚继承当作是一种额外的负担
,编译器自动处理这种负担,就是给每个对象添加一个指针。那么既然现在这个指针是派生类的成员了,派生类数据成员不为空了,所以,也就没有编译器安插的一个字节空间了,所以派生类对象大小是4.
上次写到这里!今天开题答辩,挺累的,还是写点吧
至于最后一个对象的大小,是12,书中的解释是因为共享了虚基类的一个字节,其实,我还是认为这个一个字节不是在虚基类添加的,而是虚基类对象添加的(用来区分不同的对象)。我的解释是这里之所以是12,是因为编译器还没有优化,没有将指向虚基类对象的指针看作是派生类的一部分,因此派生类也是空类,编译器需要给他们一个字节的空间,这样,继承了两个这样的类的A,就需要(4+1)*2
空间,在由编译器进行对齐,就变成了12字节。
对于优化了的编译器,为什么是8?原因是指向虚基类的指针被当作成员了,那么派生类就不是空类了,他们的对象的空间大小是4,两个相加就是8咯!
我觉得我的解释还是比较有道理,但是没有这样的编译器,也就不好验证,而且目前都是优化了的,也就没有太大意义去钻这个牛角尖了。
临时插一句话,这本书讲的还是比较有深度的,今天身体实在是不舒服,而且要回宿舍洗澡,这一章的内容还是差一些才能看完,预计还需要一到两天,而且白天我要写论文。
数据成员的绑定
这一小节的内容还是比较晦涩的,原因是没办法验证。而且我不知道这个对我的帮助有多大,我还是不了解这条规则的意义。
接着写
数据成员的布局
这一节主要介绍了对象在内存的布局,对象只存储非静态的数据成员,多个数据成员的排列是相对有序的,按其声明的顺序排列,但是不一定shiite连续的,中间也许会存取一些别的东西,比如:指向虚函数表的指针、边界补齐的对齐字节。
另外,在C++中有访问控制符,每种访问控制符access section
可以出现多次,但是不会带来额外的负担。
这一节的内容就是这么少,其实用图形可能描述的更好。
数据成员的存取
这一节比较有意思,用一个例子引起的,其实整节内用就可以用这个例子来描述。
1 | Point3d origin,*pt = &origin; |
例子中给出的非静态数据成员的访问。非静态数据类型存储在对象中,因此只能通过对象或者对象的指针访问非静态数据类型。
非静态数据类型的地址,是对象的地址添加上数据成员的了偏移(offset
),这里的偏移是要减去一个1的,这一点在后面会讲到,我这里提前说了。
这里提到的偏移实际上是数据成员的指针(data member指针),与普通的指针不同,这个对象指针是相对于对象的地址,也就是这个数据成员的偏移地址。
指针可以指向空,数据成员指针也是一样的,这里为了区分数据成员的指针是指向第一个成员(第一个成员的偏移地址是0),还是什么都没指向(通常设为0),就
把指向成员的数据成员指针加1,实际使用的时候再减1.
数据成员的地址定义如下:Point3d::*
。一个成员的成员指针是&Point3d::y
。可以通过&origin + (&Point3d::y -1)
获取数据成员的地址。
每个数据成员的偏移地址,也就是数据成员指针一般是在编译时期就可以获得的。所以这样的情况下,用对象访问数据成员和用指针访问数据成员代价是一样的。
但是当出现了虚基类,存取是虚基类的数据成员,那就不一样了,因为指针不一定是指向的哪一个类类型,在编译的时候就不能计算出数据成员的偏移量,偏移量的计算会推迟到执行阶段。所以,这种情况下使用类的对象访问数据成员与指针访问数据成员速度不一样,指针是要慢的。
“继承”与数据成员
这一节就是把多种情况介绍了一下,其实也没啥,单一继承没什么,多继承也没什么,有虚函数就是多了一个指向虚表的指针,最麻烦就是有虚基类的,这时候就要分为共享部分和不变部分,所谓的共享部分就是指的虚基类的子对象,所有派生类的对象都只有一个虚基类的子对象实例。关键点是这个共享部分怎么找到。
对象成员的效率
这节没啥意思,就是讲的添加封装,多态等特征后,相对于直接访问,C语言结构体访问,访问代价,但是编译器添加了优化,几乎都是一样的了。
指向对象成员的指针
这个刚才介绍了,就是相对于对象的指针,也就是偏移,为了区分没有指向数据成员的指针和指向数据成员的指针,数据成员指针都要加1.
总结
这一章的内容就是这些,不是很多,但是需要记忆的挺多的,有些技巧还是需要理解,比如在面试的时候如何访问一个对象的私有成员,应该回答是用指针,但是如果把数据成员指针也给解释一通,那就更好不过了。