Not Only Algorithm,不仅仅是算法,关注数学、算法、数据结构、程序员笔试面试以及一切涉及计算机编程之美的内容 。。
你的位置:NoAlGo博客 » 面试 » ,

C++ sizeof的内存计算(2)

内存控制是程序设计过程中非常关键的一环,C/C++中使用sizeof计算数据占用的内存大小是一个常见的手段,但是这个问题涉及到很多基础的编程细节,能够很好地反映一个程序员的基本功,成为了笔试面试常见的问题之一。
这里总结了一些常见的问题,鉴于篇幅问题,分成两部分进行,这里主要介绍稍微进阶的第二部分。

  1. C++ sizeof的内存计算(1)
  2. C++ sizeof的内存计算(2)

三 复合类型

对于基本的数据类型,如char、int、double,其在计算机所占内存的大小一般是固定的,如32位机器中,其大小为1Byte、4Byte、8Byte。对于复合类型而言,其所占内存是大小跟成员变量的出现位置有很大关系,并不是总是等于各个数据成员所占内存大小之和。

为了提高cpu对内存的访问效率,编译器会调整结构成员在内存的位置,使其地址满足一定的条件,称为内存对齐。

  1. 结构体变量的首地址是其最长基本类型成员的整数倍。
  2. 结构体内每个成员相对于结构体首地址的偏移量都是成员大小的整数倍。因为结构体首地址是最长类型的整数倍,所以每个成员的地址也是其类型长度的整数倍。如果某个成员的长度不足以满足这个条件,编译器会在成员中间添加适当的填充字节,使其能够满足整数倍的要求。
  3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍。如果不满足,编译器会在结构体最后添加适当的填充字节。
  4. 如果结构体内最长基本类型长度大于处理器的位数,以处理器的倍数为对齐单位;否则,以结构体里面最长基本类型为对齐单位。

具体参考如下代码的输出结果。

struct s1{
	int a;
	char b;
};
struct s2{
	char a; 
	int b;
};
struct s3{
	char a;
	s1 s;
};
struct s4{
	char a[13];
	int b;
};
void test(){
	printf("int 大小:%d\n", sizeof(int));  //int 大小:4
	printf("char大小:%d\n", sizeof(char)); //char大小:1
	printf("s1  大小:%d\n", sizeof(s1));   //s1  大小:8
	printf("s2  大小:%d\n", sizeof(s2));   //s2  大小:8
	printf("s3  大小:%d\n", sizeof(s3));   //s3  大小:12
	printf("s4  大小:%d\n", sizeof(s4));   //s3  大小:20
}

s1成员int的长度为4,char长度为1,于是以4为结构体对齐单位。a的偏移量为0,满足条件。b的偏移量为4,满足条件。但整体长度为5不是4的倍数,于是编译在后面补上3个字节,整体长度变为8。

而s2中a的偏移量为0,b的偏移量为1,b不满足条件,于是在a、b之间填充3个字节使之满足条件。

s3中涉及结构体嵌套的问题,即结构体的成员中又有其他的结构体成员,在寻找最宽基本类型成员时,应该把结构体成员拆分来看,即逐个考虑结构体成员的每个成员。于是s3的对其单位为int的4个字节。

s4中涉及数组一类的类型相同的连续元素,此时每个元素在内存中会连续排列,而不会填充字节。但整体还是要满足倍数倍数条件,必要时在后面填充字节。

内存数据对齐是编译在编译时的优化操作,但我们在写程序时可以指定内存对齐的对齐单位,如使用#pragma pack(n)即告诉编译以n字节为标准对齐数据,使用#pragma pack()恢复默认的对齐标准。

注意以下代码中输出的内存大小与上面代码结果的差异。

#pragma pack(1)

struct s1{
	int a;
	char b;
};
struct s2{
	char a; 
	int b;
};
struct s3{
	char a;
	s1 s;
};
struct s4{
	char a[13];
	int b;
};
void test(){
	printf("int 大小:%d\n", sizeof(int));  //int 大小:4
	printf("char大小:%d\n", sizeof(char)); //char大小:1
	printf("s1  大小:%d\n", sizeof(s1));   //s1  大小:5
	printf("s2  大小:%d\n", sizeof(s2));   //s2  大小:5
	printf("s3  大小:%d\n", sizeof(s3));   //s3  大小:6
	printf("s4  大小:%d\n", sizeof(s4));   //s3  大小:17
}

四 继承

类中的数据成员会占用一定的内存,但成员函数一般是不占用对象内存的。成员函数被放在代码区,创建对象时不会为成员函数分配空间,而是所有对象共享这一份函数代码。于是,对于空类或只有成员成员函数的类,其占用的内存为1个字节。

