c++类的大小计算及分析windows中VS与Linux中gcc结果的差异

 

 写在开头:文中提到Windows中VS用的是默认的MSVC编译器,Linux使用Gcc编译器。其实在Windows下使用MinGW可以得到与Gcc一样的结果。

另外,在结构体中存在long,int的时候还需要考虑编译器数据模型:关于int,long等数据类型占用字节数


喵哥最近在复习C++虚函数的时候遇到一道题,关于计算类的大小的问题,为了吃透这种题目,在网上找到几篇文章学习了一下。也发现了不少问题,借此契机记录一二。

C++的类大小计算通常会加入虚函数,静态成员,虚继承,多继承等情况,这些情况都对应着不同的计算方式。

在计算类占用空间的大小时,一般先粗略的用如下规则进行判断:

1.类大小的计算遵循结构体的对齐原则

2.类的大小与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,        
  静态常量数据成员均对类的大小无影响(静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成        
  员被该类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区)

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

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

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

VS与gcc的编译结果都是在继承的地方会出现偏差,所以在后面再具体区分gcc和VS,使用的都是x64,指针大小为8字节。

1.字节对齐。静态成员、普通成员函数无影响

#include<iostream>
using namespace std;


class base
{
    public:
    base()=default; //构造函数
    ~base()=default;//析构函数
    void haha(int x){ b = x;}
    private:
    static int a; //静态成员
    int b;
    char c;

};


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

base的大小为8。用之前的判断方法,只有b和c是需要计算大小的,又由于字节对齐的要求,所以就是4+4 = 8。

需要注意的是这个字节对齐的方式,它是按照变量声明的顺序对齐的。比如:

int b;
char c1;
char c2;

这样的结果也是8,因为后面的俩char可以在一个4字节的段内。

而对于这样的情况:

char c1;
int b;
char c2;

由于俩char分开了,总的大小变成12字节。

 

2.计算空类

对于一个不带任何数据的类,生成的对象大小不会为0,毕竟需要分配内存的,大小为1

在继承空类时,这一个字节是不会被加到派生类中的。但是在一个类中声明一个空类对象为数据成员,那么在计算这个类时,需要加上这一个字节。

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

AA的大小为8。

 

3.含有虚函数的类

 虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。 
  每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类 中,编译器秘密地置入一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。 当构造该派生类对象时,其成员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; }

};

一个没有继承的其他虚函数表的类只有一个虚函数表,所以其对象只有一个虚函数表指针。在x64的环境下,大小为8.

è¿éåå¾çæè¿°

虚函数指针在对象的最前面,所以在对齐字节的时候不用考虑虚函数声明的位置。

#include <iostream>
using namespace std;

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; }
	char c;
};

int main()
{
	Base obj;
	cout << sizeof(obj) << endl;
	return 0;
}

这里的对齐方式是:

所以结果为16。

 

4.含有虚函数的继承

4.1派生类不覆盖基类的派生类,并且增加新的虚函数。

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; }
	char c;
};


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的对象的虚函数表的指针指向的的结构为:

è¿éåå¾çæè¿°

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

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

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

 

4.2派生类覆盖基类的派生类的虚函数

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; }
	char c;
};


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; }

};

此时Derived对象的虚函数表的指针指向的结构为:

è¿éåå¾çæè¿°

可以发现:

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

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

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

 

所以继承一个基类的情况下,只会增加一个虚函数表。同理,简单的继承多个基类的情况,就会有同样个数的虚函数表。

 

4.3多继承:无虚函数覆盖

继承关系:

è¿éåå¾çæè¿°

 

派生类对象的虚函数表:

è¿éåå¾çæè¿°

 

我们可以看到:

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

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

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

4.4多重继承,含虚函数覆盖

继承的结构:

è¿éåå¾çæè¿°

 

派生类对象的虚函数表:

è¿éåå¾çæè¿°

 

#include<iostream>
using namespace std;


class A
{
	/*char a;
	long long aa;*/
};

class B
{
	virtual void func0() {  }
	char ch;
	char ch1;
};

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() {  }
};

class F : public D, public E {
	int f;
};

int main(void)
{
	//测试结果再x64下运行
	cout << "int = " << sizeof(int) << endl;
	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=24
	cout << "E=" << sizeof(E) << endl;    //result=40
	cout << "F=" << sizeof(F) << endl;    //result=72
	return 0;
}

注意:以上结果都是在VS的x64环境下运行的结果,如果在gcc下会不一样,具体表现为:

        cout << "int = " << sizeof(int) << endl;
	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
	cout << "F=" << sizeof(F) << endl;    //result=56
	return 0;

因为派生类会继承基类的普通数据成员,所以需要考虑数据的对齐方式。在windows中,各个基类的数据各自对齐,派生类也自己对齐。

 

在linux下,最后一个基类的数据将和派生类的数据一起对齐。

用一个例子说明:

#include<iostream>
using namespace std;



class B
{
	virtual void func0() {  }
	char ch;
};

class C
{
	char ch1;
	short c2;
	virtual void func() {  }
	virtual void func1() {  }
};


class E : public B, public C
{
	int e;
	virtual void func0() {  }
	virtual void func1() {  }
};



int main(void)
{
	//测试结果再x64下运行
	cout << "E=" << sizeof(E) << endl;    //Windows result=40  Linux result=32
	return 0;
}

分析:

在windows下,B、C、E类的数据成员分别对齐8字节,又有两个虚函数表的指针,所以8*5 = 40.

在Linux下,B单独对齐8字节,C、E的数据对齐8字节,加上两个虚函数表的指针,8*4 = 32.

并且这个例子可以说明,B、C、E三个类的数据没有一起对齐,假设三个类一起参与字节对齐,那么:

 

红绿蓝分别代表:char、short、int。完全可以对齐一个8字节空间,这样的话,结果应该是24,但是事实上在Linux中是32.

 

5.虚继承的情况

虚继承时,会产生虚基表,有一个虚基表指针,用来指向虚基类,多重继承虚基类就有多个虚基表指针。

如下例子(x64):

#include<iostream>  
using namespace std;

class A  //大小为4  
{
public:
	int a;
};
class B :virtual public A  //Windows 24  Linux 16
{
public:
	int b;
};
class C :virtual public A  //Windows 24  Linux 16
{
public:
	int c;
};
class D :public B, public C  //Windows 48  Linux 40
{
public:
	int d;
};

int main()
{
	A a;
	B b;
	C c;
	D d;
	cout << sizeof(a) << endl;
	cout << sizeof(b) << endl;
	cout << sizeof(c) << endl;
	cout << sizeof(d) << endl;
	return 0;
}

A类的对象大小为4字节是比较清楚的。

对于Windows和Linux的情况分别画图解释。

Windows:

B类继承了A类的int a(4字节),自己拥有一个int b(字节),并且有一个虚基表的指针(8字节),这个指针位于成员变量的前面。

 

 

C类是类似的情况。

D类两个指针在前面,各自的成员变量各自对齐。

 

Linux

类似于虚函数的继承情况,派生类与最后一个基类的成员变量进行对齐,拿D类作示例:

 

C、D的成员变量一起参与对齐。

 

 

 

发布了55 篇原创文章 · 获赞 29 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/ruibin_cao/article/details/91363995