深度探索C++对象模型笔记(四)

Function语意学(The Semantics of Function)

1、Member的各种调用方式
  • Nonstatic Member Functions(非静态成员函数)
C++的设计准则之一就是:非静态成员函数至少必须和一般的非成员函数有相同的效率,也就是说:
float magnitude3d(const Point3d* _this){ }
float Point3d::magnitude3d() const { }
那么选择成员函数不应该带来额外的负担,因为编译器内部已将成员函数转换为对等的非成员函数实体。
转换步骤:
  1. 改写函数原型,添加一个额外的参数,使class object得以调用该函数,这个额外的参数被称为this指针
  2. 将每一个对非静态数据成员的存取操作改为由this指针来存取
  3. 将成员函数重新写成一个外部函数,对函数进行“mangling”处理
  • 名称的特殊处理(name mangling)
一般而言,member名称前面会被加上class名称,形成独一无二的命名,如:
class Bar{ public: int ival; }
//member经过name-mangling之后的可能结果
ival_3Bar
为什么要这么做?考虑派生操作
class Foo : public Bar { public: int val;}
//Foo内部
class Foo
{
public:
int ival_3Bar;
int ival_3Foo;
};
不管你要处理哪一个ival,通过name-mangling,都可以绝对清楚的指出来。由于member functions可以被重载,所以需要更加广泛的mangling手法。为了让它们独一无二,唯有再加上它们的参数链表。( 但如果声明extern "C",就会压抑mangling效果
  • Virtual Member functions(虚拟成员函数)
如果normalize()是一个virtual member function,那么下面的调用:
ptr->normalize();会被内部转化为:
(*ptr->vptr[1])(ptr);
  • vptr表示编译器产生的指针,指向virtual table,事实上,vptr也会被mangled,因为在一个复杂的class派生体系中,可能存在多个vptrs
  • 1是virtual table slot的索引值,关联到normalize()函数
  • 第二个ptr表示this指针
  • Static Member Function(静态成员函数)
如果Point3d::normalize()是一个static member function,以下的两个调用操作:
obj.normalize();
ptr->normalize();
将会被转换为一般的nonmember函数调用,类似:
normalize_7Point3dSfv();
normalize_7Point3dSfv();
static member fuctions的主要特性就是它没有members指针:
  • 它不能够直接存取其class中的nonstatic members
  • 它不能够被声明为const、volatile或virtual
  • 它不需要经由class object才被调用
如果取一个static member function的地址,得到的是其在内存中的位置:
&Point3d::object_count();
会得到一个数值,类型是:
unsigned int (*)();
而不是 unsinged int (Point3d::*)();
2、Virtual Member Function(虚成员函数)
为了支持虚函数机制,必须能够对于多态对象有某种形式的“运行时类型判断法”,也就是说,ptr->z()需要在运行时期的某些相关信息,如此才能找到并调用z()适当的实体。
或许最直截了当但成本最高的方法是把信息加在ptr身上,这样的策略下,一个指针(或者一个reference)含有两项信息:
  • 它所参考到的对象的地址(也就是它当前所含有的东西);
  • 对象类型的某种编码,或是某个结构(内含信息,以确定z()函数实例)的地址
此方法会带来两个问题,第一,明显增加了空间负担,即使不使用多态,;第二,它打断了与C程序的兼容性。
在C++中,多态表示“以一个public base class 的指针或引用,寻址出一个derived class object”的意思,识别一个class是否支持多态,唯一适当的方法就是看它是否含有任何virtual function。什么信息才能让我们在执行期调用正确的z()实体?
  • ptr所指对象的真实类型,这可以使我们选择正确的z()实体
  • z()实体位置,以便我们调用它
一个class只会有一个virtual table,每一个table内含对应的class object中所有的active virtual function函数的地址,这些active virtual functions包括:
  • 这个class所定义的函数实体,它会改写(overriding)一个可能存在的base class virtual function函数实体。
  • 继承自base class的函数实体,这是在derived class决定不改写virtual function时的情况
  • 一个pure_virtual_called()函数实体(这个是针对抽象类声明纯虚函数,因为没有实现,而添加到抽象类virtual table中的地址,当子类实现它时则存放的是具体纯虚函数地址),它既可以扮演pure virtual function的空间保卫者角色, 也可以当做运行时异常处理函数(有时候会用到)
class Point
{
public:
virtual ~Point();
virtual Point& mult(float) = 0;

float x() const{ return _x;}
virtual float y() const { return 0; }
virtual float z() const { return 0; }
protected:
Point(float x = 0.0);
float _x;
};

virtual destructor被赋值slot1,而mult被赋值slot2,这里并没有mult的函数定义,他是一个pure virtual function,如果该函数被意外调用,通常会结束程序。y()被赋值slot3,z()被赋值slot4,x的slot是多少?答案是没有,因为想()不是virtual function。
当一个类派生自Point,一共会有三种可能:
  1. 它可以继承base class所声明的virtual functions的函数体,正确的说是该函数体的地址被拷贝到derived class的virtual table相对应的slot之中
  2. 它可以使用自己的函数体,这表示它自己的函数体地址必须放在对应的slot之中
  3. 它可以加入一个新的virtual function,此时virtual table的尺寸会增大一个slot,新的函数体地址会被放进该slot之中
现在,如果我们有ptr->z()这样的式子,那么我们如何有足够的知识在编译期设定virtual functions的调用呢?
  • 一般而言,我们不知道ptr所指对象的真正类型,然而我们知道,通过ptr可以存取到该对象的virtual table
  • 虽然我们不知道哪一个z()函数会被调用,但我们知道一个z()函数地址被放在slot4
以上的信息使得编译器可以将该调用转化为:
(*ptr->vptr[4]) (ptr);
vptr表示编译器安插的指针,指向virtual table,4表示z()被赋值的slot编号。
单继承体系中,virtual function机制的行为十分良好,不但有效率而且恨容易塑造出模型来,但是在多重继承和虚拟继承之中,就没有那么美好了。

多重继承下的Virtual Functions

多重继承支持virtual functions的复杂度围绕在第二个及后继的base classed身上,以及“必须在运行时调整this指针”这一点。

考虑如下类体系

class Base1
{
public:
Base1();
virtual ~Base1();
virtual void speekClearly();
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;
};

“Derived支持virtual functions”的困难度,统统落在Base2 subobject身上,有三个问题需要解决,以此列而言是:(1)virtual destructor,(2)被继承下来的Base2::mumble(),(3)一组clone()函数体
首先,如下:
Base2 *pbase2 = new Derived;
新的Derived对象的地址必须调整,以指向其Base2 subobject,编译期会产生如下代码:
Derived *temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;
如果没有这样的调整,指针的任何非多态调用都将失败。
当程序员要删除pbase2所指对象时:
//必须首先调整正确的virtual destructor
//然后施行delete运算符
//pbase2可能需要调整,以指出完整对象的起始点
delete pbase2;
指针必须被再一次调整,以求再一次指向Derived对象的起始地址,然后上述的offset加法不能在编译时期直接设定,因为pbase2所指的对象只有在执行期才能确定,一般的规则是由指向“第二或后继的base class 指针”或reference来调用derived class virtual function:
比较有效率的解决办法是利用所谓的thunk。所谓Thunk是一小段assembly码,例如由一个Base2指针调用Derived destructor,相关的thunk看起来类似:
pbase2_dtor_thunk:
this += sizeof(base1);
Derived::~Derived(this);
Thunk技术允许virtual table slot继续包含一个简单指针,Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(当需要调整this指针时)。
同一函数在virtual table中可能需要多笔对应的slots,例如:
Base1 *pbase1 = new Derived:
Base2 *pbase2 = new Derived:
delete pbase1;
delete pbase2;
虽然调用相同的Derived destructor,但他们需要两个不同的virtual table slots:
1.pbase1不需要调整this指针,因为它是最左端的base class,其virtual table slot放置真正的destructor地址
2.pbase2需要调整this指针,其virtual table slot需要相关thunk地址
在多重继承下,一个derived class需要n-1个额外的virtual tables,n表示其上一层base classes的数目,因此,单一继承将不需要额外的virtual tables,对于本例的Derived 而言,会有两个virtual tables被编译器产生。

针对每一个virtual tables,Derived对象中有对应的vptr。

多重继承有三种情况会影响对virtual function的支持,第一种就是通过一个指向第二个base class的指针,调用derived class virtual function,如:

Base2 *ptr = new Derived:

delete ptr;//调用Derived::~Derived

第二种情况是通过一个指向Derived class的指针,调用第二个base class中一个继承而来的virtual function,这种情况下Derived class指针必须再次调整,以指向第二个base subobject,如:

Derived *pder = new Derived;

pder->mumble();//pder必须被向前调整sizeof(Base1)个bytes

第三种情况发生在一个语言扩充性质下:允许一个virtual function的返回值类型有所变化,可能是Base type,也可能是publicly derived type。这一点可以通过Derived::clone()函数体来说明,当我们调用第二个base class的指针来调用clone()时,this指针的offset问题于是诞生了:

Base2 *pb1 = new Derived;

Base2 *pb2 = pb1->clone();//调用的是Derived::clone(),返回值必须被调整,以便指向Base2 subobject

虚拟继承下的virtual functions

有如下继承关系:
class Point2d
{
public:
Point2d(float = 0.0, float = 0.0);
virtual ~Point2d();
virtual void mumble();
virtual float z();
protected:
float _x, _y;
};

class Point3d : public virtual Point2d
{
public:
Point3d(float = 0.0, float = 0.0, float = 0.0);
float z();
protected:
float _z;
};
虽然Point3d只有唯一一个base class,但它们的起始部分并不像非虚拟的单一继承情况那样,由于Point2d,Point3d的对象不再相等,因为它们的转换也需要调整this指针。
当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了迷宫一样,我的建议是,不要在一个virtual base class中声明nonstatic data members,如果这么做,你会距离复杂的深渊越来越近

3、函数的效能
书中举例测试,其中有nonmember friend function,member function,virtual member function,并且virtual分别在单一、虚拟、多继承三种情况下测试:
nonmember 或static member或nonstatic member函数都被转化为完全相同的形式,效率完全相同,而未优化的inline函数提高了25%左右的效率,而优化版本的表现简直是奇迹,对virtual function的调用,效率降低了4%到11%不等,这是对于delta-offset(偏移差值)模型来支持virtual function的情况,而在thunck模型中,this指针的调整成本可以被局限在有必要那么做的函数中。多重继承中virtual function的调用似乎用掉较多成本。
注意:单一继承下,继承深度越多,构造函数复杂度也会增加,也会多消耗一些成本
4、指向Member Function的指针
此前我们已经看到过,取一个nonstatic data member的地址,得到的是它在class布局中的bytes位置(+1),它需要绑定到某个class object地址上,才能被存取。
同样,取一个nonstatic member function的地址,如果是nonvirtual,则得到的是它在内存中真正的地址,然而这个值也是不完全的,它也要绑定到某个class object的地址上,才能够通过它调用改函数。
一个指向member function的指针:
double (Point::*pmf)();
然后这样初始化该指针:
double (Point::*coord)() = &Point::x;
也可以这样指定:
coord = &Point::y;
调用它,可以这样:
(origin.*coord)();
或:
(ptr->*coord)();
这些操作会被转化为:
(coord)(&origin);
和
(coord)(ptr);
使用一个member function指针,如果并不用于virtual function,多重继承、virtual base class等情况,与一个nonmember function指针,编译器可以提供相同的效率

支持“指向virtual member functions”的指针

如下程序片段:
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;
pmf是一个指向member function的指针,被设置为Point::z()一个virtual function的地址,如果我们:
ptr->z();
被调用的是Point3d::z();
如果我们:
(ptr->*pmf)();
仍然是Point3d:z()被调用吗?虚拟机制仍然能够在使用指向member fucntion的指针下运行吗?答案是yes
太郁闷了真的,对于4这一点的笔记,上次保存前都是全部写完了的,但是这次继续打开写后面的东西发现没有了,不知道是不是csdn服务器没保存好还是bug了,这次就直接写下一点了,这里遗漏的就不补充了
5、Inline Functions
我们并不能强迫将任何函数都变成inline,cfront有一套复杂的测试法,通常是用来计算assignments、function calls、virtual function calls等操作的次数,每个表达式种类有一个权值,而inline函数的复杂度就以这些操作的总和来决定。一般而言,处理一个inline函数,有两个阶段
  1. 分析函数定义,以决定函数的“intrinsic inline ability”。“intrinsic”意指“与编译器相关”。
  2. 真正的inline函数扩展操作是在调用的那一点上,这会带来参数的求值操作以及临时性对象的管理。
大部分的厂商似乎认为不值得在inline支持技术上做详细的讨论,通常你必须进入到汇编器中才能看到是否真的实现了inline

形式参数

inline扩展期间,每一个形式参数都被实际参数取代,如果说有什么副作用,那就是不可以只是简单地一一封塞程序中出现的每一个形式参数,因为这会导致实际参数的多次求值操作。如果实际参数是一个常量表达式,在替换之前先完成求值操作,如果不是常量表达式,也不是带有副作用的表达式,则直接替换之。

inline int min(int i, int j)
{
return i < j? i : j;
}

下面三个调用操作:
iline int bar()
{
int minVal;
int val1 = 1024;
int val2 = 2048;
minval = min(val1, val2);//(1)
minval = min(1024, 2048);//(2)
minval = min(foo(), bar() + 1);//(3)
return minval;
}
标记为(1)那一行会被扩展为:
minval = vla1 <val2 ? val1 : val2;
标记为2那一行直接使用常量:
minval = 1024;//替换之后

标记为(3)那一行则引发参数的副作用,他需要引入一个临时对象,以避免重复求值:
int t1;
int t2;
minval = (t1 = foo()), (t2 = bar()+1),
t1 < t2 ? t1 : t2;

局部变量

在inline中定义一个局部变量,会怎样?inline函数的局部变量以“mangling”操作,以拥有独一无二的名称。inline函数中的局部变量,再加上有副作用的参数,可能会导致大量临时性对象的产生


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《深度探索C++对象模型》是由侯捷所著的一本经典的C++图书,该书于2012年由机械工业出版社出版。本书的主要内容涵盖了C++对象模型的深入解析和探讨。 在书中,作者详细讲解了C++中的对象模型和相关的概念,如类、对象、继承、多态等。作者首先介绍了C++对象模型的基本概念和特点,包括对象的内存布局、虚函数表和虚函数指针等。然后,作者深入探讨了C++中的继承机制和多态性,包括单继承、多继承、虚继承等。作者还详细介绍了虚函数的实现原理和使用方法。 在书中,作者对C++对象模型的实现细节进行了深入的剖析,包括成员变量和成员函数的内存布局、函数指针和成员函数指针的用法等。同时,作者还讨论了C++中的一些高级特性,如模板、内存管理和异常处理等。通过对C++对象模型深度探索,读者可以更好地理解C++的内部机制和原理,提高程序设计和开发能力。 《深度探索C++对象模型》适合具有一定的C++编程基础的读者阅读,尤其是对C++对象模型感兴趣的读者。通过阅读本书,读者可以进一步了解C++的底层实现和运行机制,从而提高自己的编程能力和代码质量。此外,本书还提供了大量的示例代码和实践案例,可以帮助读者更好地理解和应用所学知识。 总之,《深度探索C++对象模型》是一本深入探讨C++对象模型的经典著作,通过对C++的底层实现和内部机制的剖析,帮助读者深入理解C++编程语言,并提高自己的软件开发能力。 ### 回答2: 《深度探索C++对象模型》是由Stanley B. Lippman于1994年所著的一本经典畅销的C++书籍,该书详细介绍了C++对象模型的内部实现细节。 C++对象模型是指C++编译器在处理对象、继承、多态等面向对象特性时所采用的具体实现方式。这本书通过对对象模型的剖析,帮助读者深入理解C++的内部工作原理,从而写出更高效、更可靠的C++代码。 在《深度探索C++对象模型》中,作者首先介绍了对象、虚函数、继承等C++核心概念,然后详细讲解了C++对象模型的构建过程,包括对象布局、成员函数指针、虚函数表等。作者逐步深入地剖析了C++对象模型在内存中的表示方式,解释了为什么C++可以支持如此强大的面向对象特性。 此外,本书还探讨了一些高级主题,如多重继承、虚拟继承、构造函数和析构函数的执行顺序等。对于想要深入学习C++的读者来说,这本书提供了一些宝贵的技术手册和实用的经验。 尽管《深度探索C++对象模型》的出版时间是1994年,但它仍然被广泛认可为学习C++对象模型的经典之作。在2012年时,由于C++的发展和演进,也许一些内容已经有些过时,但很多基本概念和原理仍然适用。 总而言之,《深度探索C++对象模型》是一本值得阅读的C++经典著作,通过深度探索C++对象模型,读者可以更加深入地了解C++的内部工作原理和实现方式,提升自己的开发技能。 ### 回答3: 《深度探索C++对象模型》是一本于2012年出版的书籍。该书的作者Andrews和Sorkin以全面的角度深入探讨了C++对象模型。该书重点介绍了C++中的对象表示、虚函数、继承、多重继承、构造函数、析构函数等内容,以及与之相关的语法、原理和底层实现。 这本书为读者揭示了C++对象模型的奥秘,让人更加深入地理解C++语言中的类和对象。作者通过分析对象布局、虚函数表、虚函数调用、多继承中的数据布局和函数调用等等,解释了C++对象模型的实现机制。 在读者了解C++对象模型的基础上,该书还介绍了如何有效地利用对象模型来提高程序的性能。作者讨论了虚函数的成本以及如何减少虚函数调用的开销,提供了一些优化技巧。此外,书中还对C++的构造函数和析构函数进行了深入的讨论,详细解释了构造函数和析构函数的执行机制和注意事项。 总的来说,《深度探索C++对象模型》是一本深入剖析C++对象模型的重要参考书籍。通过阅读该书,读者可以更加全面地了解C++的类和对象的实现原理,对于理解C++语言的底层机制和优化程序性能具有积极的作用。无论是对于初学者还是有一定C++基础的开发人员来说,该书都是一本值得阅读的重要参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值