但是当成员函数中有虚函数,情况就会不一样。首先要弄清楚什么是虚函数,虚函数是C++中为了实现多态而在基类中声明为virtual的成员函数,派生类可以重写这个虚函数,之后可以使用基类的指针实现函数调用的动态绑定。当类中声明有虚函数时,其会隐式地包含一个成员变量:虚函数指针,这个虚指针指向一个虚函数表,虚函数表的包含有类的信息以及这个类中所有虚函数的地址。于是含有虚函数的空类的大小需要包含这个虚指针的大小,即为4,而且不论有多少个虚函数,大小均为4。

class A{};
class B{ void f(); };
class C{ virtual void f(); };
class D{ 
	virtual void f(); 
	virtual void g(); 
};
void test(){
	printf("sizeof(A)=%d\n", sizeof(A)); //sizeof(A)=1
	printf("sizeof(B)=%d\n", sizeof(B)); //sizeof(B)=1
	printf("sizeof(C)=%d\n", sizeof(C)); //sizeof(C)=4
	printf("sizeof(D)=%d\n", sizeof(D)); //sizeof(D)=4
}

下面考虑继承时内存占用的变化。

继承时子类会继承父类的成员,虽然父类的私有成员在子类中不可用的,但是其还要占据子类的内存空间。所以子类的内存就是子类本身的成员加上父类继承下来的成员所占用的内存之和。

class A{ void f(); };
class B{ virtual void f(); };
class AA : public A{};
class B1 : public B{};
class B2 : public B{ virtual void g(); };

void test(){
	printf("sizeof(A)=%d\n", sizeof(A));   //sizeof(A)=1
	printf("sizeof(B)=%d\n", sizeof(B));   //sizeof(B)=4
	printf("sizeof(AA)=%d\n", sizeof(AA)); //sizeof(AA)=1
	printf("sizeof(B1)=%d\n", sizeof(B1)); //sizeof(B1)=4
	printf("sizeof(B2)=%d\n", sizeof(B2)); //sizeof(B2)=4
}

C++为了实现多重继承引进了虚继承的概念。主要解决的问题是当B、C都继承了A时,如果D继承了B和C,那么D中就会两份A的成员的副本,这时就需要把B和C的继承设为虚继承。

使用虚继承可以避免多个基类对象的拷贝,与普通继承不同,其需要一个指向基类的指针,占用大小为4。具体参看如下代码,注意类D和类E的区别。

class A{};
class B : public virtual A{};
class C : public virtual A{};
class D : public B, public C{};
class E : public virtual B, public virtual C{};
void test(){
	printf("sizeof(A)=%d\n", sizeof(A)); //sizeof(A)=1
	printf("sizeof(B)=%d\n", sizeof(B)); //sizeof(B)=4
	printf("sizeof(C)=%d\n", sizeof(C)); //sizeof(C)=4
	printf("sizeof(D)=%d\n", sizeof(D)); //sizeof(D)=8
	printf("sizeof(E)=%d\n", sizeof(E)); //sizeof(E)=12
}

使用了虚继承的多继承,类内会有多个多张虚函数表,即多个虚指针指向这些表。

下面以一个较为综合的例子总结这部分内容。

class A{
	char a;
	virtual void fA();
};
class B : public A{
	virtual void fB();
};
class C : public virtual A{
	virtual void fC();
};
class D : public virtual A{
	virtual void fD();
};
class E : public C, public D{
	virtual void fE();
};
class F : public virtual C, public virtual D{
	virtual void fF();
};
void test(){
	printf("sizeof(A)=%d\n", sizeof(A)); 
	printf("sizeof(B)=%d\n", sizeof(B)); 
	printf("sizeof(C)=%d\n", sizeof(C)); 
	printf("sizeof(E)=%d\n", sizeof(D)); 
	printf("sizeof(E)=%d\n", sizeof(E)); 
	printf("sizeof(F)=%d\n", sizeof(F)); 

	//程序输出结果为:
	//sizeof(A)=8 虚指针&内存对齐
	//sizeof(B)=8
	//sizeof(C)=16
	//sizeof(E)=16
	//sizeof(E)=24
	//sizeof(F)=32
}
上一篇: 下一篇:
  1. //程序输出结果为:
    //sizeof(A)=8 虚指针&内存对齐
    //sizeof(B)=8
    //sizeof(C)=12
    //sizeof(D)=12
    //sizeof(E)=16
    //sizeof(F)=16
    自己机器上运行的结果是这样的。

我的博客

NoAlGo头像编程这件小事牵扯到太多的知识,很容易知其然而不知其所以然,但真正了不起的程序员对自己程序的每一个字节都了如指掌,要立足基础理论,努力提升自我的专业修养。

站内搜索

最新评论