天天看点

C++ 类 sizeof 计算大小 虚函数 虚继承 虚基类 虚函数表c++类的大小计算

文章目录

  • c++类的大小计算
    • 关于类/对象大小的计算
      • 一.简单情况的计算
      • 二.空类的大小
      • 三.含有虚函数成员
        • (1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数
        • 在派生类中对基类的虚函数进行覆盖
        • (3)多继承:无虚函数覆盖
        • (4)多重继承,含虚函数覆盖
      • 四.虚继承的情况
    • 虚函数表
      • 创建时期
    • 五、 取消字节对齐
#include <iostream>
using namespace std;
class onlyHaveAInt{
private:
	int a;
};
class HaveAIntAndDouble{
	int a;
	double b;
};
class IntDoubleChildren:public HaveAIntAndDouble{
	int c;
	double d;
};
class VirtualInherit:virtual HaveAIntAndDouble{

};
class OnlyAFunc{
	void func(){};
};
class VirtualFunc{
	virtual void func(){};
};
int main(){
	void *getByte=nullptr;
	int size=sizeof(getByte);
	if(size==8){
		cout<<"Bytes=64"<<endl;
	}else if(size==4){
		cout<<"Bytes=32"<<endl;
	}
	cout<<"sizeof(int)="<<sizeof(int)<<" sizeof(double)="<<sizeof(double)<<endl;
	cout<<"-	OnlyHaveAInt Size:"<<sizeof(onlyHaveAInt)<<endl;
	cout<<"-	HaveAIntAndDouble Size:"<<sizeof(HaveAIntAndDouble)<<endl;
	cout<<"-	OnlyHaveAFunc Size:"<<sizeof(OnlyAFunc)<<endl;
	if(sizeof(OnlyAFunc)==1){
		cout<<"Class with Func do not get ram"<<endl;
	}
	cout<<"-	VirtualFunc Size:"<<sizeof(VirtualFunc)<<endl;
	
	cout<<"-	IntDoubleChildren Size:"<<sizeof(IntDoubleChildren)<<endl;
	cout<<"-	VirtualInherit Size:"<<sizeof(VirtualInherit)<<endl;
}


           

输出:

Bytes=64
sizeof(int)=4 sizeof(double)=8
-	OnlyHaveAInt Size:4
-	HaveAIntAndDouble Size:16
-	OnlyHaveAFunc Size:1
Class with Func do not get ram
-	VirtualFunc Size:8
-	IntDoubleChildren Size:32
-	VirtualInherit Size:24
           

c++类的大小计算

c++中类所占的大小计算并没有想象中那么简单,因为涉及到虚函数成员,静态成员,虚继承,多继承以及空类等,不同情况有对应的计算方式,在此对各种情况进行总结。

首先要明确一个概念,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 我们这里指的类的大小,其实指的是类的对象所占的大小。因此,如果用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。

关于类/对象大小的计算

首先,类大小的计算遵循结构体的对齐原则

struct/class/union内存对齐原则有四个:

1).数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节, 则要从4的整数倍地址开始存储),基本类型不包括struct/class/uinon。

2).结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。

3).收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的"最宽基本类型成员"的整数倍.不足的要补齐.(基本类型不包括struct/class/uinon)。

4).sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。

类的大小与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响

虚函数对类的大小有影响,是因为虚函数表指针带来的影响

虚继承对类的大小有影响,是因为虚基表指针带来的影响

空类的大小是一个特殊情况,空类的大小为1

解释说明

静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成员被该类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区。

空类的大小,以及含有虚函数,虚继承,多继承是特殊情况,接下来会一一举例说明

注意:因为计算涉及到内置类型的大小,接下来的例子运行结果是在64位gcc编译器下得到的。int的大小为4,指针大小为8

一.简单情况的计算

#include<iostream>
using namespace std;
class base
{
    public:
    base()=default;
    ~base()=default;
    private:
    static int a;
    int b;
    char c;

};
int main()
{
    base obj;
    cout<<sizeof(obj)<<endl;
}
           

计算结果:8

静态变量a不计算在对象的大小内,由于字节对齐,结果为4+4=8

二.空类的大小

本文中所说是C++的空类是指这个类不带任何数据,即类中没有非静态(non-static)数据成员变量,没有虚函数(virtual function),也没有虚基类(virtual base class)。

直观地看,空类对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而,C++标准规定,凡是一个独立的(非附属)对象都必须具有非零大小。换句话说,c++空类的大小不为0

为了验证这个结论,可以先来看测试程序的输出。

#include <iostream>
using namespace std;

