C++对象模型


摘要:

本文从内存的角度去了解C++对象,明白C++的一些解析机制,从而达到更加准确使用C++的目的。

1)程序的内存分布

2)C++的对象

3)C++对象的内存分布

程序的内存分布

一个程序占用的内存区一般分为五个区:

l  代码区-存放程序的代码

l  全局/静态数据区-存储全局变量和静态变量(包括全避静态变量和局部静态变量)

l  常量数据区-存储常量字符串等

l  堆-存储动态产生的数据,由用户自己控制

l  栈-存储自动变量或者局部变量,及传递的参数等

 

说明:

1.        静态变量只有在第1次使用时被初始化,以后使用时不再初始化。即初始化只执行一次。

2.        堆和栈在使用和释放上有所不同,有如下一些区别:

1)       大小:一般说来,一个程序使用栈的大小是固定的,由编译器决定,如DVR目前arm平台的栈空间定义32KB。而堆的大小一般只受限于虚拟内存的大小。

2)       效率:栈上的内存是系统自动分配的,压栈和出栈都有相应的指令进行操作,因此效率较高,并且分配的内在空间是连续的,不会产生碎片;而堆上的内存是由开发人员动态分配和回收的,当申请时,系统需要按一定的算法在堆空间中寻找合适大小的空闲堆,并修改相应的维护堆空间的链表,然后返回地址给程序。因此效率比栈低,还容易产生内存碎片。

C++对象

构造函数是一个递归操作,从顶层基类开始,在每一层中,首先调用父类的构造函数,然后调用成员对象的构造函数,再执行构造函数体。当构造该类自己的成员变量时,严格按照成员变量在该类中的声明顺序即可,而与其在初始化列表中出现的顺序无关。(如果和初始化程序顺序有关的,那么析构函数的顺序应该如何指定呢?)对两类成员变量,即“常量”(const)型和“引用”(reference)的变量的初始化必须在初始化列表中进行,而不能将其放在构造函数体内。

析构则严格按照与构造相反的次序执行,该次序是唯一的。

由于对象的创建必须调用构造函数和析构函数,所以针对堆和栈的分配,我们可以自己控制对象的创建。

禁止产生堆对象

产生堆对象的唯一方法是使用new操作, new操作执行时会调用operator new,而operator new是可以重载的。于是使new operator 为private就可以了。为了对称,最好将operator delete也重载为private。

难道创建栈对象不需要调用new吗?是的,因为创建栈对象不需要搜索内存,而是直接调整堆栈指针,将对象压栈,而operator new的主要任务是搜索合适的堆内存,为堆对象分配空间。

Class CTestHash

{

       …

private:

void* operator new(size_t size) //非严格实现,仅作示意之用

  {

   return malloc(size) ;

  }

  void operator delete(void* pp) //非严格实现,仅作示意之用

  {

   free(pp) ;

  }

       …

}

 

       如果你执行:

       CTestHash*pTest = new CTestHash ();  // 编译器错误

       deletepTest;

 

在类CTestHash的定义不能改变的情况下,有没有其它办法产生该类型的堆对象吗?有,指针类型的强制转换。

char* temp = newchar[sizeof(CTestHash)] ;

//强制类型转换,现在ptr是一个指向CTestHash对象的指针

CTestHash *obj_ptr = (CTestHash *)temp ;

delete[] temp;

说明:不建议使用

禁止产生栈对象

创建栈对象时会移动栈顶指针以“挪出”适当大小的空间,然后在这个空间上直接调用对应的构造函数以形成一个栈对象,而当函数返回时,会调用其析构函数释放这个对象,然后再调整栈顶指针收回那块栈内存。在这个过程中是不需要operator new/delete操作的,所以将operator new/delete设置为private不能达到目的。

将构造函数或析构函数设为私有的,这样系统就不能调用构造/析构函数了,当然就不能在栈中生成对象了。可是,如果我们将构造函数设置为私有,那么我们也就不能用new来直接产生堆对象了,因为new在为对象分配空间后也会调用它的构造函数啊。所以,我只能将析构函数设置为private。将析构函数设为private除了会限制栈对象生成外,还会限制继承。所以,如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private。 为了限制栈对象,却不限制继承,我们可以将析构函数声明为protected,这样就两全其美了。如下代码所示:

class CTestStack

{

public:

       void destroy()

{

              deletethis;

}

protected:

       ~ CTestStack();

}

可以这样使用:

CTestStack*pTest = new CTestStack();

pTest-> destroy();

我们用new创建一个对象,却不是用delete去删除它,而是要用destroy方法。从面向对象的角度来考虑,是不习惯这种怪异的使用方式的。

