【C语言】自定义类型 —— 结构体2

简介: 【C语言】自定义类型 —— 结构体

8 结构体内存对齐


8.1 问题引入


到这里,结构体的基础知识我们基本了解了。


但是结构体的大小如何计算?这我们就不得而知了,看一个样例:

struct S1
{
  char c1;//1
  int i;//4
  char c2;//1
};
struct S2
{
  char c1;
  char c2;
  int i;
};
int main()
{
  struct S1 s1;
  struct S2 s2;
  printf("%d\n", sizeof(s1));//12
  printf("%d\n", sizeof(s2));//8
  return 0;
}

按照我们平时的想法,这两个结构体成员相同,那么就是1+4+1=6了吗?让我们运行一下:


4d92d2fb59b8a7e4553bdbe220c9dbfe.png


8.2 offsetof


我们发现结果和我们的想法截然不同,这是为什么?


在解答之前我们先了解两部分,先介绍第一部分:offsetof

size_t offsetof( structName, memberName );


  • structName:结构体类型的名称
  • memberName:结构体成员名

计算结构体成员相对于起始位置的偏移量

让我们先计算一下S1每个成员的偏移量:

#include <stddef.h>//所需头文件
struct S1
{
  char c1;
  int i;
  char c2;
};
int main()
{
  printf("%u\n", offsetof(struct S1, c1));
  printf("%u\n", offsetof(struct S1, i));
  printf("%u\n", offsetof(struct S1, c2));
  return 0;
}


运行结果:

6ea5cf4be4ecf1e2ee0bc98a3de8268a.png

根据这个偏移量,我们假设一个位置为起始位置,画出它的内存分布图

49513fe40e53a0ea65aaa7865dbd6053.png


而其中1~3的内存单位是被浪费的,且根据大小为12。9,10,11三个位置也是被浪费的。这是什么原因?看下一部分↓



8.3 结构体的内存对齐


要说这里的原理,就要讲讲结构体的内存对齐



       第一个成员在与结构体变量偏移量为0的地址处。

       其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

       对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

       VS中默认的值为8

       结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。


       如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。


通过这些规则,我们再重新看一下S1:


分析:


   c1为结构体第一个成员,在结构体变量偏移量为0的地址处,占用1个字节。

   i的大小为4,默认对齐数为8,取其较小值为4。对齐到4的倍数处,也就是偏移为4的位置,占用4个字节,1,2,3三个字节被浪费。

   c2的大小为1,默认对齐数为8,取其较小值对为1。对齐到1的倍数处,也就是偏移为8的位置,占用1个字节。

   结构体的总大小为最大对其数的整数倍。c1,i,c2的对齐数分别为1,4,1。结构体大小为4的倍数,当前结构体所占空间大小为9字节,要为4的倍数,则大小为1字节2,9,10,11三个字节被浪费。

这样就解释了为什么S1的大小为什么是12!我们接着看S2:


struct S1
{
  char c1;
  char c2;
    int i;
};



分析:


   c1为结构体第一个成员,在结构体变量偏移量为0的地址处,占用一个字节。

   c2的大小为1,默认对齐数为8,取其较小值为1。对齐到1的倍数处,也就是偏移为1的位置,占用1个字节。

   i的大小为4,默认对齐数为8,取其较小值为4。对齐到4的倍数处,也就是偏移为4的位置,占用4个字节。2,3两个字节被浪费。

   结构体的总大小为最大对其数的整数倍。c1,c2,i的对齐数分别为1,1,4。结构体大小为4的倍数,直接为当前结构体所占空间大小:8字节。


42ca030c773d9deadb0e5bf2c10b2612.png

8.4 小试牛刀


自己试着计算两个结构体的大小并描述内存分布和画出内存分布图:


题1:

struct S3
{
  double d;
  char c;
  int i;
};


分析:

   d为结构体第一个成员,在结构体变量偏移量为0的地址处,占用8个字节。

   c的大小为1,默认对齐数为8,取其较小值为1。对齐到1的倍数处,也就是偏移为8的位置,占用1个字节。

   i的大小为4,默认对齐数为8,取其较小值为4。对齐到4的倍数处,也就是偏移为12的位置,占用4个字节。9,10,11三个字节被浪费。

   结构体的总大小为最大对其数的整数倍。d,c,i的对齐数分别为8,1,4。结构体大小为8的倍数,当前结构体当前所占空间大小为16字节,为当前大小。


