深度探索C++ 对象模型 笔记 - 第四章
Function 语意学
概述: 这一章好难,有些地方作者语焉不详的,图示和内容有些出入不知道是不是错误. 主要内容是在object中的函数存放位置,虚继承及多重继承下vptr的存取方案,inline 函数的展开以及指向member function 的指针.
member的各种调用方式
C++的设计准则之一: nonstatic member function 至少必须和一般的nonmember function有相同的效率
对于调用 nostatic member function 而言,例如:
编译器会进行下面几步转化过程:
1.改写函数原型
//non-const
Point3d Point3d::magnitude(Point3d *const this)
//const
Point3d Point3d::magnitude(const Point3d *const this)
2.将每一步对nonstatic data member的存取操作改为经由this指针来存取
3.将member function重新携程一个外部函数。函数名经过名称的特殊处理,使它在程序中独一无二
extern magnitude__7Point3dFv(
register Point3d *const this);// 这里的Fv的意识是返回值为void的Function
Point3d p;
Point3d *ptr=&p
p.magnitude();
ptr->magnitude();
//转化后
magnitude__7Point3dFv(&p);
magnitude__7Point3dFv(ptr);
名称的特殊处理
一般而言,member的名称前面会加上class 形成独一无二的命名。同时function需要支持可重载,则需要更广泛的处理,
例如:
class Point{
public:
void x__5Point(float newX);
float x__5Point();
}
转换后:
class Point{
public:
void x__5PointFf(float newX);
float x__5PointFv();
}
//将参数的类型添加进去.这是cfront采用的编码方法.目前各种编译器有各种处理方案.
虚拟成员函数
ptr->normalize();
//转换后
(*ptr->vptr[1](ptr)); // 1为虚函数表中的索引值
虚拟成员函数 (重点)
单一继承
如上图所示,对于每一个object中都维护一个vptr,每个vptr中对应的slot就是成员函数.图中索引值为0(type_info for Point)的位置存放的是类类型,如果是虚继承的话这里存放的是在派生类中的偏移值.
一个class 只会有一个virtual table .每一个table内涵其对应的class object中所有active virtual functions函数实例的地址.这些active virtual function 包括:
- 这一class 所定义的函数实例.覆盖基类的虚函数指针.(仍然可以调用父类的,但是此时只能显性调用)
- 继承自base class 的函数实例
- 一个纯虚函数实例
多重继承
class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speakClearly();
virtual Base1 *clone() const;
protected:
float data_Base1;
}
class Base2 {
public:
Base2();
virtual ~Base1();
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;
}
利用Thunk技术解决在执行期完成”this指针调整”操作.
pbase2_dtor_thunk:
this+= sizeof(base1); //用适当的offset值调整this指针
Derived::~Derived(this); //跳转到virtual function
在多重继承下,一个derived class 内含n-1 个额外的virtual tables,n表示其上一层base classed的个数.对于本例,会产生两个虚表:
- 一个主要实例,与base1共享
- 一个次要实例,与base2有关
针对每一个虚表,derived对象中都有对应的vptr.如下图所示
针对每一个虚表,Derived对象中有对应的vptr,这些vptrs将在构造函数中设定初值。这一点可以说明构造函数一般不能是虚函数。
虚继承下的Virtual Function
Point2d的内存布局在单一继承(上一章)中就进行了类似的讨论。对于Point3d,非常复杂。
建议:不要在virtual base class中声明nonstatic data member.
静态成员函数
以下内容摘自博客https://blog.csdn.net/qq_25467397/article/details/80333163 幸福的起点_
静态成员最重要的特性在于它没有this指针,以下特性来源于这个主要特性。
- 不能够直接存取class中的nonstatic members;
- 不能被声明为const, volatile,virtual;
- 不需要经由class object才被调用,可以直接经由classname::调用。
因此静态成员函数转化为一般的nonmember函数调用时,不会添加this指针,只会应用”name mangling”以及”NRV”优化。
例如:假设一个三维坐标点类Point3d,有个静态函数定义为:
int Pointe3d::object_count() { return _object_count; }
转化为:int object_count__5Point3dSFV() { return _object_count__5Point3dSFV; }
其中SFV表示static member function,拥有一个空白(void)的参数链表(argument list).
如果取一个静态成员函数的地址,获取的是其在内存中的位置,也就是其地址,地址的类型是”nonmember
函数指针”而非”class member function指针”。
例如:&Pointe3d::object_count();
得到的类型是:int ()();而不是:int (Point3d::)();
其他
函数效能
对于nonmember、static member、nonstatic member函数都是转换为一样的形式,所以三者的效率完全一样。inline函数经过优化后效率有了极大地提升。
指向Member Functions的指针
(1).指向non-virtual nonstatic member functions的指针
例如:double (Point::*pmf)();//声明pmf函数指针指向Point的member function
指定其值:pmf = &Point::y;//y是Point的成员函数
或者直接声明是赋值:double (Point::*pmf)() = &Point::y;
假设一个Point对象origin以及一个Point指针ptr,那么可以这样调用:
(origin.*pmf)();或者(ptr->*pmf)();
编译器的转化为:(pmf)(&origin);或者(pmf)(ptr);
(2).指向Virtual Member Functions的指针
对一个虚成员函数取地址,所能获得的只是一个索引值。
假设有如下Point声明:
class Point {
public:
virtual ~Point();
float x();
float y();
virtua float z();
}
取析构函数的地址:&Point::~Point();得到索引1;取&Point::x()得到函数在内存中的地址;取
&Point::z()得到索引2。若通过函数指针pmf来调用z():
float (Point::*pmf)() = &Point::z();
Point* ptr;
(ptr->*pmf)();
内部转换为:(*ptr->vptr[(int)pmf])(ptr);实际(int)pmf就是2,还是原来的形式:(*ptr->vptr2)(ptr);
还有一个问题在于如何辨别这个数值是内存地址还是索引值呢?cfront2.0非正式版的策略:
(((int)pmf) & ~127) ? (*pmf)(ptr) : (*ptr->vptr(int)pmf);
对于x&~127=0 当x<=127,这种实现技巧必须假设继承体系中的virtual functions的个数小于128.
内联函数
内联函数只是建议请求编译器实行,真正决定内联还是看编译器本身。如果请求被接受,编译器必须认为它可以用一个表达式合理地将这个函数扩展开来。通常编译器会计算assignments, function calls, virtual function calls等操作的次数的总和来决定是否内联。
一般处理inline function的两个阶段:
- 分析函数定义,以决定函数的”intrinsic inline ability”(本质的inline能力)。
- 真正的inline函数扩展操作是在调用的那点上。
形式参数(Formal Arguments)
在内联函数的扩展期间,形式参数有什么变化?看如下例子:
inline int min(int i, int j) { return i < j ? i : j; }
inline int bar() {
int minval;
int val1 = 1024;
int val2 = 2048;
/*(1)*/minval = min(val1, val2);
/*(2)*/minval = min(1024, 2048);
/*(3)*/minval = min(foo(), bar() + 1);
return minval;
}
标识(1)中直接参数替换:minval = val1 < val2 ? val1 : val2;
标识(2)中直接拥抱常量:minval = 1024;
标识(3)中引发参数副作用,需要导入两个临时对象:
int t1,t2;minval = (t1 = foo()), (t2 = bar() + 1), t1 < t2 ? t1 : t2;
局部变量(Local Variables)
如果在inline定义中加入局部变量:
inline int min(int i, int j){
int minval = i < j ? i : j;
return minval;
}
假设有操作:int minval = min(val1, val2);为了维护局部变量可能变成:
int __min_lv_minval;
int minval = (__min_lv_minval = val1 < val2 ? val1 : val2),
__min_lv_minval);
inline函数的每一个局部变量都必须放在函数调用的一个封闭的区段中,拥有独一无二的名称。
另外,如果扩展多次,可能会产生很多临时变量:
int minval = min(val1, val2) + min(foo(), foo() + 1);
可能扩展为:
//为局部变量产生的临时对象
int __min_lv_minval_00, __min_lv_minval_01;
//为放置副作用产生的临时变量
int t1, t2;
int minval = (__min_lv_minval_00 = val1 < val2 ? val1 : val2),
__min_lv_minval_00)
+
((__min_lv_minval_01 = (t1 = foo()), (t2 = bar() + 1),
t1 < t2 ? t1 : t2), __min_lv_minval_00);
因此如果一个inline函数参数带有副作用或者进行多重调用或者函数内部有多个局部变量,这样都会产生临时对象,产生大量的扩展码,使得程序大小暴增。