C++类对象内存模型与成员函数调用分析(中)

2.4 虚拟成员函数

是本文中最复杂也最有趣的话题了。虚拟函数也是和继承这个话题相伴相生,所以本节将纳入对单继承多重继承虚拟继承,一起描述他们之间的关系,这样,对C++对虚拟函数的调用,以及由此所变现出来的多态的理解,应该是非常清晰了。

2.4.1 单继承下的虚拟成员函数

对于虚拟函数,我们首先引入两个数据结构,为什么引入一会就知道了。

1.         Virtual table. 大名鼎鼎的vtbl,如果一个类有虚拟函数,编译器首先一堆指向virtual function的指针,这些指针,就存放在了这个vtbl之中。

2.         vptr. 编译器会为每个或自己有,或其父类/祖爷类等有虚拟函数的类的实例压入一个指针,指向相关联的virtual table,这个指针就是vptr

先不管为什么要这么做,先看看这么一些数据结构引入之后,编译器怎么来处理虚拟函数调用的问题。考虑代码段7

class base{

public:

   virtual int sayhello(){

      std::cout<<"Hello, I'm a BASE lass instance!/n";

   }

};

 

class derived : public base{

public:

   virtual int sayhello(){

      std::cout<<"Hello, I'm a DERIVED class instance!/n";

   }

};

 

base b;

derived d;

 

base* pB = &d;

pB->sayhello();

 

pB = &b;

pB->sayhello();

对于这句:pB->sayhello();

虚拟函数的关键——从效用角度讲就是多态的关键——就是为sayhello()找到适当的执行体。为此我们必须好好理解多态。

/ *******************************************************插叙:关于多态

我的理解是:同样的操作,得到不同的结果。我们或许接触得多的是override,因为一开始比较正式和系统的讲多态这个概念的时候是虚函数的 override 引发的,但是不尽然,按照“同样的操作,得到不同的结果”的观点,overrideoverload都是实现多态的手段。(当然还有其它的手段)。

l  override意思是重写,仅仅发生在继承这个过程,在基类中定义了某个函数,且这个函数是virtual——这是必要条件——再从基类继承出一个新的派生类,就可以在派生类重新定义这个函数”。override的条件比较苛刻,继承+虚函数。

l  overload就是重载了,允许多个函数具有相同的名字,这里的函数既可以是类的成员函数——如构造函数就可以重载多个版本,也可以是全局的函数。更明显的例子是运算符重载,complex类中复数的相加等运算就是+的重载,可以把运算符看成函数,从而被overload

由上面的讨论可以看出,overrideoverload最大的相同点是:多个函数具有相同的名字。最大的不同点是:override是在程序运行的时候才决定调用哪个函数,overload是在代码被编译的时候决定调用哪个函数——静态联编。

那么,多态到底有什么用呢?

Google一下,曰:多态是一种不同的对象以单独的方式作用于相同消息的能力。注意这几个相同与不同,这个概念是从自然语言中引进的——这个意识对于理解OOP是很好的——我的一种学习体会就是,尽量在自然界中寻找神似的感觉,嘿,OOP还是很好理解嘛!举个例子,动词“关闭”应用到不同的事务上其意思是不同的。关门,关闭银行账号或关闭一个程序的窗口都是不同的行为,其实际的意义取决于该动作所作用的对象。这个比方应该对理解多态有帮助,总之还是那句话:同样的操作,不同的对象执行,就会出现不同的结果。

大多数面向对象语言的多态特性都仅以虚拟函数的形式来实现,但C++除了一般的虚拟函数形式之外,还多了两种“类似于静态的”(因为我觉得没有虚函数那样足够灵活,不过,也够强大的了)多态机制:

1、操作符重载(函数重载的一种):例如,对整型和string对象应用 += 操作符时,每个对象都是以单独的方式各自进行解释。显然,潜在的 += 实现在每种类型中是不同的。但是从直观上看,我们能够预期结果是什么。