class NoMembers
{
};

int main()
{
    NoMembers n;  // Object of type NoMembers.
    cout << "The size of an object of empty class is: "
         << sizeof(n) << endl;
}
           

输出:

The size of an object of empty class is: 1

C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址。这是由于:

new需要分配不同的内存地址,不能分配内存大小为0的空间

避免除以 sizeof(T)时得到除以0错误

故使用一个字节来区分空类。

但是,有两种情况值得我们注意

第一种情况,涉及到空类的继承。

当派生类继承空类后,派生类如果有自己的数据成员,而空基类的一个字节并不会加到派生类中去。例如

class Empty {};
struct D : public Empty { int a;};
           

sizeof(D)为4。

第二中情况,一个类包含一个空类对象数据成员。

class Empty {};
class HoldsAnInt {
    int x;
    Empty e;
};
           

sizeof(HoldsAnInt)为8。

因为在这种情况下,空类的1字节是会被计算进去的。而又由于字节对齐的原则,所以结果为4+4=8。

继承空类的派生类,如果派生类也为空类,大小也都为1。

三.含有虚函数成员

首先,要介绍一下虚函数的工作原理:

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。

每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类 中,编译器秘密地置入一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的vTable。 当构造该派生类对象时,其成员vPtr被初始化指向该派生类的vTable。所以可以认为vTable是该类的所有对象共有的,在定义该类时被初始化;而vPtr则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。

假设我们有这样的一个类:

class Base {

public:

virtual void f() { cout << "Base::f" << endl; }

virtual void g() { cout << "Base::g" << endl; }

virtual void h() { cout << "Base::h" << endl; }

};
           

当我们定义一个这个类的实例,Base b时,其b中成员的存放如下:

C++ 类 sizeof 计算大小 虚函数 虚继承 虚基类 虚函数表c++类的大小计算

指向虚函数表的指针在对象b的最前面。

虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符”\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在vs下,这个值是NULL。而在linux下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。

因为对象b中多了一个指向虚函数表的指针,而指针的sizeof是8,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加8。

例如:

class Base {

public:
int a;
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
           

sizeof(Base)为16。

vptr指针的大小为8,又因为对象中还包含一个int变量,字节对齐得8+8=16。

下面将讨论针对基类含有虚函数的继承讨论:

(1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数

比如有如下的派生类:

class Derived: public Base
{
public:
virtual void f1() { cout << "Derived::f1" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
           

基类和派生类的关系如下:

这里写图片描述

当定义一个Derived的对象d后,其成员的存放如下:

这里写图片描述

可以发现:

1)虚函数按照其声明顺序放于表中。

2)基类的虚函数在派生类的虚函数前面。

此时基类和派生类的sizeof都是数据成员的大小+指针的大小8。

在派生类中对基类的虚函数进行覆盖

假设有如下的派生类:

class Derived: public Base
{
public:
virtual void f() { cout << "Derived::f" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
           

基类和派生类之间的关系:其中基类的虚函数f在派生类中被覆盖了

这里写图片描述

当我们定义一个派生类对象d后,其d的成员存放为:

这里写图片描述

可以发现:

1)覆盖的f()函数被放到了虚表中原来基类虚函数的位置。

2)没有被覆盖的函数依旧。

派生类的大小仍是基类和派生类的非静态数据成员的大小+一个vptr指针的大小

这样,我们就可以看到对于下面这样的程序,

Base *b = new Derive();
b->f();
           

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

(3)多继承:无虚函数覆盖

假设基类和派生类之间有如下关系:

C++ 类 sizeof 计算大小 虚函数 虚继承 虚基类 虚函数表c++类的大小计算

对于派生类实例中的虚函数表,是下面这个样子:

C++ 类 sizeof 计算大小 虚函数 虚继承 虚基类 虚函数表c++类的大小计算

我们可以看到:

1) 每个基类都有自己的虚表。

2) 派生类的成员函数被放到了第一个基类的表中。(所谓的第一个基类是按照声明顺序来判断的)

由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加上三个指针的大小。

(4)多重继承,含虚函数覆盖

假设,基类和派生类又如下关系:派生类中覆盖了基类的虚函数f

C++ 类 sizeof 计算大小 虚函数 虚继承 虚基类 虚函数表c++类的大小计算

下面是对于派生类实例中的虚函数表的图:

C++ 类 sizeof 计算大小 虚函数 虚继承 虚基类 虚函数表c++类的大小计算

我们可以看见,三个基类虚函数表中的f()的位置被替换成了派生类的函数指针。这样,我们就可以任一静态类型的基类类来指向派生类,并调用派生类的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
           