18b243a3243ac6738a37ebfa1233a22a.png

运行结果:

eba2aa5846bef9eae2cabad57ef05494.png

题2:

struct S4
{
    char c1;
    struct S3 s3;
    double d;
}//结构体嵌套情况下,结果是多少?


分析:


结构体嵌套结构体,这时就要用到我们的第四条规则:


   如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

   c1为结构体第一个成员,在结构体变量偏移量为0的地址处,占用1个字节。

   s3为结构体第二个成员,为嵌套的结构体,对齐到自己的最大对齐数的整数倍处,s3的最大对齐数我们在上面算过,为8,那么就对齐到8字节处,上面1~7字节被浪费。s3占用16个字节。

   d为结构体第三个成员,默认对齐数为8,自身大小为8,所以对齐到8的倍数处,对齐到24字节处,占用8个字节。

   结构体总大小为所有最大对其数的整数倍处。c1,s3,d最大对齐数为1,8,8。对齐到8的倍数处,结构体当前所占空间大小为32字节,为8的倍数,所以结构体大小为32字节。

4f0dc243ddc54d35252d1c1f004fd448.png

运行结果:

66e548b1d45e83a4279f21f82a925d2d.png


8.5 为什么存在内存对齐?


   平台原因(移植原因):


   不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。


   性能原因:


   数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。


下面对第二条原因做出一定解释:


1f539dd7b8340b656f4e38696fe08c3a.png

假设一次读取4个字节的数据,要读取到内存中的i。


在不考虑内存对齐的情况下,需要读取两次,从c开始读,第一次读取i的三个字节,第二次读取剩余的一个字节。


而在考虑内存对齐的情况下,需要读取一次,直接从i开始读,读取i的四个字节。


总体来说:


   结构体的内存对齐是拿空间换取时间的做法。



8.6 设计结构体的细节


如何在设计结构体时,既满足对齐,又要节省空间?


让占用空间小的成员尽量集中在一起。

这样,浪费的字节也就少了。并且,当成员集中到一定程度时,说不定就正好放置到下一个元素的对齐位置上方,让空间最大程度上得到利用。


例如:

struct S1
{
    char c1;
    int i;
    char c2;
};
//更好
struct S2
{
    char c1;
    char c2;
    int i;
};



S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别,S2大小比S1小。因为S2把占用空间小的成员集中在一起。



8.7 如何修改默认对齐数


之前我们见过了#pragma这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。


#pragma pack(4)//设置默认对齐数为4
#pragma pack()//恢复默认对齐数

我们不妨设想一下,如果将默认对齐数设置为1,结构体的大小会是多少:

#pragma pack(1)
struct S1
{
  char c1;//从0开始对齐
  int i;//4 1 对齐数为1,对齐到1位置处
  char c2;//1 1 对齐数为1,从5开始对齐
  //最大对齐数为1,所以结构体大小为1的倍数即可
  //6,其实也就是没对齐
};
#pragma pack()
int main()
{
  printf("%d\n", sizeof(struct S1));//6
  return 0;
}

   c1为结构体第一个成员,在结构体变量偏移量为0的地址处,占用1个字节。

   i的大小为4,默认对齐数为1,取其较小值为1。对齐到1的倍数处,也就是偏移为1的位置,占用4个字节。

   c2的大小为1,默认对齐数为1,取其较小值为1。对齐到1的倍数处,也就是偏移为5的位置,占用1个字节。

   结构体的总大小为最大对其数的整数倍。c1,i,c2的对齐数分别为1,1,1。结构体大小为1的倍数,所以不需要调整结构体的大小,直接为当前大小,为6个字节。


相当于对齐了个寂寞~


运行结果:


811d6d88f50d745d6fbb80691e947676.png



但是需要注意的是:


虽然支持这样修改默认对齐数,但是也不要胡乱修改,一般默认对齐数修改为2^n,机器在读取时,读取的字长为4/8个字节,尽量朝着适合读写的方法来设定。但是当结构体在对齐方式上不合适的时候,我们可以自己更改默认对齐数。




9. 结构体传参


struct S
{
    int data[1000];
    int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
    printf("%d\n", s.data[0]);//结构体变量.结构体成员访问结构体成员
}
//结构体地址传参
void print2(struct S* ps)
{
    printf("%d\n", (*ps).data[0]);//*ps访问到结构体,结构体变量.操作符访问成员
    printf("%d\n", ps->data[0]);//结构体指针->结构体成员访问成员
}
int main()
{
    struct S ss = { { 1, 2, 3, 4, 5 }, 100 };
    print1(ss); //传结构体
    print2(&ss); //传地址
    return 0;
}