2、模板:例如,当接受到相同的消息时,整型vector对象和串vector对象对消息反映是不同的,我们以关闭行为为例:

vector<int> vi; 

vector<string> names;

string name("C++有点BT");

vi.push_back(5);

names.push_back(name);

静态的多态机制不会导致和虚拟函数相关的运行时开销。此外,操作符重载和模板两者是通用算法最基本的东西,在STL中体现得尤为突出。

关于多态的优点,说不清,可能主要是编程实践不够,很多书上是这样说的(比如C++ primer)依赖动态联编,达到统一的接口,不同的实现的功能。从代码执行角度来看,动态联编产生对象的静态类型和动态类型的区别,用户通过对象动态类型来匹配相应的实现,使得同样的代码有了不同的表现。多态使得类的接口和实现分离,降低了程序的耦合性和编译的依赖性,提高了软件的模块化,从而诞生了各种各样所谓的模式。概括起来多态所带来的优点——灵活。

*******************************************************************/

罗里吧嗦一大堆,让我们再次回到代码段7。为了实现所谓的根据对象的实际情况作出相应动作的所谓“多态”,必须首先能够对于多态对象有某种形式的运行期对象识别办法。也就是说,我们需要在运行期获得pB->sayhello();中关于pB的某些相关信息,pB他老人家到底指向了啥子捏?前面我(猜想)着说过,指针类型中可能加入了某些类似于sizeof的信息,好吧,计算这个猜想是对的,也不能保证多态就一定可以实现——单纯这样一个信息太老土了,不够。万一子类没有引入新的数据成员怎么办呢?那好吧,我就直接引入一个对象类型的编码,比如我用某些bit位表示表示类,但是这样对空间要求增加了,而且,这样也不优雅,不简洁。

根据《Inside the C++ Object Model》,这些额外(类型)信息是有的——我前面的猜测部分是正确的——但是不是和指针放在一起。我们一步步来,首先,额外信息到底是什么?知道了这个,我们可以精确的评估开销。其次,我们到底把这些信息放哪里呢?放对了地方,才有可能争取时间与空间的优势。

对于第一个问题,我们需要知道:

A.       pB所指对象的真实类型,到底是base还是derived

B.       sayhello()函数体在内存中间的位置。

对于第二个问题,C++的办法是,在每一个需要多态(virtual函数)的类对象身上压入两个成员:

a)       一个字符串或数组,表示class的类型,即type-info

b)      一个指针,指向某个表格,表格中保存了类和类的继承链中virtual函数的运行期地址。

这两点,分别对应于前面A, B两项需求。而对于b)中提到的两份数据,就是本小节一开始提到的vptrvtbl了。

vtbl中的virtual函数地址从何得知呢?在C++中,virtual函数可以在编译期间就得到,此外,这一组地址是固定不变的,运行期不可能增加或更改。由于程序在执行中vtbl的大小和内容不会改变,所以vtbl构造可以完全由编译器掌控,不需要运行期的任何介入。

然而,为运行期准备好这些地址雷锋还只做了一半。还有一个问题就是找到这些地址。这个,就是vptr的用途了。首先,vptr将指向编译器分配好的vtbl表格,然后被压入类的实例中,这样,我们借助这个vptr找到了vtbl,又因为vtbl表格中一个个表项就是这些virtual的地址,所以万里长征终于到头了。剩下的,就是运行期在vtbl中找到特点的表项,取出virtual函数的地址即可。

一个类只有一个vtbl,每个vtbl内含其对应的类对象中所有虚函数实体的地址,这些虚函数包括:

1.         该类所定义的函数实体。它会override一个可能存在的基类中的虚函数。

2.         继承自基类的函数实体,这些是在子类决定不改写虚拟函数时才会出现的情况。

3.         一个纯虚函数实体。

每一个虚拟函数都被指派一个固定的索引值,这个索引在整个集成体系中保持与特定的虚函数的关联。考虑一个实例代码段8