此情况派生类的大小也是类的所有非静态数据成员的大小+三个指针的大小

举一个例子具体分析一下大小吧:

#include<iostream>
using namespace std;
class A     
{     
};    
class B     
{  
    char ch;     
    virtual void func0()  {  }   
};   
class C    
{  
    char ch1;  
    char ch2;  
    virtual void func()  {  }    
    virtual void func1()  {  }   
};  
class D: public A, public C  
{     
    int d;     
    virtual void func()  {  }   
    virtual void func1()  {  }  
};     
class E: public B, public C  
{     
    int e;     
    virtual void func0()  {  }   
    virtual void func1()  {  }  
};  

int main(void)  
{  
    cout<<"A="<<sizeof(A)<<endl;    //result=1  
    cout<<"B="<<sizeof(B)<<endl;    //result=16      
    cout<<"C="<<sizeof(C)<<endl;    //result=16  
    cout<<"D="<<sizeof(D)<<endl;    //result=16  
    cout<<"E="<<sizeof(E)<<endl;    //result=32  
    return 0;  
}  
           

结果分析:

1.A为空类,所以大小为1

2.B的大小为char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16

3.C的大小为两个char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16

4.D为多继承派生类,由于D有数据成员,所以继承空类A时,空类A的大小1字节并没有计入当中,D继承C,此情况D只需要一个vptr指针,所以大小为数据成员加一个指针大小。由于字节对齐,大小为8+8=16

5.E为多继承派生类,此情况为我们上面所讲的多重继承,含虚函数覆盖的情况。此时大小计算为数据成员(按单个类对齐)的大小+2个基类虚函数表指针大小

考虑字节对齐,结果为8+8+2*8=32

四.虚继承的情况

虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。

虚继承要求子类不和父类共享虚表指针

对虚继承层次的对象的内存布局,在不同编译器实现有所区别。

在这里,我们只说一下在gcc编译器下,虚继承大小的计算。

它在gcc下实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。

class A {
    int a;
};

class B:virtual public A{
    virtual void myfunB(){}
};

class C:virtual public A{
    virtual void myfunC(){}
};

class D:public B,public C{
    virtual void myfunD(){}
};
           

以上代码中sizeof(A)=16,sizeof(B)=24,sizeof©=24,sizeof(D)=32.

解释:A的大小为int大小加上虚表指针大小。

B,C中由于是虚继承因此大小为int大小加指向虚基类的指针的大小。

B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针,他们两个共用虚基类A的虚表指针。 B/C的虚基类指针+虚表指针+A类

D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于int变量的大小+B中的指向虚基类的指针+C中的指向虚基类的指针+一个虚表指针的大小,由于字节对齐,结果为8+8+8+8=32。

虚函数表

  • 拥有虚函数的类会有一个虚表,而且这个虚表存放在类定义模块的数据段中。模块的数据段通常存放定义在该模块的全局数据和静态数据区,这样我们可以把虚表看作是模块的全局数据或者静态数据
  • 类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们的虚表指针都指向同一个虚表,从这个意义上说,我们可以把虚表简单理解为类的静态数据成员。值得注意的是,虽然虚表是共享的,但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针。
  • 虚表中存放的是虚函数的地址。

创建时期

虚函数和虚函数表是两个东西,虚函数调用是在Run-Time时确定,虚函数表是在compile-Time时期决定的。

五、 取消字节对齐

c/c++下取消结构体字节对齐方法

在c/c++下编译器会默认地对结构体进行对齐,其对齐的方法跟平台具体的特性有关,本文主要介绍结构体不进行对齐的方法。

1、结构体字节不进行对齐的用途

(1)、减小内存占用的空间

结构体默认进行对齐,占用的空间比结构体内部成员变量字节加起来大,如果取消字节对齐,可以减小一部分空间。见下面具体例子。

(2)、直接将结构体作为通信协议(在低带宽下通讯)

在不同的平台下,保证结构体内基本数据的长度相同,同时取消结构体的对齐,就可以将定义的数据格式结构体直接作为数据通信协议使用。

2、结构体字节不对齐的方法

利用伪指令 #pragma pack (n) 可以动态的调整内存对齐的方式:

#pragma pack (n)  // 编译器将按照n个字节对齐;
#pragma pack()   // 恢复先前的pack设置,取消设置的字节对齐方式
#pragma  pack(pop)// 恢复先前的pack设置,取消设置的字节对齐方式
#pragma  pack(1)  // 按1字节进行对齐 即:不行进行对齐
           

继续阅读