C语言结构体大小及对齐问题

写在前面:
本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。

目录

一、内存大小问题

二、分配问题

三、结构体分配的空间

四、内存大小对齐原则

五、其他


一、内存大小问题

有时候,我们在不同的编译环境,或者不同的机子上测试编译,会呈现不同的结果,于是我们会陷入疑问,内存的大小是谁分配的呢?

在系统中,系统对内存的识别是以 Byte(字节)为单位,每个字节由8位二进制数组成,即 8bit(比特,也称“位”)。按照计算机的二进制方式,1Byte=8bit;1KB=1024Byte;1MB=1024KB;1GB=1024MB;1TB=1024GB。

我们都知道 Byte(字节)的大小是固定的,但是,我们目前接触的都是以 Word(字长)、HalfWord(半字),而他的解释是:在电脑领域,对于某种特定的计算机设计而言,(英语:word)是用于表示其自然的数据单位的术语。在这个特定电脑中,字是其用来一次性处理事务的一个固定长度的位(bit)组。一个字的位数(即字长)是电脑系统结构中的一个重要特性。

来源:字 (计算机) - 维基百科

二、分配问题

我们之所以有可能出现编译的数据内存大小不一样,是因为在不同的处理器和编译器中它所呈现的结果是不一样的,直白的说就是所对应的 Word(字长)不一样,其实字长并非一个十分严格的概念,要从汇编语言的角度理解,就是指令集里面的运算和内存操作时操作数的长度,具体还是看一下关于计算机底层的东西。

在 32位处理器中 32位指的就是 CPU GPRs(General-Purpose Registers,通用寄存器)的数据宽度,当然 64位 CPU的数据宽度为 64位,所以 32位CPU的数据宽度指的是 32位了。

64位指令集就是运行 64位数据的指令,也就是说处理器一次可以运行 64Bit数据。这样一来 32位处理器一次最多只能处理 32位,也就是 4个字节的数据,而 64位处理器一次就能处理 64位,即 8个字节的数据。

按照上面总的来说:各种类型的存储大小与系统位数有关

而至于为什么又跟编译器有关呢?

假设我们在 64位处理器上运行 32位的编译器来写代码的,这样编译器就只会默认我们的程序是在 32位系统下运行,因为这编译器最多只能处理 32位,多了它处理不来啊 [哭笑] 

所以,一般严格来说,在进行编译测试后,想要告诉别人结果,也应该把你当时所测试的操作系统以及编译系统告诉别人,同样的例子也有:就像我们在下载一个程序的时候,你可能会看到程序文件名后面带有 x32 或者 x64等一些标志性文本,这就是要告诉别人它支持的操作环境

三、结构体分配的空间

在进行讨论之前先来看一下程序

/* 
 * 操作系统:基于 x64的处理器
 * 编译环境:Dev-C++ V5.11 
 */

#include <stdio.h> 

#define SIZE		1

struct s {
    char a;
    short b;
    int c;
    double d;
    char* e;
    char f[SIZE];
//    double i;
};
struct s temp;

char *p = NULL;

int main(void)
{
	printf("char:%d,short:%d,int:%d,double:%d\n\n",sizeof(char),sizeof(short),sizeof(int),sizeof(double));
	printf("sizeof = %d\n",sizeof(char *));
	printf("sizeof = %d\n\n",sizeof(temp));
	
	printf("temp addr = %p\n",&temp);
	printf("a    addr = %p,%2d\n",&temp.a,sizeof(temp.a));
	printf("b    addr = %p,%2d\n",&temp.b,sizeof(temp.b));
	printf("c    addr = %p,%2d\n",&temp.c,sizeof(temp.c));
	printf("d    addr = %p,%2d\n",&temp.d,sizeof(temp.d));
	printf("e    addr = %p,%2d\n",&temp.e,sizeof(temp.e));
	printf("\nf    addr = %p\n",&temp.f);
	printf("f    sizeof = %d\n",sizeof(temp.f));
	
//	printf("\ni addr = %p,%d\n",&temp.i,sizeof(temp.i));

	
	return 0;
}

我们定义了一个 struct s 的结构体,里面有不同数据类型的结构体成员 .a /.b /.c /.e /.f(成员 i我们先屏蔽先,这个是关于后面对齐问题测试的);成员 f为数组,特殊点我们把地址跟大小分开打印;指针我们不知道他的大小,也打印出来;最后我们打印一下输出看下结果:

先看第一行,我们打印出了 char /short /int /double所对应的数据类型大小;

第二行我们是打印了指针的数据大小,大小为 8Byte(因为指针在实质上是一个内存地址,内存地址的长度跟 CPU的寻址有关;在32位系统上, CPU用 32位表示一个内存地址,这样的系统上一个指针占据 4个字节(32/8=4);在 64位系统上, CPU用 64位表示一个内存地址,这样的系统上一个指针占据 8个字节(64/8=8))

第三行是打印出结构体总的大小为 32

再到后面的,当前的结构体地址对应着第一个结构体成员地址,这个没问题;后面每个结构体成员所对应的数据类型大小也没问题;但是,当我们一加起来 1+2+4+8+8+1 = 24 ???跟打印出来的总大小不一样啊,这就关系到数据对齐了

四、内存大小对齐原则

  1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
  2. 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)。即结构体成员的末地址减去结构体首地址(第一个结构体成员的首地址)得到的偏移量都要是对应成员大小的整数倍。
  3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在成员末尾加上填充字节。