运行结果:

2a89b6dbb9d35ea1064488fabcab544d.png



两个函数的作用是相同的,但是上面的print1和print2函数哪个好?


在结构体成员的访问部分中,我们是通过print函数对结构体成员进行访问并打印的,而这两种传参方式截然不同,一个为结构体变量ss(传值调用),一个为结构体变量的地址&ss(传址调用)。


那么这两种传参方式哪个更好呢?当然是第二种方式,传址调用的方式。


可能大家可能会觉得print1比较好,原因是print2可能可以通过结构体指针改变结构体的内容,但是这完全可以避免,只需要对*ps加上const修饰,便可避免这种情况。


认为第二种方法更优的原因还因为:


   结构体传参时,若实参为结构体变量,那么就要创建变量的一份临时拷贝,需要大量的空间,而实参为结构体指针的话,形参的大小为4/8个字节,大大节省了空间。


   而函数传参的时候,参数是需要压栈的。


   如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。


   简单来说若结构体空间过大,在压栈时需要使用大量的空间,不仅浪费了空间,更浪费了时间!

结论:结构体传参时,要传结构体的地址。


注:结构体传值时,实参结构体的地址可能和形参结构体的地址相同,编译器可能不会创建临时空间,自己进行了优化,我们使用的空间依然可能是实参的空间,为了避免这些乱七八糟的优化,我们还是选择传址调用~



10. 结语


到这里,本篇博客到此结束。相信通过这篇博客,大家对结构体也有了一定的认识。而在下篇博客中,我将利用结构体的知识,进行简易通讯录的实现,更多精彩内容,敬请期待!





相关文章
|
13天前
|
存储 C语言
如何在 C 语言中实现结构体的深拷贝
在C语言中实现结构体的深拷贝,需要手动分配内存并逐个复制成员变量,确保新结构体与原结构体完全独立,避免浅拷贝导致的数据共享问题。具体方法包括使用 `malloc` 分配内存和 `memcpy` 或手动赋值。
23 10
|
13天前
|
安全 编译器 Linux
【c语言】轻松拿捏自定义类型
本文介绍了C语言中的三种自定义类型:结构体、联合体和枚举类型。结构体可以包含多个不同类型的成员,支持自引用和内存对齐。联合体的所有成员共享同一块内存,适用于判断机器的大小端。枚举类型用于列举固定值,增加代码的可读性和安全性。文中详细讲解了每种类型的声明、特点和使用方法,并提供了示例代码。
14 3
|
12天前
|
存储 大数据 编译器
C语言:结构体对齐规则
C语言中,结构体对齐规则是指编译器为了提高数据访问效率,会根据成员变量的类型对结构体中的成员进行内存对齐。通常遵循编译器默认的对齐方式或使用特定的对齐指令来优化结构体布局,以减少内存浪费并提升性能。
|
17天前
|
编译器 C语言
共用体和结构体在 C 语言中的优先级是怎样的
在C语言中,共用体(union)和结构体(struct)的优先级相同,它们都是用户自定义的数据类型,用于组合不同类型的数据。但是,共用体中的所有成员共享同一段内存,而结构体中的成员各自占用独立的内存空间。
|
17天前
|
存储 C语言
C语言:结构体与共用体的区别
C语言中,结构体(struct)和共用体(union)都用于组合不同类型的数据,但使用方式不同。结构体为每个成员分配独立的内存空间,而共用体的所有成员共享同一段内存,节省空间但需谨慎使用。
|
22天前
|
编译器 C语言 C++
C语言结构体
C语言结构体
21 5
|
23天前
|
编译器 Linux C语言
C语言 之 结构体超详细总结
C语言 之 结构体超详细总结
13 0
|
27天前
|
存储 编译器 Linux
深入C语言:探索结构体的奥秘
深入C语言:探索结构体的奥秘
|
27天前
|
存储 编译器 C语言
c语言回顾-结构体(2)(下)
c语言回顾-结构体(2)(下)
28 0
|
27天前
|
存储 编译器 程序员
c语言回顾-结构体(2)(上)
c语言回顾-结构体(2)(上)
27 0