C 语言自定义类型详解:结构体

之前的学习中,我们已经接触了 C 语言中最基础的自定义类型 ------ 结构体。这篇文章将带你深入理解 C 语言中其他自定义类型,并且深入剖析它们在内存中的存储原理。

一、结构体

1.1 什么是结构体

结构体(Structure)是一组不同类型数据的集合 ,这些数据被称为结构体的成员变量(Member Variables)。

与数组不同,结构体可以把不同类型的数据封装到一起,这样就可以用结构体描述一个完整的对象。

举个例子来看:当我们要描述一个学生的时候,他的信息包含姓名(字符串)、身高(浮点数或整数)、年龄(整型)、成绩(浮点数),这些数据类型各不相同,我们就可以用结构体进行封装。

1.2 结构体类型声明

结构体类型的声明如下:

cs 复制代码
struct 结构体名
{
    成员类型1 成员名1;
    成员类型2 成员名2;
    // ... 更多成员
}变量名1, 变量名2, 变量名3;

注意:结构体声明后必须加分号,表示结构体声明结束!

举个例子看一下:

1.3 结构体变量的定义与初始化

1.3.1 结构体变量的定义
(1)先声明类型 再定义变量
(2)声明类型 同时定义变量

注意:s1,stu10定义在结构体声明的末尾是全局变量

(3)匿名结构体定义

见名知意,匿名结构体就是没有名字的结构体。

语法格式:

cs 复制代码
struct
{
    成员类型1 成员名1;
    成员类型2 成员名2;
    // ...
} 变量名1, 变量名2;

注意:匿名结构体没有"名字",编译器没办法通过名字识别它,所以它只能在声明的同时定义变量。这也就说明,此结构体类型只能使用一次。(使用时一定注意!)

到这里,我想问屏幕前的你们一个问题:匿名结构体不能通过名字被找到,那我们是不是可以将它的地址传给一个指针变量,通过这个指针变量来访问它?

答案:匿名结构体可以在声明的同时定义指针变量,通过地址访问成员,但无法在声明结束后,再定义新的指针或变量来引用它。

举个例子说明:

1.3.2 结构体变量的初始化
(1)成员顺序初始化

按照结构体成员的顺序赋值

(2)指定成员初始化(乱序初始化)

1.4 结构体成员访问

访问结构体成员必须使用.操作符

如果是指针 则用->操作符

1.5 结构体的自引用

1.5.1 什么是结构体的自引用

结构体里包含一个指向自身类型的指针,它是链表、树等数据结构的核心基础。

这里引入一个知识点给大家讲解:

数据结构:数据结构是指数据在内存中的存储结构,例如:线性表(顺序表、链表)

(1)如果我们想存储12345这五个数字,首先我们想到要建立一个数组来存放这五个数据,这五个数据在内存中是连续存放的,这就称为顺序表。

(2)如果这五个数据在内存中随机存放我们要如何找到它们?

C语言中要如何描述这个节点?这就用到了结构体的自引用

语法格式:

cs 复制代码
struct Node
{
	int data;  //数据域
	struct Node* next;  //指针域
};

注意:不能直接包含结构体自身变量(struct Node next;),这样程序并不知道到底有多少个变量,程序将会陷入死循环。

1.5.2 结构体自引用的使用
cs 复制代码
#include <stdio.h>

// 定义一个结构体,里面包含一个指向自身类型的指针
struct Node
{
    // 数据域:存放数据
    int data;
    // 指针域:存下一个节点的地址 (结构体的自引用)
    struct Node* next;
};

int main()
{
    // 1. 创建 3 个节点 
    struct Node n1, n2, n3;

    // 2. 给每个节点的数据域赋值
    n1.data = 1;
    n2.data = 2;
    n3.data = 3;

    // 3. 用 next 指针把节点"串起来" 
    n1.next = &n2;  // n1 的下一个是 n2
    n2.next = &n3;  // n2 的下一个是 n3
    n3.next = NULL; // 最后一个节点没有下一个,指向空

    // 4. 遍历链表:从头节点开始,一个一个往后找
    struct Node* p = &n1; // p 先指向第一个节点
    while (p != NULL)
    {
        // 打印当前节点的数据
        printf("节点数据: %d\n", p->data);
        // 移动到下一个节点
        p = p->next;
    }

    return 0;
}
1.5.3 typedef修饰结构体(类型重命名)

typedef用于对已定义的类型进行重命名,目的是简化代码,尤其适用于自定义类型。

语法格式:

cs 复制代码
typedef struct Node
{
    int data;                           
    struct Node *next;
} Node;

int main()
{
  
    Node n;         // 直接用Node声明变量(=struct Node)
    return 0;
}

注意!!!!!错误示例!!!!!

二、结构体的大小------内存对齐

在C语言中结构体大小时以内存对齐方式进行计算

2.1 内存对齐规则

(1)结构体的第一个成员要对齐到结构体变量起始位置位零的地址处

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

编译器取对齐数为默认对齐数与该成员变量的较小值(对齐数取决于编译器,例如VS中默认对齐数为8)。

(3)结构体总大小为最大对成员的整数倍

举例说明:

2.2 修改默认对齐数

#pragrom pack 这个预处理指令可以改变编译器的默认对齐数

语法格式:

cs 复制代码
// 设置默认对齐数为 n(n 通常取2的次方数(2\4\6\8) 等
#pragma pack(n)

// 恢复编译器默认对齐数
#pragma pack()

使用举例:

三、结构体传参

3.1 传值调用

3.2 传址调用

四、结构体实现位段

4.1 什么是位段

位段 是 一种按 "位(bit)" 分配内存 的特殊结构体,用来极度节省空间

普通结构体是按字节 分配内存,位段是按二进制位分配内存。

C语言中为什么使用位段

位段与结构体相似

  • 类型限制 :只能用 int / unsigned int / signed int / char 等整型(不能用 float、double)
  • 冒号 + 数字 :数字表示该成员占用多少个二进制位
  • 内存分配 :按分配,不是按字节,极大节省空间
  • 内存对齐 :位段会按存储单元(int 4 字节、char 1 字节)打包,不够存就新开单元

语法格式:

cs 复制代码
struct 位段名
 {
    类型 成员名 : 占用的位数;
};

4.2 位段的大小

位段大小的计算

(1)位段按bit位来分配内存,根据位段成员的类型开辟空间( int(32bits)/ char (8bits) )

(2)将变量按位存储,不够时再开辟新的成员的类型空间

举例讲解:

换个例子来看:

用上面的讲解方法

复制代码
struct Test3 {
    int a:31;
    int b:1;
    int c:1;
};
  • 顺序填充/反向分配:a(31bit)+b(1bit) 刚好填满第一个intc放第二个int → 总大小 8 字节
  • 整块跳过:a用了 31bit,剩 1bit,b直接新开单元,c再新开单元 → 总大小变成 3×4=12字节

4.3 位段的注意事项

(1)由位段的大小讲解可知,位段的具体分配细节是由编译器决定的,C 标准没有强制规定,所 以跨平台使用时要注意兼容性。

(2)

  • int a:32
  • char a:32 ❌ (char 只有 8bit)

(3)位段是按二进制位来分配空间的(内存中每个字节有独立的地址,比特位没有),所以对位段不能用&取地址符,不能用scanf直接给位段输入值。

正确输入方法:先把输入值读到一个临时变量中,再通过赋值操作把值赋给位段成员