看了以上的原则后,我们来继续分析一下上面的数据类型大小问题

实际上:结构体 s = a(1byte)+空闲(1byte)+b(2byte)+c(4byte)+d(8byte)+e(8byte)+f(1byte)+空闲(7byte)=32(byte)。

解释一下:上面输出的结构体变量的首地址为 0x0407A20,对于第一个成员 a的地址就是结构体的首地址,占用 1个字节(符合第一个的原则);因为成员 a只用了 1个字节,而成员 b地址要 2的倍数,中间隔着一个字节,那么就需要把这个一个字节填充进去,为后面的成员 b的地址制造出 2的倍数的地址数);成员 b自己的大小为 2byte,他所对的地址必须是 2的倍数,那么在填充完那一个字节后,他的地址排下去就是 0x0407A22,对应上了 2的倍数(即上面的第二点原则);成员 c大小为 4,同样的他所对的地址就得要 4的倍数,算一下前面一共占用的地址数:a(1byte)+空闲(1byte)+b(2byte) = 4(byte),当前的地址就已经是 4的倍数了,所以不需要填充;后面两个成员也一样;然后再到最后一个成员 f的分析,因为这是最后一个成员了,这样就已经知道在整个结构体 s中他的最宽基本类型成员大小是多小了,没错,是 8byte(double类型或者指针的内存地址大小);那么按照第三点的原则,我们先能加起来的先加起来,先不考虑最后一项成员需要填充多少个字节: a(1byte)+空闲(1byte)+b(2byte)+c(4byte)+d(8byte)+e(8byte)+f(1byte) = 25(byte);25byte明显不是最宽基本类型成员大小 8byte的倍数了,那么距离 25最近的 8的倍数有两个 24和 32,若是选 24,内存明显不够放,所以只能取大进行填充 7byte,以便总大小成为 32byte

因此简单总结一下(在 x64位机,64位编译器中):

类型 对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
Char 偏移量必须为 sizeof(char)即 1的倍数
Short 偏移量必须为 sizeof(short)即 2的倍数
int 偏移量必须为 sizeof(int)即 4的倍数
float 偏移量必须为 sizeof(float)即 4的倍数
double  偏移量必须为 sizeof(double)即 8的倍数

各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节会自动填充。同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

五、其他

案例一(结构体中的成员数组不指定大小,只限于成员数组在结构体尾部):

/* 
 * 操作系统:基于 x64的处理器
 * 编译环境:Dev-C++ V5.11 
 */

#include <stdio.h> 

#define SIZE		1

struct s {
    char a;
    short b;
    int c;
    double d;
    char* e;
    char f[];
//    double i;
};
struct s temp;

char *p = NULL;

int main(void)
{
	printf("char:%d,short:%d,int:%d,double:%d\n\n",sizeof(char),sizeof(short),sizeof(int),sizeof(double));
	printf("sizeof = %d\n",sizeof(char *));
	printf("sizeof = %d\n\n",sizeof(temp));
	
	printf("temp addr = %p\n",&temp);
	printf("a    addr = %p,%2d\n",&temp.a,sizeof(temp.a));
	printf("b    addr = %p,%2d\n",&temp.b,sizeof(temp.b));
	printf("c    addr = %p,%2d\n",&temp.c,sizeof(temp.c));
	printf("d    addr = %p,%2d\n",&temp.d,sizeof(temp.d));
	printf("e    addr = %p,%2d\n",&temp.e,sizeof(temp.e));
	printf("\nf    addr = %p\n",&temp.f);
//	printf("f    sizeof = %d\n",sizeof(temp.f));
	
//	printf("\ni addr = %p,%d\n",&temp.i,sizeof(temp.i));

	
	return 0;
}
 

运行结果:

可以发现在程序中,由于没有为结构体成员数组 f指定大小,将不为其分配空间

案例二(在案例一下,再追加一个结构体成员 i(double类型)):

#include <stdio.h> 

#define SIZE		1

struct s {
    char a;
    short b;
    int c;
    double d;
    char* e;
    char f[];
    double i;
};
struct s temp;

char *p = NULL;

int main(void)
{
	printf("char:%d,short:%d,int:%d,double:%d\n\n",sizeof(char),sizeof(short),sizeof(int),sizeof(double));
	printf("sizeof = %d\n",sizeof(char *));
	printf("sizeof = %d\n\n",sizeof(temp));
	
	printf("temp addr = %p\n",&temp);
	printf("a    addr = %p,%2d\n",&temp.a,sizeof(temp.a));
	printf("b    addr = %p,%2d\n",&temp.b,sizeof(temp.b));
	printf("c    addr = %p,%2d\n",&temp.c,sizeof(temp.c));
	printf("d    addr = %p,%2d\n",&temp.d,sizeof(temp.d));
	printf("e    addr = %p,%2d\n",&temp.e,sizeof(temp.e));
	printf("\nf    addr = %p\n",&temp.f);
//	printf("f    sizeof = %d\n",sizeof(temp.f));
	
	printf("\ni addr = %p,%d\n",&temp.i,sizeof(temp.i));

	
	return 0;
}

这下编译直接报错: [Error] flexible array member not at end of struct,因为成员数组 f并不知道他的大小

发布了48 篇原创文章 · 获赞 17 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_42992084/article/details/104219659