实用经验 66 class对象大小与什么有关系?

在C++中,对象大小牵涉着对象内存的使用。现在我们就分析一下C++中类大小的影响因素。为了保证分析的渐进性。我们首先从未涉及虚函数普通类开始,然后分析虚函数继承类。最后分析虚继承类。

现在我们开始普通类对象大小分析。假设我们有这样的一个class类型:

// CPerson 类实现
class CPerson
{
public:
    char *GetName(){ return m_szName; }
private:
    // 名称属性。
    char m_szName[28];
};

cout <<sizeof(CPerson) =<< sizeof(CPerson);  // 输出结果是:sizeof(CPerson)=28

在VC++2010编译器上运行上面的代码,输出结果应该是:sizeof(CPerson)=28。从这个输出结果我们可以得出这样的结论:在C++中对象的大小仅与对象的数据成员大小有关系,而与对象中的函数成员无任何关系。

现在我们明白了,对象的大小与对象的函数成员无关系。我想你可能会问,如果一个class中无任何数据成员。那class的大小是多少呢。是0,还是其他值?为了验证我们的猜想。我们看下面这个class。

class CEmpty{}
cout<<sizeof(CEmpty)=<< sizeof(CEmpty);// 输出结果是:sizeof(CEmpty) = 1

由上述代码可知,对于一个空类。其大小是1,而不是0。类CEmpty明明是空类,它的大小应该为0,为什么编译器输出的结果为1呢?这就是牵涉到class的实例化(空类同样可以被实例化)了,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址。所以sizeof(CEmpty)的大小为1。

由上述可知,我们得知class的大小与其数据成员有关系。为了使这共识更精确一些。来看下面这个定义。假设存在一个CWnd定义:

// 无虚函数的窗口类实现
class CWnd
{
public:
  CWnd();
  ~CWnd();
  // 获得窗口标题名称
  char *GetTitle() { return m_szTitle;}
private:
  // 窗口标题名称属性
  char m_szTitle[25];
}

cout << "sizeof(CWnd) = " << sizeof(CWnd); // 输出结果是:sizeof(CWnd) = 28

如果按照前面的理论,对于类CWnd其大小应该是25,但为什么计算机却输出结果为28呢。这就涉及到class的内存布局了。class的内存布局和struct定义时采用的内存布局策略(内存对齐)一样。具体详细描述你可以参考(实用经验14)。此处不在赘述。

无虚函数类对象大小影响因素

  • class对象非静态数据成员占用内存大小,会影响类对象大小。
  • class对象采用的内存对齐策略,会影响类对象大小
  • class对象中函数成员不会影响对象大小。

虚函数是C++中实现多态的基础。现在我们开始讨论包含虚函数的class对象的大小。看下面的代码段:

// 含虚函数的窗口类实现
class CWnd
{
public:
     CWnd();
     virtual ~CWnd();
     // 获取窗口标题名称
     char *GetTitle() { return m_szTitle;}
     // 窗口显示函数
     virtual void Show() {}
private:
     // 窗口标题名称属性
     char m_szTitle[28];
}

// Form特殊窗口实现,派生于CWnd。
class CForm : public CWnd
{
public:
     CForm();
     virtual ~CForm();
     // 窗口显示函数
     virtual void Show() { }
private:
     // 闪烁显示
     void  ShowBlink() {}
}

cout << "sizeof(CWnd) = " << sizeof(CWnd);  // 输出结果是:sizeof(CWnd) = 32
cout << "sizeof(CForm) = " << sizeof(CForm);  // 输出结果是:sizeof(CForm) = 32

你可以在VC++2010编译器或其他编译器验证一下上述结果。现在我们分析一下sizeof(CWnd)为什么输出32。我们看CWnd的实现,其包含一个28字节的数据成员m_szTitle。按道理应该是28才对。

