《小菜狗 C 语言入门 + 进阶笔记》(37)结构体进阶 1 -- 内存对齐与结构体数组【难度升级】


《小菜狗 C 语言入门 + 进阶笔记》目录:《小菜狗 C 语言入门 + 进阶笔记》(0)简介

1、结构体的大小

C 语言中,我们可以使用 sizeof 运算符来计算结构体的大小。对于结构体,sizeof 将返回结构体的总字节数,包括所有成员变量的大小以及可能的填充字节

以下代码演示了如何计算结构体的大小:

#include <stdio.h>

struct Person {
    char name[20];
    int age;
    float height;
};

int main()
{
    struct Person person;
    printf("结构体 Person 大小为: %zu 字节\n", sizeof(person));
    return 0;
}

以上实例步骤:

(1) 我们定义了一个名为 Person 的结构体,它包含了一个字符数组 name、一个整数 age 和一个浮点数 height

(2) 在 main 函数中,我们声明了一个 Person 类型的变量 person,然后使用 sizeof 运算符来获取 person 结构体的大小。

(3) 最后,我们使用 printf 函数打印出结构体的大小(20+4+4 = 28)。

输出结果如下:

结构体 Person 大小为: 28 字节

注意:结构体的大小可能会受到编译器的优化对齐规则的影响,编译器可能会在结构体中插入一些额外的填充字节以对齐结构体的成员变量,以提高内存访问效率

因此,结构体的实际大小可能会大于成员变量大小的总和,如果需要确切地了解结构体的内存布局和对齐方式,可以使用 offsetof 宏attribute((packed)) 属性等进一步控制和查询结构体的大小和对齐方式。

请继续往下看我分析!

2、结构体内存对齐

2.1、对齐规则

⾸先得掌握结构体的对齐规则:

(1) 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为 0 的地址处。

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

对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值

  • VS 中默认的值为 8;
  • Linux 中 gcc 没有默认对齐数,对齐数就是成员自身的大小;

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

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

2.2、对齐经典实例
//练习1
struct S1
{
    char c1;
    int i;
    char c2;
};

//练习2
struct S2
{
    char c1;
    char c2;
    int i;
};

//练习3
struct S3
{
    double d;
    char c;
    int i;
};

//练习4-结构体嵌套问题
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};

int main()
{
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    printf("%d\n", sizeof(struct S3));
    printf("%d\n", sizeof(struct S4));
    return 0;
}

输出结果:

12
8
16
32

详细分析(重点):

  • S1 存储空间分布为 c1(1byte) + 空闲(3byte) + i(4byte) + c2(1byte) + 空闲(3byte) = 12(byte)。
  • S2 存储空间分布为 c1(1byte) + c2(1byte) + 空闲(2byte) + i(4byte) = 8(byte)。
  • S3 存储空间分布为 d(8byte) + c(1byte) + 空闲(3byte) + i(4byte) = 16(byte)。
  • S4 存储空间分布为 c1(1byte) + 空闲(7byte) + s3(16byte) + d(8byte) = 32(byte)。

结构体内存对齐的规则很简单:

​ 一、成员变量内存相对起始位置为数据类型所占内存的整数倍(例如:int 类型数据相对起始位置必须是结构体中 4 字节的整数倍),若不足则需要对齐不足部分的内存(内存补充给前一个变量)。

​ 二、结构体所占总内存为其成员变量中所占空间最大数据类型的整数倍。

​ 三、结构体中每个成员相对于结构体起始地址的偏移量必须是该成员大小的倍数。

其中,最宽基本类型指的是 long double、double 和 long long 中占用空间最大的类型。如果结构体中没有这些类型的成员,则以 int 或者 char 作为最宽基本类型。

以下是一个示例,展示了一个结构体的内存对齐过程:

struct example {
    char a;       // 1 byte
    int b;        // 4 bytes
    double c;     // 8 bytes
    short d;      // 2 bytes
};

根据内存对齐规则,该结构体中的成员变量将按照以下方式进行排列:

|------------------------|------------------------|
|  char a                |  padding (3 bytes)     |
|------------------------|------------------------|
|  int b                 |                        |
|------------------------|------------------------|
|  double c              |                        |
|------------------------|------------------------|
|  short d               |  padding (6 bytes)     |
|------------------------|------------------------|

由于 int 和 double 的大小都是 4 的倍数和 8 的倍数,因此它们的偏移量和结构体总大小都可以被 4 和 8 整除,而 short 的大小为 2,因此需要填充 6 个字节以满足偏移量为 8 的要求。

2.3、为什么存在内存对齐?
  1. 平台原因 (移植原因):

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

  1. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取 8 个字节,则地址必须是 8 的倍数。如果我们能保证将所有的 double 类型的数据的地址都对齐成 8 的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个 8 字节内存块中。

总体来说:

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

结构体设计的建议:

