扫描二维码
随时随地手机看文章
int ( *pq ) ( ); //声明
当语言无法区分那是一个声明还是一个表达式时,我们需要一个超越语言范围的规则,而该规则会将上述式子判断为一个“声明“
struct和class可以相互替换,他们只是默认的权限不一样
如果一个程序员需要拥有C声明的那种struct布局,可以抽出来单独成为struct声明,并且和C 部分组合起来
1.3 对象的差异
C 支持三种程序范式:程序模型、抽象数据类型模型、面向对象模型
面向对象模型在继承体系中 ,有时候编译期间无法确定指针或引用所指类型
C 支持的多态类型:
经由一组隐式的转化操作:如派生类指针转化为指向父类的指针
经由虚函数机制
经由dynamic_cast 和 typeid运算符
一个class所占的大小包括:
其非静态成员所占的大小
由于内存对齐填补上的大小
加上支持虚函数而产生的大小
指针的类型,只能代表其让编译器如何解释其所指向的地址内容,和它本身类型无关,所以转换其实是一种编译器指令,不改变所指向的地址,只影响怎么解释它给出的地址
当一个基类对象被初始化为一个子类对象时,派生类就会被切割用来塞入较小的基类内存中,派生类不会留下任何东西,多态也不会再呈现。
二、构造函数语意学
2.1 默认构造函数的构造操作
以下四种情况下,会合成有用的构造函数:
类声明(或继承)一个虚函数
类派生自一个继承串链,其中有一个或更多的虚基类
带有默认构造函数的成员函数对象,不过这个合成操作只有在构造函数真正需要被调用时才发生,但只是调用其成员的默认构造函数,其他则不会初始化
如果一个派生类的父类带有默认构造函数,那么子类如果没有定义构造函数,则会合成默认构造函数,如果有的话但是没有调用父类的,则编译器会插入一些代码调用父类的默认构造函数
带有一个虚函数的类
带有一个虚基类的类
C 新手常见的两个误解:
任何class如果没有定义默认构造函数,就会被合成出来一个
编译器合成出来的默认构造函数会显式设定类中的每一个数据成员的额 默认值
2.2 拷贝构造函数的构造操作
有三种情况会调用拷贝构造函数:
对一个对象做显式的初始化操作
当对象被当作参数交给某个函数
当函数传回一个类对象时
如果类没有声明一个拷贝函数,就会有隐式的声明和隐式的定义出现,同默认构造函数一样在使用时才合成出来
什么情况下一个类不展现“浅拷贝语意”:
编译器会合成一个拷贝构造函数,安插一些代码用来设定虚基类指针和偏移的初值,对每个成员执行必要的深拷贝初始化操作,以及执行其他的内存相关工作
编译器会显式的设定新类的虚函数表,而不是直接拷贝过来指向同一个
这两个编译器都会合成拷贝构造函数并且安插进那个成员和基类的拷贝构造函数
当类内含有一个成员类而后者的类声明中有一个拷贝构造函数(例如内含有string成员变量)
当类继承自一个基类而基类中存在拷贝构造函数
当类声明了一个或多个虚函数
当类派生自一个继承串链,其中有一个或多个虚基类
2.3 程序转化语意学
在将一个类作为另一个类的初值情况下,语言允许编译器有大量的自由发挥的空间,用来提升效率,但是缺点是不能安全的规划拷贝构造函数的副作用,必须视其执行而定
拷贝构造的应用,编译器会多多少的进行部分转换,尤其是当一个函数以值传递的方式传回一个对象,而该对象有一个合成的构造函数,此外编译器也会对拷贝构造的调用进行调优,以额外的第一参数取代NRV(Named Return Value)
2.4 成员们的初始化队伍
四种情况下你需要使用成员初始化列表
当初始化一个引用成员变量
当初始化一个const 成员变量
当调用一个基类的构造函数,而它拥有一组参数
当调用一个类成员变量的构造函数,而它拥有一组参数
class Word{
String _name;
int _cnt;
public:
Word(){
_name = 0;
_cnt = 0;
}
/*使用成员列表初始化可以解决
Word() : _name(0),_cnt(0){
}
*/
}
上式不会报错,但是会有效率问题,因为这样会先产生一个临时的string对象,然后将它初始化,之后以一个赋值运算符将临时对象指定给_name,再摧毁临时的对象
-
成员初始化列表中的初始化顺序是按照类中的成员变量声明的顺序,与成员初始化列表的排列顺序无关
3、Data语意学
class X{};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y,public Z {};
sizeof(X) //1
sizeof(Y) //4
sizeof(Z) //4
sizeof(A) //8
-
X为1是因为编译器的处理,在其中插入了1个char,为了让其对象能在内存中有自己独立的地址
-
Y,Z是因为虚基类表的指针
-
A 中含有Y和Z所以是8
-
每一个类对象大小的影响因素:
-
非静态成员变量的大小
-
virtual特性
-
内存对齐
3.1 数据成员的绑定
-
如果类的内部有typedef,请把它放在类的起始处,因为防止先看到的是全局的和这个typedef相同的冲突,编译器会选择全局的,因为先看到全局的
3.2 数据成员的布局
-
非静态成员变量的在内存中的顺序和其声明顺序是一致的
-
但是不一定是连续的,因为中间可能有内存对齐的填补物
-
virtual机制的指针所放的位置和编译器有关
3.3 成员变量的存取
-
静态变量都被放在一个全局区,与类的大小无关,正如对其取地址得到的是与类无关的数据类型,如果两个类有相同的静态成员变量,编译器会暗自为其名称编码,使两个名称都不同
-
非静态成员变量则是直接放在对象内,经由对象的地址和在类中的偏移地址取得,但是在继承体系下,情况就会不一样,因为编译器无法确定此时的指针指的具体是父类对象还是子类对象
3.4 继承下的数据成员
-
在下面给定的两个类中依次讨论不同情况:
原本的数据模型
-
在单一继承没有虚函数的情况下布局图
单一继承且无虚函数
-
这种情况下常见错误:
-
可能会重复设计一些操作相同的函数,我们可以把某些函数写成inline,这样就可以在子类中调用父类的某些函数来实现简化
-
把数据放在同一个类中和继承起来的内存布局可能不同,因为每个类需要内存对齐
-
叠在一起的内存布局
分层继承的布局
可见内存大了100%
-
容易出现的不易发现的问题:
继承下易犯错误
-
当加上多态之后,对空间上增加的额外负担包括:
-
析构函数的调用顺序是反向的,从子类到父类
-
导入一个虚函数表,表中的个数是声明的虚函数的个数加上一个或两个slots(用来支持运行类型识别)
-
在每个对象中加入vptr,提供执行期的链接,使每一个类能找到相应的虚函数表
-
加强构造函数,使它能够为vptr设定初值,让它指向对应的虚函数表,这可能意味着在派生类和每一个基类的构造函数中,重新设定vptr的值
-
加强析构函数,使它能够消抹“指向类的相关虚函数表”的vptr,vptr很可能以及在子类析构函数中被设定为子类的虚表地址。
-
以下是三种情况不同的继承下会有不同的布局
-
Vptr放在尾端
-
-
vptr放在前端
-
含虚函数的数据分布
-
多重继承
-
**单一继承特点:**派生类和父类对象都是从相同的地址开始,区别只是派生类比较大能容纳自己的非静态成员变量
-
多重继承下会比较复杂
多重继承关系
-
一个派生对象,把它的地址指定给最左边的基类,和单一继承一样,因为起始地址是一样的,但是后面的需要更改,因为需要加上前面基类的大小,才能得到后面基类的地址
多重继承数据分布
-
虚继承
-
STL标准库中使用的虚继承:
-
虚继承关系图
-
-
虚继承关系:
虚继承例子
-
虚继数据在内存中的分布
-
虚继承数据模型
-
-
虚继承数据模型2
-
3.5 对象成员的效率
-
程序员如果关心程序效率,应该实际测试,不要光凭推论、常识判断或假设。
-
优化操作并不一定总是能够有效运行,我不止一次以优化方式来 编译一个已通过编译的正常程序,却以失败收场
3.6 指向数据成员的指针
-
vptr通常放在起始处或尾端,与编译器有关,C 标准允许放在类中的任何位置
-
取某个类成员变量的地址,通常取到得的是在类的首地址的偏移位置
-
例如