浅谈Struct(结构体大小如何确定以及内部成员如何存放)

Struct结构体的成员存放以及空间大小

今天看《C和指针》这本书时,书中10.3结构的存储分配一小节提到了结构体的存放规则。于是突然心血来潮,想要实际测试一下结构体的空间占用、成员内存存放情况。

测试IDE,codeblocks。

1.结构体成员在内存中的存放顺序

首先聊聊最简单的结构体成员在内存的存放顺序。

Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increasein the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.

按照C99标准的内容,结构体内的成员是按照成员变量的声明顺序,依次存放在内存中

测试结构体如下。

typedef struct
{
        char a;
        int b;
        short c;
}test1;

那么如果内存中会依次存放:
1字节的成员a
4字节的成员b
2字节的成员c
测试代码如下:

int main(void)
{
        test1 test;
	printf("test1.a = %p \n",&test.a);
	printf("test1.b = %p \n",&test.b);
	printf("test1.c = %p \n",&test.c);
	return 0;
}

测试结果截图:
运行结果
运行结果如上图所示。
a的地址60FEF4
b的地址60FEF8
c的地址60FEFC
成员a,b,c的地址依次递增,显然可以看出它们是按照结构体中的顺序进行排列的。

另外也可以用《C和指针》书中提到的offsetof宏来测试证明。
offsetof( type , member )
offsetof宏接受两个参数,分别是type(结构体类型名)以及member(成员名)。使用这个宏需要使用stddef库
该宏的输出是一个size_t类型的值(特殊的无符号整形),代表成员相对于结构体起始地址的偏移字节量
测试代码:

#include <stddef.h>
int main(void)
{
	printf("offsetof a = %d \n",offsetof(test1,a));
	printf("offsetof b = %d \n",offsetof(test1,b));
	printf("offsetof c = %d \n",offsetof(test1,c));
	return 0;
}

测试结果截图:
在这里插入图片描述
成员a距结构体起始地址偏移字节量为0
成员b距结构体起始地址偏移字节值为4
成员c距结构体起始地址偏移字节值为8
结论不变,内存中先存放成员a,再存放成员b,最后存放成员c

2.结构体的大小

>typedef struct
{
        char a;
        int b;
        short c;
}test1;

对于上面这个结构体来说,char类型1个字节,int类型占用4个字节,short类型占用2个字节。
那么总共应该占用1+2+4 = 7个字节。
测试代码:
printf("sizeof test1 = %d \n",sizeof(test1));

实际运行结果:
在这里插入图片描述

通过打印sizeof输出test1的大小,发现实际运行得到的test1的大小为12,并不是想当然的7

造成这一现象的原因有两点:
1.结构体内部的成员不是连续紧挨着存放的。
2.结构体的大小是成员类型中最大类型的整数倍。

先讲第一点。

1.结构体内部的成员不是连续紧挨着存放的。

再次用到前面测试test1成员内存地址的结果截图。
在这里插入图片描述
从上图可以看出,b与a相差4个字节,c与b相差4个字节。

假设结构体成员是连续紧挨着存放。
由于成员b是int类型,占用4个字节,所以c与b相差4个字节说得通。
但是,a是char类型,只占用1个字节。如果成员是连续存放,那么b和a应该只差1个字节。但是实际上两个成员却相差了4个字节。

显然,结构体内的成员连续紧挨着存放的假设站不住脚

那么造成这一现象的原因是什么呢?结构体内的成员是如何存放的呢?

先上结论:每个结构体成员的存放地址都等于结构体起始地址加上自身类型长度的整数倍。
换成公式:成员的地址 = 结构体起始地址 + 自身类型长度 * N (N为某个整数)**

这个说法可能不太直观,但是举个例子就很好理解了。
还是以结构体test1为例。
测试代码:

typedef struct
{
        char a;
        int b;
        short c;
}test1;
int main(void)
{
    test1 test;
	printf("address test1   = %p \n",&test);
    printf("address test1.a = %p \n",&test.a);
	printf("address test1.b = %p \n",&test.b);
	printf("address test1.c = %p \n",&test.c);
	return 0;
}

运行结果:
在这里插入图片描述
结构体test1的起始地址60FEF4

成员a为char类型,char长度为1个字符,则a的地址 = 60FEF4 + 1*0 。即起始位置加上char长度的0倍,N等于0(结构体的首成员地址和结构体一致,算特殊情况。)

成员b为int类型,int长度为4个字符,则b的地址 = 60FEF4 + 4*1 。即起始位置加上int长度的1倍,N等于1。

成员c为short类型,short长度为2个字符,60FEF4 = 60FEF4+4*2。即起始位置加上short长度的4倍,N等于4。
可以看出每个成员的地址都满足: 结构体起始地址 + 自身类型长度 * N (N为某个整数)。

好了,接下来的问题就是如何确定N了。
其实这个问题答案很简单。
在满足顺序存储的条件下,N尽可能小
或者说,在满足顺序存储的条件下,两个成员的距离尽可能小
1.由于a是结构体中的第一个成员,它前面没有成员,所以它可以直接存在结构体的起始位置,也就是N等于0(最小值).由于char只占1个字符,所以成员a只占用1个字符。
此时内存的存放情况:

(结构体起始地址)60FEF4(成员a)60FEF5(空)60FEF6(空)……总长度12字节

2.对于b而言,N最小也可以取到0。但是存在一个问题,当N等于0时,b的地址 = 60FEF4(即结构体起始地址)。但是这个地址已经被a占了,所以N最小只能取1。此时b的地址 = 60FEF8 。由于int长度4个字节,所以成员b要占用连续的4个字节。
此时内存的存放情况:

(结构体起始地址)60FEF4(成员a)60FEF5(空)60FEF6(空)60FEF7(空)60FEF8(成员b)60FEF9(成员b)60FEFA(成员b)60FEFB(成员b)60FEFC(空)……总长度12个字节

3.对于成员c而言,它是short类型,长度为2个字节,所以它需要以2字节为长度单位进行存放。同时由于顺序存储,它必须放在b后面,所以它最好的情况下也只能放在地址60FEFC中(紧挨在成员b后面),这个位置相对于结构体起始地址的距离恰好等于2的4倍,所以满足存放条件。成员c为short类型,占用2个字节的内存。
此时内存的存放情况:

(结构体起始地址)60FEF4(成员a)60FEF5(空)60FEF6(空)60FEF7(空)60FEF8(成员b)60FEF9(成员b)60FEFA(成员b)60FEFB(成员b)60FEFC(成员c)60FEFD(成员c)60FEFE(空)60FEFF(空)总长度12个字节

可以看出,结构体内的成员是在满足自身存放规则下的情况下,尽可能近的存放在一起。

回顾一下,之前测试的结构体test1的实际大小为12,所有成员大小为7,也就是说浪费了5个字节。

从上面推演的内存图来看,3个空的字节在a和b之间,c到结构体末尾空了2个字节。那么这5个没有利用的字节岂不是被白白浪费了吗?

是的,事实就是这5个字节的内存被浪费了!

那么有办法改善这一问题吗?

有!

看看下面这个例子

typedef struct
{
        char a;
        int b;
        short c;
}test1;
typedef struct
{
        char a;
        short c;
        int b;
}test2;

以上的代码声明了两个成员类型完全一致的结构体。这两个结构体唯一的不同就是交换了int类型成员和short类型成员在结构体内的位置
接下来是测试代码。

int main(void)
{
	printf("sizeof test1 = %d \n",sizeof(test1));
	printf("sizeof test2 = %d \n",sizeof(test2));
	return 0;
}

测试结果截图:
在这里插入图片描述
可以看到,test2相比test1仅仅只是把char类型的成员c和int类型的成员b交换了一下位置,占用的空间就变小了。

原因也很简单,之前a和b之前浪费了3个字节,而成员c为short类型,长度两个字节,成员c将3个被浪费的字节中的2个字节利用了起来。(short类型每隔两个字节就可以存放)。

(这里可能有人会有疑问,明明只减少了2个字节的浪费,为什么实际的空间占用大小却少了4个字节?这一点后面再讲。)

此时再次运行代码查看各成员变量的地址。
在这里插入图片描述
可以看到成员c被放在了距离结构体初始地址2个字节的60FEFA上。
此时的结构体内存图如下:
(结构体起始地址)60FEF4(成员a)60FEF5(空)60FEF6(成员c)60FEF7(成员c)60FEF8(成员b)60FEF9(成员b)60FEFA(成员b)60FEFB(成员b)总长度为8

再额外测试一组用例来加深感受。
测试代码

typedef struct
{
        char a;
        int b;
        char c;
        int d;
}test3;
typedef struct
{
        char a;
        char c;
        int b;
        int d;
}test4;
int main(void)
{

	printf("sizeof test3 = %d \n",sizeof(test3));
	printf("sizeof test4 = %d   ",sizeof(test4));
	return 0;
}

运行结果:
在这里插入图片描述
test3结构体各成员地址
在这里插入图片描述
test3内存图
(结构体起始地址)60FEF0(成员a)60FEF1(空)60FEF2(空)60FEF3(空)60FEF4(成员b)60FEF5(成员b)60FEF6(成员b)60FEF7(成员b)60FEF8(成员c)60FEF9(空)60FEFA(空)60FEFB(空)60FEFC(成员d)60FEFD(成员d)60FEFE(成员d)60FEFF(成员d)总长度16字节

test4结构体各成员地址
在这里插入图片描述
test4内存图
(结构体起始地址)60FEF4(成员a)60FEF5(成员c)60FEF6(空)60FEF7(空)60FEF8(成员b)60FEF9(成员b)60FEFA(成员b)60FEFB(成员b)60FEFC(成员d)60FEFD(成员d)60FEFE(成员d)60FEFF(成员d)总长度为12字节

