《深度探索C++对象模型》阅读笔记(一)——对象的内存布局1

来源:互联网 发布:家用干洗机 知乎 编辑:程序博客网 时间:2024/05/16 06:20

本章是从整个对象的全局观来审视C++,并对比C++和C,讨论出一个较为高效和简明的C++对象模型,最后详细介绍了继承如何对程序造成影响。

主要分为以下几个内容:

  1. 引入。C++如何基于C构建对象
  2. C++简单对象的内存布局模型讨论
  3. Class 和 Struct 间的关系和影响
  4. 详细讨论对象的继承

关于C++的强大以及C的简洁之间的讨论已经很多,因而我不打算介绍C++是如何基于C构建对象的。而是直接进入C++对象的内存布局模型的讨论。

由于本章是C++对象模型的概述,也是全书的一个热身。我打算比较详细的介绍,故本文将只介绍2. C++简单对象的内存布局模型讨论。

今后再继续后面的讨论。

对象布局

我经常听人说,利用函数指针,他可以利用C实现面向对象。

诚然如此,但用C实现面向对象,以及C++内建的面向对象之间的执行效率有何区别?

如果不深入底层,我们是无法讨论出个结果的,现在,我就将简要的介绍书中提到的三种内存布局模型。

注:以如下类定义为例:

class Point{public:    Point( float xval ) : _x(xval) {}    virtual ~Point();    float x() const { return _x; }    static int PointCount() { return _point_count; }protected:    virtual ostream& print( ostream &os ) const {        os << _x;        return os;    }    float _x;    static int _point_count;};


简单对象模型

这个模型非常简单,每一个对象有自己独立的存储空间,而存储的内容是一列指针(书中称存储此指针的空间为slot),每个指针指向该对象的一个成员变量或成员函数。如下图:


由此图可以清楚的看到这种模型的实现方式。

这种模型很简单,每个slot都是指针,因而有固定的大小。

但问题是,每次访问其成员,都必须加一层指针访问。这将极大的影响模型的效率。

表格驱动对象模型

这种模型相对于前一个模型,更具扩展性,它是在对象的存储空间中,只存储两个指针:一个指向成员变量表,一个指向成员函数表。成员变量表直接存储变量,函数表存储函数指针:


这种模型的好处在于,每个对象的存储空间都有相同的表现方式,扩展性非常强。

但实际上,这种模型比前一种模型效率更低。但是,在后面可以看到,成员函数表的模型将被应用于虚函数的实现上。

C++对象模型

这种模型是C++编译器实际使用的模型(不同的编译器只有细节差别,基本模型是一致的)。它对空间、时间都进行了优化,当然,这种优化也必然导致它的复杂度有些许增加。

书中讲述的比较简洁,可能有些难懂,我在这里分步骤来介绍这个模型。

首先,暂时不考虑虚函数的实现,那么其实现方式如下:

  1. 每个对象直接存储非静态变量,这与C中的struct完全一致
  2. 对象的静态变量存储在一块共享区域,每个对象都可以访问
  3. 对象的静态、非静态函数的执行体都存储在对象之外,每个对象都可以访问
  4. 静态及非静态函数的区别在于,非静态函数将多传递一个this指针,以便函数访问this指针指向的内容

其模型如下图:


图中可以清晰的看到,实际的动态内存需求,仅仅只是每个对象内部的非静态成员变量所占内存大小的总和。

并且,编译器内部访问对象的成员函数的方式,实际上是同C的函数访问完全一致的。

我们可以这样理解C++编译器的实现:

它将C++代码转换成了如下的C代码(暂时忽略虚函数):

// Point类定义,只有非静态成员变量struct Point{    float _x;};// 静态成员变量定义在Point类之外static int _Class_Point_point_count;// 构造函数初始化成员变量void _Class_Point_Constructor(struct Point* this, float xval) {    this->_x = xval;}float _Class_Point_x(struct Point* this) {    return this->_x;}// 静态成员函数直接访问静态成员变量static int _Class_Point_PointCount() {    return _Class_Point_point_count;}

之后,再进行编译。

这样,一个不含虚函数的对象的存储空间,就与此对象成员变量组成的结构体毫无区别。

并且,对成员函数的访问效率,与直接访问一个C函数的效率等同。这一点,是使用函数指针模拟对象的C结构体无法比拟的。

C++对象模型(加虚函数)

下面我们再来看看加上虚函数的情形,它将在前文提到的实现上再加上几条:

  1. 一个包含虚函数的类,将定义一个虚函数列表,称为vtbl。此列表的每一个slot都指向该类的一个虚成员函数。
  2. 每个对象增加一个指针,指向对应类的虚函数列表。此指针称为vptr。这个vptr相当于一个变量。但只由编译器自己管理(在构造、析构和复制函数中设定和重置),用户不能访问。同一个类的不同对象,将包含指向同一个vtbl的vptr,并且,不会随着对象指针类型的变化而变化(后文再详细讨论)。
注意,为便于理解,这里暂时不考虑运行时类型识别(RTTI, RunTime Type Identification)的实现。

其模型结构如下图:


相对于前文的非虚函数类,增加了一个__vptr__Point的指针以及虚函数列表。图中所示的type_info for Point用于RTTI,暂不考虑。

此模型,翻译成c语言实现,将有如下定义:

