文章目录
索引
C++【对象模型】| 【01】简单了解C++对象模型的布局
C++【对象模型】|【02】构造函数何时才会被编译器自动生成?
C++【对象模型】|【03】拷贝构造是如何工作的,何时才会用到呢?
C++【对象模型】 | 【04】程序在内部被编译器如何转化?
C++【对象模型】 | 【05】类与类之间各种关系下对数据成员的存取、绑定、布局
C++【对象模型】| 【06】类中各种函数的刨析
C++【对象模型】| 【07】构造、析构、拷贝做了哪些事?
C++【对象模型】| 【08】类在执行期会处理哪些事呢?
C++【对象模型】| 【09】类模板、异常处理及执行期类型识别
一、简介
成员函数分类:静态函数、非静态函数、虚函数;
静态函数:不能为声明为const、没有this指针,不能直接存取非静态成员数据;
非静态函数:内含一个this指针;
虚函数:通过虚vptr,地址存储在虚表中;
class Point3d {
public:
Point3d normalize() const {
register float mag = magnitude();
Point3d normal;
normal._x = _x/mag;
normal._y = _y/mag;
normal._z = _z/mag;
return normal;
}
float magnitude() const {
return sqrt(_x*_x + _y*_y + _z*_z);
}
private:
float _x, _y, _z;
};
1、成员的各种调用方式
1.1 nonstatic member functions
该类型函数和非成员函数具有相同的效率;
float func(const Point3d *_this) {} == float Point3d::func() const {}
实际上成员函数被内化为非成员函数;
成员函数如何转换为非成员函数
- 改写函数原型,安插一个this指针,用于存取数据成员;
- 将成员函数该为一个外部函数,将该名称经由`mangling`处理,让其独一无二;
例:转换
【成员函数】
Point3d Point3d::func(){
return sqrt(_x * _x +_y * _y + _z * _z);
}
【非成员函数】
Point3d Point3d::func(Point3d *const this){
return sqrt(this->_x * this->_x + this->_y * this->_y + this->_z * this->_z);
}
【mangling处理】
extern func_7Point3dFv(register Point3d *const this);
当调用该函数时:
非指针调用,obj.func(); ==> func_7Point3dFv(&obj);
mangling
对于成员函数,为了区分一般会加上当前类的名称,避免派生类中出现重名函数;
为了放置函数重载的同名,为此加上了参数的类型和参数个数,来加以区分;
如果禁止使用该方法对函数进行别名,那么请使用extern"C";
1.2 virtual member functions
当调用一个虚函数时会通过vptr索引到虚表中的地址从而进行函数调用;
如:ptr->normalize() ==> (*ptr->vptr[1])(ptr); 【如果上述代码中normalize为虚函数】
其中ptr为this指针;1为func在虚表中的索引值;vptr为虚表;
显示调用会抑制虚拟机制,从而不会产生不必要的重复调用操作
假设上述的magnitude也为虚函数,而它在normalize函数中,该函数已将虚拟机制决议妥当,故可直接obj::magnitude调用,效率会更高,减少通过虚表
索引等不必要的操作;
1.3 static member function
当有非静态数据成员在成员函数中被存取时,才需要this指针;那么当一个类中不需要对类中的成员进行存取,那么久没有必要通过一个obj来调用;
- 不能直接存取class中的非静态数据成员;object_count((Point3d*)0) =>0强转为指针,提供this指针
- 不能被声明为const、volatile、virtual;
- 不需要经由class obj才被调用;
【内存地址】
由于静态成员函数不在类中,那么对其取地址,则得到将是在内存中的位置;
即unsigned int(*)() 而不是usigned int(Point3d::*)();
【好处】
静态成员函数可以成为一个回调函数,应用于线程上;
1.4 virtual member functions
C++为了支持虚函数机制,必须先要对多态对象有某种形式的执行期类型判断法(需要在执行期的相关信息);
【相关信息】
- 所参考到的对象地址;
- 对象类型的某种编码或结构的地址;
此类信息将存储于何处呢?
考虑将其放置在对象本身;
- 当面对class的声明时需要存储,这将保留对struct的兼容性;
- 当class支持执行期多态时需要存储;
- 而对于一个类是否支持多态,则查看其是否有任何虚函数,即当有一个虚函数即需要该信息;
什么样的信息需要存储呢?
当调用一个虚函数时,我们需要知道该对象的真实类型以及该函数的位置;
为此我们在多态的类对象上增加两个成员:
- 一个字符串或数字,用来表示class的类型;
- 一个指针,指向某个表格,记录虚函数的执行期地址;
【下一步我们将构建该表格】:
- 由于虚函数在编译起即获知,且在程序执行表格大小和内存不会改变;
【该如何找到函数地址】:
- 通过指针指向该表格进行查找;
- 通过表格索引值,找到函数地址;
【执行期该做什么】
- 只需在特定的虚表中激活相应的虚函数;
1.5 多重继承下的virtual functions
class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speakClearly();
virtual Base1 *clone() const;
protected:
float data_base1;
};
class Base2 {
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual Base2 *clone() const;
protected:
float data_Base2;
};
class Derived : public Base1, public Base2 {
public:
Derived();
virtual ~Derived();
virtual Derived *clone() const;
protected:
float data_Derived;
};
当创建一个对象时:Base2 *pbase2 = new Derived;
编译器转换 =====> Derived *temp = new Derived; Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;
this调用虚函数需要在执行期完成,因为执行期才能确定好offset;,而如何才能知道offset呢?
offset的大小需要加入this上头的代码,编译器该在何出插入?
起初采用将虚表扩大,让此处容纳所需的this指针,此时表中每一个slot不在是一个指针,而是一个集合体,内含offset以及函数地址;
于是:(*pbase2->vptr[1](pbase2)) ⇒ (*pbase2->bptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);
其中faddr存储虚函数地址,offset内含this偏移值;
该做法不管该虚函数是否要被调用,都需要扩张其大小;
更加有效的做法thunk
thunk用处:
- 以适当的offset调整this指针;
- 跳到virtual function;
该技术允许virtual table solt继续内含一个简单的指针,因此不需要其他额外的负担;
即slot中地址可以直接指向虚函数,当它需要调整this指针时可以指向一个相关的thunk;
调用this指针的第二个负担
调用的可以有两种:将会导致同一函数在虚表中可能需要多个对应的slot
- 由子类或第一个基类(base1)调用;
- 由第二个基类(base2)调用;
如:
Base1 *pbase1 = new Derived;
Base2 *pbase2 = new Derived;
delete pbase1;
delete pbase2;
上述两个delete中调用相同的Derived destructor需要两个不同给的virtual table slots;
- pbase1不需要调整this,由于它在Derived起始处;
- pbase2需要调整this,其virtual table slot需要相关的thunk地址
主次实例
当一个子类内含n-1个虚表时,n为上一层基类的个数,此时子类将会有两个虚表产生:
- 主要实例,与Base1共享;
- 次要实例,与base2有关;
如何调节执行期链接器的效率
将多个virtual tables连锁成一个,形成次要表格,若要获取可通过主要表格名称加上一个offset即可获得;
第二个base class 会影响对虚函数的支持有三种情况
【通过一个指向第二个base class的指针,调用子类虚函数】
当调用时,该指针必须调整以指向子类的起始处;
【一个指向子类的指针,调用第二个基类中一个继承而来的虚函数】
子类指针必须再次调整,以指向第二个基类子对象;
【允许一个虚函数的返回值有所变化】:
当我们指向第二个基类的指针来调用clone时,该指针也需要被调整,否则返回一个指向子类的对象;
1.6 虚拟继承下的virtual functions
class Point2d {
public:
Point2d(float = 0.0, float = 0.0);
virtual ~Point2d();
virtual void mumble();
virtual float z();
protected:
float _z, _y;
};
class Point3d : public virtual Point2d {
public:
Point3d(float = 0.0, float = 0.0, float = 0.0);
~Point3d();
float z();
protected:
float _z;
};
如上述继承关系,虚基类从另一个虚基类派生,都支持虚函数和非静态数据成员,两个都会导致类内膨胀,且需要offset及各种调
整;
为此,建议不要再一个虚基类中声明非静态数据成员;
2、函数的效能
经过测试非成员函数、静态成员函数、非静态成员函数都被转换为完全相同形式,效率相同;
而内联函数能够达到最高的效率可以节省一般函数调用所带来的额外负担;
虚函数再继承中,没多一层继承其执行时间将会明显增加,由于没增加一层久会多增加一个额外的vptr设定;
3、指向成员函数的指针
当取一个非静态成员函数时,它需要绑定再类上才能够调用该函数,故需要一个this指针;
double(Point::* pmf)() = &Point::x; // 初始化成员函数指针 (或pmf=&Ponit::x)
(origin.*pmf)(); // 调用函数(或(ptr->*pmf)())
再由编译器转化==>(pmf)(&origin);
4、支持指向virtual member functions的指针
float (Point::*pmf)() == &Point::z; // 将一个虚函数z赋给pmf
Point *ptr = new Point3d;
ptr->z(); // 调用虚函数
ptr->*pmf();
上述ptr调用pmf仍是调用z()?如果是,那虚拟机制是如何实现该情况的?
pmf接受一个成员函数或一个虚成员函数都能正确调用时由于内部能够持有两种数值:
- 内存地址或 虚表的索引值;
通过(((int)pmf) & ~127) ? (*pmf)(ptr) : (*ptr->vptr[(int)pmf](ptr)); // 前为非虚函数,后为虚函数
当该技巧只能满足类总最多只有128个虚函数;
5、多重继承下,指向成员函数的指针
// 该结构用以支持多重继承下成员函数的指针
struct __mptr {
int delta; // 虚函数地址
int index; // 虚表索引,默认为-1
union {
ptrtofunc faddr;
int v_offset;
};
};
该结构具有以下缺点:
- 每次调用都需要检查是否由虚函数;
- 当传递一个不变值的指针给成员函数时,它需要产生一个临时性对象;
Microsoft对于继承提出多种处理方式:
- 一个单一继承实例,持有vcall thunk或函数地址;
- 一个多重继承实例,持有faddr和delta两个成员;
- 一个虚拟继承实例,持有4个成员;
6、内联函数
处理一个内联函数由两个阶段:
- 分析函数定义,来决定函数的本质内联能力,是否能成员内联函数;
- 真正的内联函数会扩展操作,当调用时;会给参数的求值操作以及临时性对象的管理;
形式参数
当内联扩展期间,形参会被实参取代;当面对由副作用的参数,将会引入临时对象;
局部变量
如果内联函数以单一表达式扩展多次,则每次扩展都需要自己的一组局部变量;
若以分离的多个式子被扩展多次,则只需要一组局部变量;