对于test3来说,成员a和成员b之间浪费了3个字节,成员c和成员d之间浪费了3个字节。
对于test4来说,成员c移到成员b之前,将成员a和成员b之间浪费的1个字节利用起来。而成员d也因此可以挨着成员b存放。节约了4个字节。

上面的两个例子,我们可以发现,之所以改变成员变量的位置能使空间占用变小,都是因为有成员见缝插针的利用内存碎片,或者说多个数据类型较小的成员化零为整的利用整片内存空间

从上面的例子中,我们不难发现,想要减少结构体的空间占用,那么最好把数据类型相同的成员或者数据类型大小接近的成员放在一起,尽量避免不同大小的数据类型交错排列。这样便可以起到 节约空间的作用。

其实,关于这一点,《C与指针》一书已经给出了明确的指导性建议。
这也是本篇文章最具实际意义的一句建议。
那就是声明结构体时,针对结构体里出现的所有成员变量,根据它们的类型大小进行降序声明。(其实升序应该也可以)
对于上面的两个例子,就需要这样声明。

//修改后的结构体声明
typedef struct
{
        int b;//int4个字节最长,放第一位
        short c;//short2个字节,放第二位
        char a;//char1个字节,放最后一位
}test1;
typedef struct
{
        int b;//原理同上
        int d;
        char c;
        char a;
}test3;

修改后的test1内存图如下:
(结构体起始地址)成员b成员b成员b成员b成员c成员c成员a 共8个字节长度

修改后的test3内存图如下:
(结构体起始地址)成员b成员b成员b成员b成员d成员d成员d成员d 成员c成员a 共12个字节长度

从上面的两个内存图可以看出,这种排列方式可以最大化的降低结构体占用空间(尽管还是会有空间被浪费)。

好了,其实到这里就已经可以关掉本篇文章了,因为后面的知识并不会对实际编程有任何影响。

但是如果你想弄清楚结构体的最后一个问题——结构体的总大小是如何确定的?(这个问题的答案也可以解释为什么优化了存放顺序后,结构体还是有空间浪费?)如果你想弄清楚这些问题,那还是建议你看完。

2.结构体的大小是成员类型中最大类型的整数倍

typedef struct
{
        char a;
        int b;
        short c;
}test1;
typedef struct
{
        char a;
        short c;
        int b;
}test2;

还是使用这两个结构体进行说明
在这里插入图片描述
两个结构体的大小分别为12和8,显而易见,4是8和12的公约数。同时4也是int类型的长度。更重要的一点,int是这两个结构体的类型长度最大的类型了。

所以
结构体的大小 = 最大成员类型 * N(N由存放顺序,以及成员总大小确定)

对于test1和test2来说,3个成员总共7个字节(1+2+4),所以N最小也只能取到2,此时结构体占用空间最小为2*4 = 8 个字节。所以对于test1和test2两个结构体来说,无论如何改变成员的位置,都会有一个空间被浪费。

另外我们也可以换一个角度来看待这一点。
即,结构体可以分成N份,每份的大小是最大成员类型的长度

对于test1来说,结构体被分为3份,N=3
第一份成员a
第二份成员b成员b成员b成员b
第三分成员c成员c

对于test2来说,结构体被分为2份
第一份成员a成员c成员c
第二份成员b成员b成员b成员b

所以说,对于这两个结构体来说,每超过一个int长度(4个字节),那么系统就会再次分配至少4个字节给结构体。

最后举几个例子加深印象。

typedef struct
{
        char a;
        short b;
        char c;
}ex1;
typedef struct
{
        short b;
        char a;
        char c;
}ex2;
int main(void)
{
	printf("ex1 size = %d\n",sizeof(ex1));
    printf("ex2 size = %d\n",sizeof(ex2));	
	return 0;
}

在这里插入图片描述
short最长,2个字节。

typedef struct
{
        char a;
        short b;
        int c;
        double d;
        char e;
}ex3;
typedef struct
{
        double d;
        int c;
        short b;
        char a;
        char e;
}ex4;
int main(void)
{
	printf("ex3 size = %d\n",sizeof(ex3));
    printf("ex4 size = %d\n",sizeof(ex4));
	return 0;
}

在这里插入图片描述
double最长,8个字节。

最后聊完了结果和现象,再回过头,简单说一说原因。
其实
结构体内部的成员不是连续紧挨着存放的
每个结构体成员的存放地址都等于结构体起始地址加上自身类型长度的整数倍
以及
结构体的大小是成员类型中最大类型的整数倍
导致这三个现象的原因就是所谓的内存对齐
目的为了加快寻址的处理速度。这里就不具体展开了。有兴趣的可以自行搜索。

3.总结

1.结构体内的成员按照成员变量的声明顺序,依次存放在内存中。
2.结构体的大小是成员类型中最大类型的整数倍。
3.每个结构体成员的存放地址都等于结构体起始地址加上自身类型长度的整数倍

4.最重要的一点(具有实际意义):声明结构体时,针对结构体里出现的所有成员变量,根据它们的类型大小进行降序声明!!!!!!(适用于嵌入式这类对内存空间比较敏感的行业)

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值