C++对象模型
2020-03-19 16:28:36 0 举报
AI智能生成
C++对象模型
作者其他创作
大纲/内容
关于对象
加上分装后的布局成本
虚函数机制
执行器绑定
虚基类
有一个单一而被共享的实体
C++对象模式
简单对象模型
所有对象的指针都存在的类对象中
表格驱动对象模型
两个指针,一个指向成员数据表,一个指向成员函数表
C++对象模型
虚指针指向虚函数表
虚表的开头有一个type_info来表对象类型
静态数据成员,静态成员函数和成员函数都单独存储
加上继承
存在一个基类表,每个类对象有一个指针指向基类表
但是效率太低
虚基类
类对象中为每一个关联的虚基类加上一个指针
扩充虚表,负索引就是虚基类的地址
关键词带来的差异
struct和class
对象的差异
一个对象的大小
非静态数据成员的总和
由于对齐而添加的填补
为了支持virtual而内部产生的额外负担
指针的类型
指针类型会教导编译器如何解释某个特定的地址中的内容和大小
转型只改变“被指出内存的大小和内容”的解释方式,不会改变指针所含的真正地址
加上多态之后
Bear b; ZooAnimal *pz = &b; Bear * pb = &b;
pz所指是Bear对象中的ZooAnimal对象
pb所指是Bear整个对象
pz只能使用ZooAnimal子对象中出现的成员,加上多态之后还可以处理虚函数
构造函数
默认构造函数
只有在需要的时候,编译器才会自动合成
父类的中包含默认构造函数
合成的默认构造函数将会调用父类的默认构造函数
如果有多个父类,则默认构造函数中调用父类的构造函数的顺序跟继承中的顺序一致
成员类对象带有默认构造函数
合成的默认构造函数将会调用成员类对象的默认构造函数
如果包含多个成员类对象,则默认构造函数中的调用顺序跟申明的对象顺序一致,与其他顺序无关
类中带有虚函数
默认构造函数赋值将初始化类对象的虚指针
类中带有虚基类
默认构造函数需要初始化虚基类指针
申明一个类对象时
默认拷贝构造函数
只有在需要的时候,编译器才会自动合成
bitwise copy失效时
类中含有虚函数
子类对象赋值给父类对象时,父类对象中的虚指针由构造函数重新设定指向父类虚函数表
类中包含一个虚基类
类继承自一个基类,而基类中有一个拷贝构造函数
类中内含一个对象成员,而对象成员类中有一个拷贝构造函数
当一个对象赋值给同一类的对象时,即使存在虚继承,bitwise copy也足以,不需要默认拷贝构造函数
如果一个类继承自虚基类,同时该类再派生出一个子类,将子类对象赋值给父类对象时,需要合成默认拷贝构造函数,其作用是来初始化被赋值对象的虚基类指针和偏移
程序转化
明确的初始化操作
X x1(x0);
分为两个阶段
第一个阶段分配内存
X x1;
调用拷贝构造函数
X::X(const X & xx);
参数的初始化
第一种方式:首先生成一个临时对象,然后在参数改变为一个引用,在函数结束后将临时对象析构
第二种方式:以拷贝构造的方式把参数执行构建在其应该的位置上
返回值的初始化
双阶段转化
首先加上一个额外的参数,类型是类对象的一个引用,用来存放拷贝构建而得的返回值
在return指令之前安插一个拷贝构造调用操作,以便将欲传回的对象的内容当做上述新增的参数的初值
这样做,转化操作需要重写函数,使它不传回任何值。
X bar(){}
改写为void bar(X & __result){X xx; xx.X(); __result.X(xx);return ;}
X xx = bar();
改写为:X xx; bar(xx);
X(*pf)(); pf = bar;
改写为 void(*pf)(X&); pf = bar;
使用者层面做优化
X bar(const T & y, const T& z){X xx; //以y和z处理xx; return xx;}
改写为X bar(const T & y, const T & z){return X(y, z);}
需要定义一个构造函数,能传输两个参数,这样__result直接被构造出来,不需要调用拷贝构造
编译器层面做优化
命名返回值优化(NRV)
以__result参数取代具名的返回值
对应的类必须具有一个拷贝构造函数
缺点:函数复杂,难以优化;编译器默默执行,是否优化无从得知;对称性被优化打破。
拷贝构造函数是否需要
视情况而定
如果函数以传值的方式返回对象,将对象作为值传入,申明一个会优化效率
如果类中没有虚基类,虚函数,没有任何成员对象带有拷贝构造
bitwise copy的效率更高,也安全
成员们的初始化队伍
必须使用成员列表初始化的情况
初始化是一个引用成员
初始化一个常量成员
调用基类的构造函数,并且其拥有一组参数
调用成员类的构造函数,并且其拥有一组参数
使用成员列表初始化提高效率的情况
class Word{string str; Word(){str= 0;}};
这样首先调用一个构造函数初始化一个临时strig对象
并调用默认构造函数初始化str
然后调用赋值操作符对str进行赋值
最后销毁临时变量
改进版:class Word{string str; Word():str(0){}}
代码扩张成:Word(){str.string(0);}
内部实现
编译器会一一操作初始化列表,以适当次序在构造函数之内安插初始化操作
被初始化的顺序由类中成员声明的次序决定,不由成员初始化列表中的次序决定
class X{int i; int j; X(int val) : j(val), i(j){}};这种做法错误,先用j初始化i,但是还未被初始化
成员列表初始化代码会扩张在用户显式代码之前
class X{int i; int j; X(int val) : j(val){i = j;}};这种做法是正确的
Data语义
成员数据的布局
非静态成员在类对象中按其被申明的顺序一样
静态数据成员不会放进对象布局之中,放在程序的数据段中
同一访问层级的数据成员不一定连续排列,因为可能存在内存对齐的问题,编译器还可能会合成一些内部使用的数据成员,比如vptr
C++标准允许不同访问层级的数据成员自由排列,不必在乎它们出现在class声明中的次序。但是各家编译器将每个访问层级的数据成员锁在一起,依照声明的次序,成为一个连续区块。
成员数据的存取
静态数据成员
存放在类之外,被视为一个全局变量,存放在数据段中。为了防止两个类的静态成员变量命名冲突,会对每一个静态成员变量进行编码
非静态数据成员
直接存放在每一个类对象之中
对于非静态成员的存取,都是有一个this之中,在每个成员函数的最前面(隐含的),
存取方式:通过类的起始地址+数据成员的偏移量-1
-1操作的原因
offset值总是被加1,这样可以使编译器区分
一个指向数据成员的指针,用以指出类的第一个成员,和一个指向数据成员的指针,没有指出任何成员
数据成员的偏移量在编译器决定
origin.x=0.0和pt->x=0.0的重大差异
Point3d(origin的类型)是一个派生类,而在继承体系中有一个虚基类,存取的变量x是从该虚基类继承而来的,就会有重要差异
origin必定是一个Point3d对象,在编译器x的编译已经确定;而pt指针指向谁只有在执行期才能知道,可能要通过间接方式才能存取x
继承与数据成员
C++标准规定编译器可以自由安排。父类的数据成员总是排在当面类对象的前面,但是虚基类除外
四种情况
单一继承不含虚函数
考虑内存对齐的情况,采用继承可能导致对象的内存扩张,这样做是为了保证子类指针指向父类指针的时候不会覆盖子类对象部分的数据
单一继承含虚函数
C++问世指出将虚指针放在类对象的末尾,可以考虑base class C struct的对象布局
C++2.0开始,vptr放在对象的开头
丧失了C语言的兼容性
如果基类没有虚函数,而派生类有,把一个派生类对象转换成其基类对象,就需要编译器介入,用以调整地址
多重继承
对于多重派生对象,将其地址赋值给“最左端基类的指针”,情况和单一继承相同,因为二者指向相同的起始地址
至于第二个或者后继的基类的地址,则需要将地址修改,加上一个前面基类的大小(偏移)
而且指针转换过程中需要判断指针是否为nullptr,而对于引用则不用考虑
目前编译器以声明的继承顺序来完成多重基类的布局
多重继承中,子类继承自多少个父类,每个父类都有虚函数,则子类就会有多少个虚表
虚拟继承
将基类的分割成两部分
一个不变局部
其中的数据,不管后继如何衍化,总是拥有固定的偏差,这部分数据可以直接存取
一个共享局部
所表现的就是虚基类子对象部分
针对该部分有三种主流的策略
每个派生类对象中有一些指针,指向每个虚基类
引入虚基类表,每个类对象有一个虚基类指针指向该表
在虚函数表中防止虚基类的偏移,可以通过正负值来索引。如果正值索引到的是虚函数;如鞥是负值索引到的是虚基类的偏移
对象成员的效率
指向数据成员的指针
取某个数据成员的地址,得到的该数据成员在类对象中的偏移量
该偏移量比实际的类对象地址的偏移大1
为了区分一个指向数据成的指针指向第一个数据成员,还是指向空(没有指向任何数据成员)
&origin.z和&Point3d::z的区别(origin是一个Point3d对象)
&origin.z得到的是数据成员的地址
&Point3d::z得到的是z在对象中的偏移+1
Function语意
static 成员函数不能被声明为const
成员函数的各种调用方式
非静态成员函数
编译器内部已将成员函数实体转换为对等的非成员函数实体
首先改写函数的签名,以安插一个额外的参数(this)到成员函数中,如果函数是const的,则函数的参数变成const的,如果参数是指针,应该是双const
对非静态数据成员的存取操作改为进由this指针来存取
将成员函数重新生成一个外部函数,对函数名称进行处理,使它在程序中成为独一无二的
名称的特殊处理
成员的名称签名会加上class的名称,形成独一无二的命名
解决了成员函数可以被重载的问题
如果同一类中两个函数的函数名一样,再加上它们的参数链表
虚成员函数
ptr->normalize()转换成(*ptr->vptr[1])(ptr)
一个复杂的类派生体系中,可能存在多个虚指针
1是虚函数表的索引值,关联到normalize()虚函数
第二个ptr表示this指针
静态成员函数
特征
不能够存取类中的非静态成员数据
不能够被声明为const,volatile或virtual
不需要经由类对象才被调用--虽然大部分时候它是这样被调用的
一个静态成员函数会被踢到类的声明之外,并给予一个经过特殊处理的名字
对静态成员函数取地址,获得的将是其在内存中的地址。地址的类型只是一个非成员函数指针,而不是指向类成员函数的指针
提供了一个意想不到的好处:成为一个回调函数
虚函数
多态:一个基类指针或引用,寻址出一个派生类对象
编译期所做事情
备好虚函数的地址到虚函数表中
一个字符串或数字,表示类的类型
每个类对象有一个虚指针指向该表
每个虚函数都指派了一个表格索引值
执行期所做事情
在特定的虚函数表中激活虚函数
纯虚函数也会在虚表中有记录,不过调用该函数时程序会被结束
虚表中只会存储虚函数和纯虚函数的地址
派生类假如一个新的虚函数,虚函数的表会增大一个slot,而新的函数实体地址会被放入其中
虚表中可能还包含
type_info用于RTTI
虚基类指针
多重继承下的虚函数
一个类派生自多个基类,第一个基类指针指向该派生类没有问题,而第二个或后面的基类指针指向该派生类时需要加上一个偏移来调整派生类对象的起始处
虚拟继承下的虚函数
建议不要在虚基类中声明非静态成员函数
指向成员函数的指针
成员函数的指针
double (Point::*coord)() = &Point::x;或者corrd=&Point::y;
使用方式:(orgin.*coord)();或者(ptr->*coord)();
编译器转换为:(coord)(&origin);
如果类中不含有虚函数,多继承和虚基类,使用一个成员函数的指针和一个普通的函数指针的效率是一样的
指向虚函数的指针
虚函数的地址在编译器是未知的,知道的只是虚函数在虚表中的偏移,所以虚函数取地址得到的是一个索引值
float (Point::*pmf)() = &Point::z; z()是一个虚函数
通过pmf调用z(),编译器应转换成:(*ptr->vptr[(int)pmf])(ptr);
对于一个指向成员函数的地址有两种含义
常规的成员函数地址
虚函数的偏移
多继承之下,指向成员函数的指针
内联函数
inline只是一项请求,具体是否是内联函数,还需要编译器决定,如果决定是内联函数,就会用一个表达式合理地将这个函数扩展开来
处理一个inline函数的两个阶段
分析函数定义,以决定函数是否为inline函数
真正的inline函数扩招操作是在调用的那一点上
形式参数
局部变量
构造、解构、拷贝
纯虚函数的存在
可以定义和调用一个纯虚函数,不过它只能被静态的调用(Abstract_base::innterface();),不能经由虚拟机制调用
注意:纯虚的析构函数必须给其定义,因为派生类会调用每一个虚基函数和上一层基类的析构函数,如果没有定义,则会在链接是出现错误
最好的办法是让编译器合成一个析构函数,但是目前不知道编译器是否这样做。所以最好不要把虚析构函数声明为纯虚函数
虚函数最好不要被定义为const
无继承情况下的对象构造
exit()有系统产生,放在main()结束之前
C++的所有全局对象都被当做“初始化过的数据”来对待,所以BSS就相对低不重要了
抽象数据类型
显示初始化列表的缺点:
类的数据成员必须是public
只能指定常量
失败的可能性比较高
显示初始化列表有效的特殊情况
手工打造一些巨大的数据结构比如调色盘
当默认析构函数,默认拷贝构造函数或默认赋值函数是无关痛痒的,存在没有什么价值,编译器实际上根本不会产生它们
为继承而准备
如果函数以传值方式返回一个对象,推荐给该对象的类定义一个拷贝构造函数,以便编译器实现NRV优化
引入虚函数会引入其他开销,比如默认构造函数的扩张用来设置虚指针,类对象的扩大加入了虚指针,成员拷贝语义不在足够,需要拷贝赋值操作符来设置虚指针等
继承体系下的对象构造
扩充每一个构造函数都视类继承体系而定
成员列表初始化
成员数据有默认构造函数
类中含有虚函数
基类构造函数
基类被列于成员列表初始化中
基类有默认构造函数
多重继承下的第二或后继的基类,this指针需要调整
虚基类
基类被列于成员列表初始化中
基类有默认构造函数
类对象最底层的类,其构造函数可能被调用用以支持这个行为的机制被放进来
虚拟继承
派生类的构造函数中包括一个参数来压制其对虚基类构造函数的调用
vptr初始化
构造函数中调用虚函数会被转换成一个静态函数调用,即Point3d::size();,即使size()中包含虚函数,也被会判断为Point3d的函数实体
虚指针的设定在基类构造函数调用之后,在程序员供应的代码或是成员列表初始化中所有成员初始化之前
构造函数在成员列表初始化中时,成员列表中不用调用该类的虚函数,应为此时vptr尚未被设置好
对象复制
明确拒绝讲一个类对象指定给另一个类对象
将拷贝赋值函数声明为私有的,这样只能在成员函数或友元函数中进行赋值
不提供函数的定义,一旦成员函数或友元函数调用,则会在链接时失败
提供一个显式的拷贝赋值函数
只有在默认行为所导致的予以不安全或不正确时,才需要显式提供
bitwise copy失效情况
基类中有拷贝赋值函数
成员对象对应的类有拷贝赋值函数
类中含有虚函数
类继承自一个虚基类
什么都不做,实施默认行为
只是一个简单的拷贝操作,默认行为不但足够并且有效率
如果类有了字节级别的拷贝,就不会生成默认的拷贝赋值函数
当存在虚基类时,如何压制上一层基类的拷贝赋值函数被调用
把虚子对象拷贝到一个分离的函数中,并根据call path条件化调用它
建议不要在虚基类中声明数据
对象的功能
析构函数
默认析构器被合成的情况
类的基类中的析构函数,而且会以其声明顺序相反的顺序被调用
类的成员对象对应的类有析构函数,而且调用析构函数的顺序与声明的顺序相反
类中有虚表,则需要在析构函数之中重设虚指针,重设虚指针的代码会在析构函数本身之前
任何虚基类拥有析构函数
析构函数扩张的顺序
析构函数本身被执行
类拥有的成员类对象的析构函数,以其声明顺序的相反顺序被调用
类中带有虚指针,虚指针被重设,指向适当的基类
调用上一层的非虚基类的析构函数,以其声明的顺序的相反顺序被调用
虚基类的析构函数,该虚基类位于继承体系的最末端
执行期
对象的构造和析构
全局对象
会在main开头进行初始化(_main()函数对所有全局对象的静态初始化动作),main结束之前进行析构(exit()对所有的全局对象做静态析构动作)
局部对象
局部的静态类对象之后构造和析构一次,不管函数被调用多少次
实现方式:产生一个全局临时性的对象,初始化为0,如果局部静态对象被调用一次后,该全局临时性对象也会被赋值,这样下次可以通过判断全局临时性的对象是否为0来判断是否调用构造函数和析构函数
对象数组
存在默认构造函数时,对象数组的配置经由vec_new()函数完成
如果类中拥有虚基类,则由vec_vnew()
传输函数的参数
数组起始地址
类对象的大小
数组中类对象的个数
构造护士的地址
析构函数地址
存在析构函数时,对象数组的销毁经由vec_delete()完成
如果类中拥有虚基类,则由vec_vdelete()
数组起始地址
类对象的大小
数组中类对象的个数
析构函数的起始地址
如果将对象数组中的一个或多个提供初值,vec_new()和vec_delete()就不会处理这些对象
默认构造函数和数组
在程序中取构造函数的地址是不可以的,但是vec_new()可以
声明对象数组时,必须没有声明构造函数或者默认构造函数没有参数。为了解决这种问题,编译器产生一个内部的构造函数,没有参数,内部调用程序提供的构造函数
new和delete
new有两个步骤
调用全局的operator new,配置所需的内存
调用构造函数设置初值
delete
delete pi;时,如果pi的值为0,则C++要求delete运算符不要有操作,因此编译器会为此增加一层判断,如果pi不为0才调用delete pi;
new T[0];
返回一个指向默认为1字节的内存区块
针对数组的new语义
int * p_arr = new int[5];
不会调用vec_new(),因为他的主要功能是把默认构造函数施行于类对象所组成的数组的每一个元素身上
Point3d * p_arr = new Point3d[10];
如果类定义一个默认构造函数,某些版本的vec_new()就会被调用
placement operator new
该操作符需要第二参数,类型为void *
Point2W * ptw = new(arena) Point2w;
arena指向内存中的一个区块,用以放置新产生出来Point2w对象
调用构造函数
临时对象
a+b;
T operator+(const T &, const T &);
会产生一个临时对象,以放置传回的对象
是否产生临时对象视编译器的进取性以及上述操作发生时的程序上下关系而定
临时对象的生命规则
临时对象的销毁,应该是对完整表达式求值过程中的最好一个步骤
两个特例
当一个临时对象被一个引用绑定时
表达式被用来初始化一个对象时
站在对象模型的尖端
模板
模板的具现
声明一个模板类的指针,该指针指向空
Point<float> * ptr = 0;
此时并不会将模板具现出来
而如果声明的是一个引用
Point<float> & ref = 0;
此时会将模板具现出来
模板成员函数也只会在被使用的时候才具现出来
模板的错误报告
有些错误在编译时间报告
类声明最后没有加分号
一些常见的语法错误
有些错误只有在具现出来再会报错
T t; t= 1024;
词汇错误和解析错误
模板中名称决议方式
定义出模板的程序
如果一个模板函数的参数类型固定,则该函数的作用域是定义出模板所在的程序
具现出模板的程序
如果一个函数的参数是T,可变的,则该函数调用的作用域是具现模板所在的过程需
成员函数的具现行为
编译时策略
程序代码必须在program text file中备妥可用
链接时期策略
有一些工具可以引导编译器的具现行为
异常处理
三个主要的组件构成
throw子句
丢出一个异常
一个或多个catch子句
每个catch子句处理一个异常
一个try区段
区段内的程序可能引起catch起作用
当一个异常被抛出,寻找一个吻合的catch子句。如果没有,那么默认的处理例程terminate()会处理
异常处理流程
检验发生throw操作的函数
决定throw操作是否发生在try区段中
若是,编译系统必须把异常类型拿来和每一个catch子句比较
如果比较吻合,流程控制应该交到catch子句手中
如果throw的发生并不在try区段中,没有一个catch子句吻合,那么系统必须
摧毁所有的局部对象
从堆栈中奖当前的函数unwind掉
进行到程序堆栈中的下一个函数中取
执行器类型识别
保证安全的向下转型
承担额外的负担
存储类型信息,一个指针指向某个类型信息节点
需要额外的时间以决定执行器的类型
在虚函数表的第一个slot中存储RTTI 对象地址
type_info是C++标准所定义的类型描述器的class名称
保证安全的动态转型
dynamic_cast
如果向下转成安全,则会返回转型过的指针
如果不安全,则会返回0
dynamic_cast同样可以运用于引用上
如果reference真正参考到适当的派生类,向下转换会被执行
如果reference并不真正是某一种派生类,那么由于不能够传回0,所以丢出一个车bad_cast 异常
typeid
返回type_info 对象
RTTI只适用于多态类,但是type_info适用于内建类型,以及非多态的使用者自定类型
对于非多态的type_info,是静态取得的,而多态类型的type_info是在执行期取得的
动态共享函数库
共享内存
0 条评论
下一页