那在设计结构体的时候,我们既要满足对齐,⼜要节省空间,如何做到让占用空间小的成员尽量集中在⼀起。

如下你们觉得用哪一种定义会更好?

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

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

S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的大小有了⼀些区别。

从前面可知,那肯定是选 S2 的定义。

2.4、修改默认对齐数

#pragma 预处理指令可以改变编译器的默认对齐数

#include <stdio.h>
#pragma pack(1) //设置默认对齐数为1
struct S
{
    char c1;
    int i;
    char c2;
};

#pragma pack() //取消设置的对齐数,还原为默认
int main()
{
    printf("%d\n", sizeof(struct S));
    return 0;
}

上面的代码,输出的结果是什么?

猜测一下,是 12?是 6?是 8?自己玩玩看!

结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。

3、结构体作为函数参数

可以把结构体作为函数参数,传参方式与其他类型的变量类似。

如下代码:

#include <stdio.h>
#include <string.h>
 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};

/* 函数声明 */
void printBook( struct Books book );
int main()
{
   struct Books Book1;        /* 声明 Book1,类型为 Books */
   struct Books Book2;        /* 声明 Book2,类型为 Books */
 
   /* Book1 详述 */
   strcpy( Book1.title, "I");
   strcpy( Book1.author, "love"); 
   strcpy( Book1.subject, "BOBO");
   Book1.book_id = 666;

   /* Book2 详述 */
   strcpy( Book2.title, "You");
   strcpy( Book2.author, "love");
   strcpy( Book2.subject, "learn");
   Book2.book_id = 777;
 
   /* 输出 Book1 信息 */
   printBook( Book1 );

   /* 输出 Book2 信息 */
   printBook( Book2 );

   return 0;
}
void printBook( struct Books book )
{
   printf( "Book title : %s\n", book.title);
   printf( "Book author : %s\n", book.author);
   printf( "Book subject : %s\n", book.subject);
   printf( "Book book_id : %d\n", book.book_id);
}

输出结果:

Book title : I
Book author : love
Book subject : BOBO
Book book_id : 666
Book title : You
Book author : love
Book subject : learn
Book book_id : 777

4、结构体数组

一个结构体变量中可以存放一组数据(如一个学生的学号,姓名,成绩等数据)。那如果有 10 个学生呢?

那就需要有 10 个这样的结构体来存储 10 个学生数据,这就是结构体数组

4.1、定义结构体数组

和定义结构体变量的方法相仿,只需说明其为数组即可。

struct student
{
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
};

struct student stu[3];

以上定义了一个数组 stu,其元素为 struct student 类型数据,数组有 3 个元素。

也可以直接定义一个结构体数组。如下代码:

struct student
{
    int num;
    ....

} stu[3];
4.2、结构体数组的初始化

与其它类型数组一样,对结构体数组可以初始化,如下代码:

struct student
{
    int mum;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
} stu[3] = {{10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
            {10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"},
            {10101,"Li Lin", 'M', 18, 87.5, "103 Beijing Road"}};

定义数组 stu 时,元素个数可以不指定,即写成以下形式:

stu[] = {{...},{...},{...}};

编译时,系统会根据给出初值的结构体常量的个数来确定数组元素的个数

当然,数组的初始化也可以用以下形式:

struct student
{
    int num;
    ...
};
struct student stu[] = {{...},{...},{...}};

即先声明结构体类型,然后定义数组为该结构体类型,在定义数组时初始化。

4.3、结构体数组应用举例

下面例子说明结构体数组的定义和引用。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct person
{
    char name[20];
    int count;

} leader[3] = {{"Li", 0},
             {"Zhang", 0},
             {"Fun", 0}};

void main()
{
    int i, j;
    char leader_name[20];
    for(i = 1; i<= 10; i++)
    {
        scanf("%s", leader_name);
        for(j=0; j<3; j++)
            if(strcmp(leader_name, leader[j].name) == 0)
                leader[j].count ++;
    }
    printf("\n");
    for(i=0; i<3; i++)
        printf("%5s: %d\n", leader[i].name, leader[i].count);
    system("pause");
}

运行结果如下:

LI
Li
Fun
Zhang
Zhang
Fun
Li
Fun
Zhang
Li

   Li: 3
Zhang: 3
  Fun: 3

《小菜狗 C 语言入门 + 进阶笔记》目录:《小菜狗 C 语言入门 + 进阶笔记》(0)简介

每日一更!

公众号、CSDN等博客:小菜狗编程笔记

谢谢点赞关注哈!目前在飞书持续优化更新~

日更较慢有需要完整笔记请私我,C/C++/数据结构-算法/单片机51-STM32-GD32-ESP32/嵌入式/Linux操作系统/uboot/Linux内核-驱动-应用/硬件入门-PCB-layout/Python/后期小程序和机器学习!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小鹅编程笔记

你的鼓励将是我最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值