做一下改进,用静态成员函数来产生该类型的对象(设计模式中的singleton模式就可以用这种方式实现)。

class CTestStack

{

public:

       static CTestStack* createInstance()

       {

              return new CTeststack();

       }

       void destroy()

{

              delete this;

}

protected:

       CTestStack();

       ~ CTestStack();

}

可以这样使用:

CTestStack*pTest = CTestStack:: createInstance ();

pTest-> destroy();

 

C++对象的内存布局

简单对象

C++对象中包含成员数据和成员函数,成员数据可以分为静态成员数据和非静态成员数据,成员函数可以分为静态成员函数、非静态成员函数和虚函数。

首先看一个空类


sizeof(CEmpty)的大小会是多少呢,可能有些人会理解为0,但实际上会是1。那是被编译器安插进去的一个char,这使这个类的两个对象得以在内存中配置独一无二的地址。


sizeof(CMemory)的值应该是多少呢?根据输出值为12个字节。

由于静态数据成员 static int m_sCount存储在全局/静态数据区,所以sizeof()的大小不包括m_sCount所占用的内存大小。sizeof(m_iValue)+sizeof(m_cChar)应该是5个字节,但考虑到对齐,就会占用8个字节。还有的4个字节用于哪了呢?

虚函数是C++中的一个重要特性,用来实现面向对象中的多态性。为了实现这一特性,每个类产生出一堆指向虚函数的指针,放在表格中,这个表格被称为virtual table(vtbl)。每一个类对象被添加了一个指针,指向相关的virtual table,通常这个指针被称为vptr。这4个字节就是虚函数表指针所占据的4个字节。而且位于一个对象开始的4个字节。

对于静态成员函数和非静态成员函数,C++编译器采用与普通C函数类似的方式进行编译,只不过对函数名进行了名称修饰,用来支持重载。并且在参数列表中增加了一个this指针,用来表示是哪一个对象调用的该函数。

如果对象中包含虚函数,会增加4个字节的空间,而不论有多少个虚函数。


单继承

      

构造一个派生类的实例时要首先构造一个基类的实例,而这个基类的实例在派生类的实例销毁之后被销毁。所以针对单继承的情况下,派生类实例的头部存在一个基类的实例,派生类的实例使用的是在创建基类实例时建立的虚函数表。但需要注意的是,虚函数表的内容在派生类的实例时发生了变化。

生成的对象内存如下

多继承


多继承是C++语言中比较受争议的语法特性。许多后来的面向对象语言都取消了多继承,而使用接口的概念。

      

由于是多继承,因此创建其类的对象时要遵循一定的顺序,这个顺序是由派生类声明时决定的。如

classCDerivedClase : public CMemory, public CBase

则会先创建CMemory,再创建CBase。

由于CMemory的大小是12个字节,CBase的大小是8个字节,而CDerivedClass有一个成员变量,所以大小应该是24个字节。

每个派生类的实例中包含所有基类的实例,每个基类的实例有自己的虚函数表。

由于派生类CDerivedClass没有重载CMemory的虚函数foo(),因此在基类CMemory的虚函数表里,foo()的指针仍然是指向CMemory的实现,而CDerivedClass重载了test(),因此CBase的虚函数表中,test()的指针被指向了CDerivedClass实现的test()。如果CDerivedClass还有其他虚函数,则应该放在CMemory的vptr指针所指向的vptb。

 

菱形继承

其中继承代码为

class CDerivedClass : public CMidClass1,public CMidClass2

根据继承的对象内存布局,得到如下图

这样的方案不仅会造成调用上的歧义,而且也造成了内存的重复,为了避免这种情况,C++语言提供虚拟继承。

虚拟继承



当使用虚拟继承时,公共的基类只存在一个实例。如

class CMidClass1 : virtual public CMemory

class CMidClass2 : virtual public CMemory

1)             CMemory只创建了一个实例,并且CMemory的实例放在CDerivedClass实例的内存空间的最后部分

2)             没有了歧义。

3)             其对象布局也不一样

n  (VC)不使用虚继承时,CMidClass1和CMidClass2的大小都是16个字节,CDerivedClass的大小是36个字节。而使用了虚拟继承后,CMidClass1和CMidClass2的大小变为24个字节,而CDerivedClass的大小变为40个字节。为了支持虚拟继承,不同的编译器做法会有所不同。在VC中通过添加一个虚基类表(virtual base table)的指针来实现(类似于虚函数表指针)(图a)

n  (GNU)使用在虚函数中放置虚基类的偏移。VPTR的方式,只不过在VBTL的第一个值用来表示基类的地址(图b)

资料:

《深度探索C++对象模型》

《C++应用程序性能优化》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值