为了说明这一问,我们需要了解C++类的继承和多态机制:在C++中为了实现多态,在类定义时会偷偷的为你添加一个叫虚函数表的数据结构。类对象通过查询虚函数表决定当前调用的是哪个函数。此虚函数表在类中仅存储表的首地址。在32位操作系统中其占用内存应为4个字节(虚函数表的实现,建议读者查阅一下《C++对象模型》)。

虚函数表

虚函数表是一个函数指针数组,在C++类对象中仅仅存储函数指针数组的首地址。当函数通过虚函数实现调用时,类对象通过查询虚函数表决定哪个函数应该被调用。然后调用此函数完成函数调用执行。

虚函数表生成条件:只要类声明时,包含virtual函数,类对象创建就会生成此对象的虚函数表(vfTable)。并把此虚函数表的首地址(vfptr)记录到此对象中。

通过虚函数的分析,我们可得到这样的结论:影响类对象大小的因素除了上述之外,还有一个虚函数因素。

包含虚函数类对象大小影响因素

  • class对象数据成员占用内存大小,会影响类对象大小。
  • class对象采用的内存对齐策略,会影响类对象大小
  • class对象中普通函数成员不会影响对象大小,但class中virtual函数会影响类对象的大小。因为虚函数导致对象增加4字节空间, 这是 vfptr的缘故。

到此,普通的class对象和继承class对象内存的影响因素都讲述完毕了。现在我们讨论最复杂的一种:virtual继承class对象。我们看下面的代码段:

class A
{
public:
	virtual void f(){}

};
class C : public virtual A
{
public:
	virtual void f(){}
	virtual void g() {}
};
class B : public virtual A
{
public:
	virtual void f(){}
    virtual void h() {}
};
class D :  public B,  public C
{
public:
	virtual void f() {}
	virtual void g() {}
	virtual void h() {}
};

cout<<"sizeof A: " <<sizeof A<<endl
    <<"sizeof B: " <<sizeof B<<endl
    <<"sizeof C: " <<sizeof C<<endl
    <<"sizeof D: " <<sizeof D<<endl;

他们的继承关系如图9-2所示:
在这里插入图片描述

图9-2 继承关系图

可能大家会觉得他们的大小,应该都应该是0,因为他们中没有任何一个有明显的数据,只表示了继承关系。但是至少也认为class A应该是0吧,他什么都没有。但在VC2010环境下测试的结果确让人感到意外:

sizeof A: 4
sizeof B: 12
sizeof C: 12
sizeof D: 20

为什么是这个结果呢。一个空的class事实上并不是空,它有一个隐藏的1 byte, 这个我们前面已讲述过。让人搞不懂的是B,C,D的大小。

关于虚继承

虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfptr指针指向虚函数表vfTable和一个vbptr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数)。

下面说明VC2010的模型, 现在看D的布局图9-3。根据此图我们可以看出D的大小等于4(A基类的虚函数表指针)+8(B基类的虚函数表指针+虚基类指针)+8(C基类的虚函数表指针+虚基类指针)。
在这里插入图片描述

图9-3 D内存布局图

按照这个原理,B类对象大小等于4(A基类的虚函数表指针)+8(B类的虚函数表指针+虚基类指针)。B类对象大小等于4(A基类的虚函数表指针)+8(C基类的虚函数表指针+虚基类指针)。

影响class对象大小的因素

  • 空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
  • 虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的。
  • 一个对象的大小 ≥ 所有非静态成员大小的总和;
  • 当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vfptr指向虚函数表VTable;
  • 虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfptr指针指向虚函数表vfTable和一个vbptr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);
  • 在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”的影响,即编译器会插入多余的字节补齐;
  • 类对象的大小等于各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。

请谨记

  • 在类的设计中,类对象的大小并不仅仅是对象中非静态数据成员的总和。其他因素包括内存对齐,虚函数,虚继承都影响类对象的大小。
  • 在类的设计中,类对象的大小影响因素不包括静态数据成员,因为类中的静态数据成员分布于全局存储区域。不占用类对象的空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值