class Parent {

public:

    Parent():nParent(888) {}

    virtual void sayhello() { cout << "Perent()::sayhello()" << endl; }

    virtual void walk() { cout << "Parent::walk()" << endl; }

    virtual void sleep() { cout << "Parent::sleep()" << endl; }

protected:

   int nParent;

};

 

class Child : public Parent {

public:

    Child():nChild(88) {}

    virtual void sayhello() { cout << "Child::sayhello()" << endl; }

    virtual void  walk_child() { cout << "Child::walk_child()" << endl; }

    virtual void  sleep_child() { cout << "Child::sleep_child()" << endl; }

protected:

   int nChild;

};

 

class GrandChild : public Child{

public:

    GrandChild():nGrandchild(8) {}

    virtual void  sayhello() { cout << "GrandChild::sayhello()" << endl; }

    virtual void walk_child() { cout << "GrandChild::walk_child()" << endl; }

    virtual void sleep_grandchild() { cout << "GrandChild::sleep_grandchild()" << endl; }

protected:

   int nGrandchild;

};

现在,我们使用一个int** pVtbl 来作为遍历对象内存布局的指针,这样可以方便地像使用数组一样来遍历所有的成员包括其虚函数表:

typedef void(*Fun)(void);

GrandChild gc;

int** pVtbl = (int**)&gc;

cout << "[0] GrandChild::_vptr->" << endl;

for(int i=0; (Fun) pVtbl[0][i]!=NULL; i++){

    pFun = (Fun) pVtbl[0][i];

    cout << "    ["<<i<<"] ";

    pFun();

}

cout << "[1] Parent.nParent = " << (int)pVtbl[1] << endl;

cout << "[2] Child.nChild = " << (int) pVtbl[2] << endl;

cout << "[3] GrandChild.nGrandchild = " << (int) pVtbl[3] << endl;

运行结果如下:

 

 

我们发现,当一个子类继承父类时:

1.         它可以继承父类中所声明的virtual函数的函数实体,准确地说,是该函数实体的地址会被拷贝到子类的虚拟函数表中;

2.         它可以使用自己的函数体,如Child::sayhello()GrandChild::walk_child()

3.         它可以加入新的虚函数。

由前面的讨论,我们已经可以画出这三个类的内存布局图了,如下页图6所示。由这个图,如果我们计算sizeof(GrandChild)对于结果应该就不会差异了:3int变量,再加上一个指针。

下一步,就是编译期间如何对pB->sayhello()设定对虚函数的调用呢?

1.         首先,我们并不知道pB所指对象的真正类型,但是我知道经由pB可以存取到该对象的虚拟函数表。

2.         虽然我并不知道哪个sayhello()应该被调用,但是我知道每一个sayhello()函数的地址都放在vbtl的某个表项中,比如上述代码中的第1表项。

由以上的这些信息,编译器已经可以将pB->sayhello()转化为:

              (*pB->vptr[1]) (pB);

在这个转化中,vptr表示编译器所压入的指针,指向vtbl1表示sayhello()vtbl中的索引号。。唯一一个需要在运行期才能知道的信息是:该索引所对表项到底是哪一个sayhello()的函数实体,这个可以借助type-info的信息获得,因为pB也被压入了参数列表中。

 