struct Point;// Point类的虚函数定义ostream& _Class_Point_print(struct Point*, ostream&);void _Class_Point_Destructor( struct Point* );// Point类的虚函数表vtblstatic struct __vtbl__Point_Tag{    ostream& (*_p_Class_Point_print)( struct Point*, ostream& os );    void (*_p_Class_Point_De_Point)( struct Point* );} __vtbl__Point = { _Class_Point_print, _Class_Point_Destructor };// Point类(只有非静态成员变量)struct Point{    float _x;    struct __vtbl__Point_Tag *__vptr;};// Point类的静态成员变量static int _Class_Point_point_count;// Point类的非静态成员函数,加上了this指针。// 在构造函数中初始化变量以及vptrvoid _Class_Point_Constructor(struct Point* this, float xval) {    this->_x = xval;    this->__vptr = &__vtbl__Point;}float _Class_Point_x(struct Point* this) {    return this->_x;}// Point类的静态成员函数,直接返回静态成员变量static int _Class_Point_PointCount() {    return _Class_Point_point_count;}// Point类的虚函数实现ostream& _Class_Point_print(struct Point* this, ostream& os) {    os << this->_x;    return os;}void _Class_Point_Destructor( struct Point* this ) {}

需要注意的是,C语言没有访问控制(const、private、protect、public等),这些访问控制是在C++编译器编译时检查的。


下面我用一个简单的例子来说明,虚函数是如何做到正确的访问成员函数的。

首先,看看不用虚函数的情况:

class Animal {    void bark() { cout << "。。妈妈没教我叫。。。" << endl; }};class Dog {    void bark() { cout << "汪汪!" << endl; }};int main() {    Animal *a = new Dog;    a->bark();    return 0;}

输出的结果是“。。妈妈没教我叫。。。”。

而如果是使用虚函数的情况呢:

class Animal {    virtual void bark() = 0;};class Dog {    virtual void bark() { cout << "汪汪!" << endl; }};int main() {    Animal *a = new Dog;    a->bark();    return 0;}

输出当然就是“汪汪!”了。并且,使用虚函数可以强制Animal的bark被覆盖。相当于Animal成为一个接口,这也可以避免出现“妈妈没教它叫”的窘境。

那么,虚函数是如何实现这一效果的?我们还是从底层来看看。

先看非虚函数的C语言翻译版:

struct Animal {    // 没有成员变量,定义空结构体};// Animal构造函数,什么都不做void _Class_Animal_Constructor(struct Animal* this) {}void _Class_Animal_bark(struct Animal* this) {    cout << "。。妈妈没教我叫。。。" << endl;}class Dog {    // 没有成员变量,定义空结构体};// Dog构造函数,什么都不做void _Class_Dog_Constructor(struct Dog* this) {}void _Class_Dog_bark(struct Dog* this) {    cout << "汪汪!" << endl;}int main() {    Animal *a = new Dog;    // new 的是Dog,调用Dog的构造函数(此函数什么都没做)    _Class_Dog_Constructor(a);    _Class_Animal_bark(a); // a->bark();    return 0;}

再看看虚函数版本:

// Animal类的虚函数为纯虚函数,没有实现// Animal类的虚函数表vtblstatic struct _Class_Animal__vtbl_Tag{    void (*_p_Class_Animal_bark)( struct Animal* );} _Class_Animal__vtbl = { 0 };struct Animal {    // 只有一个vptr的变量    struct _Class_Animal__vtbl_Tag *__vptr;};// 在构造函数中初始化变量以及vptrvoid _Class_Animal_Constructor(struct Animal* this) {    this->__vptr = &_Class_Animal__vtbl;}// Animal类的虚函数为纯虚函数,没有实现/////////////////////////////////////////////////////// Dog类的虚函数定义void _Class_Dog_bark( struct Dog* );// Dog类的虚函数表vtblstatic struct _Class_Animal__vtbl_Tag _Class_Dog__vtbl = { _Class_Dog_bark };struct Dog {    // 只有一个vptr的变量    struct _Class_Dog__vtbl_Tag *__vptr;};// 在构造函数中初始化变量以及vptrvoid _Class_Dog_Constructor(struct Dog* this) {    this->__vptr = &_Class_Dog__vtbl;}// Dog类的虚函数实现void _Class_Dog_bark(struct Dog* this) {    cout << "汪汪!" << endl;}int main() {    Animal *a = new Dog;    // new 的是Dog,调用Dog的构造函数(此函数将a->__vptr赋值为_Class_Dog__vtbl    _Class_Dog_Constructor(a);    a->__vptr->_p_Class_Animal_bark(a); // a->bark();    return 0;}

代码中的关键在于虚函数版本中,Dog类的构造函数将a->__vptr赋值为_ClassDog__vtbl,而_ClassDog__vtbl->_p_Class_Animal_bark是赋值为_Class_Dog_bark的。因此,同样是a->__vptr->_p_Class_Animal_bark()的调用,初始化为Dog的Animal指针将访问的是_Class_Dog_bark,而没有使用虚函数的版本是直接调用了_Class_Animal_bark。这就是虚函数的实现方式。

虚函数的使用虽然多加上了一层指针访问,但其对动态访问的支持使得程序的设计可以更加抽象,有利于大型程序的设计。


原创粉丝点击