C语言--自定义类型:结构体(超详解)

目录

前言   

结构体(Structures)

结构体的定义

结构体变量的创建和初始化

结构体的内存对齐

对齐规则

定义一:结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处

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

​编辑  定义三:结构体总大小为最大对齐数的整数倍。

如何去修改结构体默认对齐数

为什么会存在内存对齐呢

平台原因 (移植原因):

性能原因:

结语



前言   

        大家好,这里是彩妙呀,今天给大家带来的是 --> 自定义类型的讲解。            

        我们在学习C语言的过程中,或多或少不可避免地会接触到各种数据类型,比如我们熟知的基本数据类型int char 等。然而,当需要处理更复杂的数据或者更大规模的项目时,这些基本数据类型就显得力不从心。幸运的是,C语言为我们提供了几种强大的自定义类型工具:结构体(Structures)、联合体(Unions)和枚举体(Enumerations)。这里,彩妙 将详细介绍这 三种 自定义类型之一:结构体的定义、使用方式及它们的应用场景。

        废话不多说,让我们开始吧!

结构体(Structures)

    定义:结构体 是一种自定义的数据类型,可以用来存储不同类型的数据并组合成一个整体。

    简单的说,结构体就是把 一个 或 多个 的 数据类型 组合在一起的一种 数据类型,相当于 是 ⼀些值 的集合,这些值称为成员变量。结构 的 每个成员 可以是 不同类型的变量,包括基本类型(如int、float)、指针类型和其他结构体类型等。

   那我们怎么去定义一个结构体呢?

结构体的定义

struct name    // 结构体类型的名字
{
 value-list;   // 结构体内部成员
}name-list;    // 该结构体的名字

这里用学生的例子来给大家在进一步说明:

struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
}; //分号不能丢

在这里,struct Stu  的作用与咱们熟知的基本类型(int , char)作用一样,起到声明作用。假如,我要去声明一个整形 a ,就要写:int a;同样的,我要去声明一个我定义好的结构体 a,就要这么写:struct Stu a。

对于结构体内部成员 ,我们可以用 “ . ” 或者  " -> "  来去引用相对应的成员。例如,我已经定义了一个结构体成员 a,要去修改恰其中 name[20] 这个成员的内容,就可以用 a.name = "张三" 或者 a-> name = "张三" 来进行修改

在这里,你也会发现,没有 name-list 这部分,原因是 name-list 这部分相当于在你定义结构体的时候就声明了一些结构体。

知道了各个部分的作用,那我们就要去使用它。

结构体变量的创建和初始化

接上上面学生的例子来说,我们如何去创建与初始化一个结构体呢?

先上代码:

#include <stdio.h>
struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
};
int main()
{
 //按照结构体成员的顺序初始化
 struct Stu s = { "张三", 20, "男", "20240604001" };
 printf("name: %s\n", s.name);
 printf("age : %d\n", s.age);
 printf("sex : %s\n", s.sex);
 printf("id : %s\n", s.id);
 
 //按照指定的顺序初始化
 struct Stu s2 = { .age = 18, .name = "李四", .id = "20240604002", .sex = "⼥"};
 printf("name: %s\n", s2.name);
 printf("age : %d\n", s2.age);
 printf("sex : %s\n", s2.sex);
 printf("id : %s\n", s2.id);
 return 0;
}

 在这段代码中,相信读者可以发现:结构体的初始化有两种不同的方法,一种就是顺序初始化,另一种就是按照你所给的顺序初始化。

顺序初始化:
代码区: struct Stu s = { "张三", 20, "男", "20240604001" };

不难发现,初始化所对应的 各个数据都 一 一对应结构体中成员的类型。

name:张三

age:20

sex:男

id:20240604001

 按照指定的顺序初始化:
代码区:struct Stu s2 = { .age = 18, .name = "李四", .id = "20240604002", .sex = "⼥"}; 

也不难发现,这里的初始化是按照你所给的类型名一一去修改的。

 那为什么会有这种不同的初始方法呢?这就和接下来的结构体的一个规则有关啦。

结构体的内存对齐

在初始化中,难免会有疑问,为什么计算机去初始化数据时要按着这些顺序来存储?在指定的顺序初始化中,为什么可以去精准找到每个成员所储存的地址呢?

这些问题的底层逻辑都离不开结构体自身的规则:内存对齐规则。

对齐规则

定义:

1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对⻬数)的整数倍的地址处。
        对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

这些定义看着就头痛,接下来彩妙带着大家逐步掌握结构体的储存逻辑吧。

定义一:结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
    定义一 如同其字面意思一样,我们去存储结构体的时候,是要向内存去申请一块空间的。位置 偏移量为0 意思就是 :第一个成员存放的位置是开始的位置。

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

对⻬数 = 编译器默认的⼀个对⻬数该成员变量⼤⼩的较⼩值

    对于大多数数据类型,例如intfloatchar等,它们的大小通常是4个字节(32位系统)或8个字节(64位系统)。因此,它们的对齐数也是4或8。

    举个例子:

struct Str{
    int a;
    float b;
    char c;
};

    在32位系统上,int的大小是4,所以a的对齐数为4。float的大小也是4,所以b的对齐数也是4。char只有1个字节,所以c的对齐数为1。因此,结构体中的成员变量都应该对齐到4的整数倍的地址处。

结构体的大小计算如下:

 a的偏移量为0,  大小为4。

    b的偏移量为4, 大小为4。

 c的偏移量为8,  大小为1。


  定义三:结构体总大小为最大对齐数的整数倍。

    在C语言中,结构体的总大小是由其成员的大小和对齐方式决定的。每个成员的对齐方式取决于编译器和平台,但通常会遵循一定的规则,比如成员的对齐数通常是2的幂次。结构体的总大小是其成员大小的总和,但还需要满足一个条件:结构体的总大小必须是其最大成员对齐数整数倍

    举个例子:

#include <stdio.h>

struct Str{
    char a;    // 1字节
    int b;     // 4字节
    short c;   // 2字节
};

int main() {
    printf("结构体的字节数: %zu\n", sizeof(struct Str));
    return 0;
}

    在这个例子中,char a 占用1字节,int b 通常占用4字节,short c 通常占用2字节。如果按照成员的大小累加,结构体的大小应该是1 + 4 + 2 = 7字节。

    但是,由于结构体的总大小需要是其最大成员对齐数(这里是 int 类型的对齐数,通常是4字节)的整数倍,编译器会在结构体的末尾添加填充字节,使得结构体的总大小为 12 字节。

 黄色区域是被占用的字节,灰色区域是为了使内存对齐所弃用的字节

运行结果:

结构体的字节数: 12

如何去修改结构体默认对齐数

  为了确保成员变量都对齐到4的整数倍的地址处,可以使用#pragma pack指令来设置对齐数。例如,可以使用#pragma pack(4)来设置对齐数为4,即:

#pragma pack(4)

struct example {
    int a;
    float b;
    char c;
};

#pragma pack()

这样,编译器会按照对齐数为4的要求分配内存,保证结构体中的成员变量都对齐到4的整数倍的地址处。

注意:VS 中默认的对齐数的值为 8,而Linux中 gcc 没有默认对⻬数,其对⻬数就是成员⾃⾝的⼤⼩

为什么会存在内存对齐呢

参考了⼤部分的资料,结果是这样说的:
平台原因 (移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
性能原因:
    数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。

    有兴趣可以去查阅《深入理解计算机系统》,但总的来说,结构体的内存对齐 是拿空间来换取时间的做法。

结语

    这次为大家介绍了结构体,下次就为大家介绍一下与结构体同兄弟的两个:联合和枚举。我们下次再见

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值