(未完待续...)

 

 

 

  • 0
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
第0章 导读(译者的话) 第1章 关于对象(Object Lessons) 加上封装后的布局成本(Layout Costs for Adding Encapsulation) 1.1 C++模式模式(The C++ Object Model) 简单对象模型(A Simple Object Model) 表格驱动对象模型(A Table-driven Object Model) C++对象模型(Th e C++ Object Model) 对象模型如何影响程序(How the Object Model Effects Programs) 1.2 关键词所带来的差异(A Keyword Distinction) 关键词的困扰 策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”的Base Class “带有一个Virual Function”的Class “带有一个virual Base class”的Class 总结 2.2 Copy Constructor的建构操作 Default Memberwise Initialization Bitwise Copy Semantics(位逐次拷贝) 不要Bitwise Copy Semantics! 重新设定的指针Virtual Table 处理Virtual Base Class Subobject 2.3 程序转换语意学(Program Transformation Semantics) 明确的初始化操作(Explicit Initialization) 参数的初始化(Argument Initialization) 返回值的初始化(Return Value Initialization) 在使用者层面做优化(Optimization at the user Level) 在编译器层面做优化(Optimization at the Compiler Level) Copy Constructor:要还是不要? 摘要 2.4 成员们的初始化队伍(Member Initialization List) 第3章 Data语意学(The Semantics of Data) 3.1 Data Member的绑定(The Binding of a Data Member) 3.2 Data Member的布局(Data Member Layout) 3.3 Data Member的存取 Static Data Members Nonstatic Data Member 3.4 “继承”与Data Member 只要继承不要多态(Inheritance without Polymorphism) 加上多态(Adding Polymorphism) 多重继承(Multiple Inheritance) 虚拟继承(Virtual Inheritance) 3.5 对象成员的效率(Object Member Efficiency) 3.6 指向Data Members的指针(Pointer to Data Members) “指向Members的指针”的效率问题 第4章 Function语意学(The Semantics of Function) 4.1 Member的各种调用方式 Nonstatic Member Functions(非静态成员函数) Virtual Member Functions(虚拟成员函数) Static Member Functions(静态成员函数) 4.2 Virtual Member Functions(虚拟成员函数) 多重继承下的Virtual Functions 虚拟继承下的Virtual Functions 4.3 函数的效能 4.4 指向Member Functions的指针(Pointer-to-Member Functions) 支持“指向Virtual Member Functions”之指针 在多重继承之下,指向Member Functions的指针 “指向Member Functions之指针”的效率 4.5 Inline Functions 形式对数(Formal Arguments) 局部变量(Local Variables) 第5章 构造、解构、拷贝 语意学(Semantics of Construction,Destruction,and Copy) 纯虚拟函数的存在(Presence of a Pure Virtual Function) 虚拟规格的存在(Presence of a Virtual Specification) 虚拟规格const的存在 重新考虑class的声明 5.1 无继承情况下的对象构造 抽象数据类型(Abstract Data Type) 为继承做准备 5.2 继承体系下的对象构造 虚拟继承(Virtual Inheritance) 初始化语意学(The Semantics of the vptr Initialization) 5.3 对象复制语意学(Object Copy Semantics) 5.4 对象的功能(Object Efficiency) 5.5 解构语意学(Semantics of Destruction) 第6章 执行期语意学(Runting Semantics) 6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete运算符 针对数组的new语意 Placement Operator new的语意 6.3 临时性对象(Temporary Objects) 临时性对象的迷思(神话、传说) 第7章 站在对象模型的类端(On the Cusp of the Object Model) 7.1 Template Template的“具现”行为(Template Instantiation) Template的错误报告(Error Reporting within a Template) Template的名称决议方式(Name Resolution within a Template) Member Function的具现行为(Member Function Instantiation) 7.2 异常处理(Exception Handling) Exception Handling快速检阅 对Exception Handling的支持 7.3 执行期类型识别(Runtime Type Identification,RTTI) Type-Safe Downcast(保证安全的向下转型操作) Type-Safe Dynamic Cast(保证安全的动态转型) References并不是Pointers Typeid运算符 7.4 效率有了,弹性呢? 动态共享函数库(Dynamic Shared Libraries) 共享内存(Shared Memory)
第0章 导读(译者的话) 第1章 关于对象(Object Lessons) 加上封装后的布局成本(Layout Costs for Adding Encapsulation) 1.1 C++模式模式(The C++ Object Model) 简单对象模型(A Simple Object Model) 表格驱动对象模型(A Table-driven Object Model) C++对象模型(Th e C++ Object Model) 对象模型如何影响程序(How the Object Model Effects Programs) 1.2 关键词所带来的差异(A Keyword Distinction) 关键词的困扰 策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”的Base Class “带有一个Virual Function”的Class “带有一个virual Base class”的Class 总结 2.2 Copy Constructor的建构操作 Default Memberwise Initialization Bitwise Copy Semantics(位逐次拷贝) 不要Bitwise Copy Semantics! 重新设定的指针Virtual Table 处理Virtual Base Class Subobject 2.3 程序转换语意学(Program Transformation Semantics) 明确的初始化操作(Explicit Initialization) 参数的初始化(Argument Initialization) 返回值的初始化(Return Value Initialization) 在使用者层面做优化(Optimization at the user Level) 在编译器层面做优化(Optimization at the Compiler Level) Copy Constructor:要还是不要? 摘要 2.4 成员们的初始化队伍(Member Initialization List) 第3章 Data语意学(The Semantics of Data) 3.1 Data Member的绑定(The Binding of a Data Member) 3.2 Data Member的布局(Data Member Layout) 3.3 Data Member的存取 Static Data Members Nonstatic Data Member 3.4 “继承”与Data Member 只要继承不要多态(Inheritance without Polymorphism) 加上多态(Adding Polymorphism) 多重继承(Multiple Inheritance) 虚拟继承(Virtual Inheritance) 3.5 对象成员的效率(Object Member Efficiency) 3.6 指向Data Members的指针(Pointer to Data Members) “指向Members的指针”的效率问题 第4章 Function语意学(The Semantics of Function) 4.1 Member的各种调用方式 Nonstatic Member Functions(非静态成员函数) Virtual Member Functions(虚拟成员函数) Static Member Functions(静态成员函数) 4.2 Virtual Member Functions(虚拟成员函数) 多重继承下的Virtual Functions 虚拟继承下的Virtual Functions 4.3 函数的效能 4.4 指向Member Functions的指针(Pointer-to-Member Functions) 支持“指向Virtual Member Functions”之指针 在多重继承之下,指向Member Functions的指针 “指向Member Functions之指针”的效率 4.5 Inline Functions 形式对数(Formal Arguments) 局部变量(Local Variables) 第5章 构造、解构、拷贝 语意学(Semantics of Construction,Destruction,and Copy) 纯虚拟函数的存在(Presence of a Pure Virtual Function) 虚拟规格的存在(Presence of a Virtual Specification) 虚拟规格const的存在 重新考虑class的声明 5.1 无继承情况下的对象构造 抽象数据类型(Abstract Data Type) 为继承做准备 5.2 继承体系下的对象构造 虚拟继承(Virtual Inheritance) 初始化语意学(The Semantics of the vptr Initialization) 5.3 对象复制语意学(Object Copy Semantics) 5.4 对象的功能(Object Efficiency) 5.5 解构语意学(Semantics of Destruction) 第6章 执行期语意学(Runting Semantics) 6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete运算符 针对数组的new语意 Placement Operator new的语意 6.3 临时性对象(Temporary Objects) 临时性对象的迷思(神话、传说) 第7章 站在对象模型的类端(On the Cusp of the Object Model) 7.1 Template Template的“具现”行为(Template Instantiation) Template的错误报告(Error Reporting within a Template) Template的名称决议方式(Name Resolution within a Template) Member Function的具现行为(Member Function Instantiation) 7.2 异常处理(Exception Handling) Exception Handling快速检阅 对Exception Handling的支持 7.3 执行期类型识别(Runtime Type Identification,RTTI) Type-Safe Downcast(保证安全的向下转型操作) Type-Safe Dynamic Cast(保证安全的动态转型) References并不是Pointers Typeid运算符 7.4 效率有了,弹性呢? 动态共享函数库(Dynamic Shared Libraries) 共享内存(Shared Memory)

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值