🌊🌊点击进入C语言专栏🌊🌊 🌊🌊如何收集自己的代码仓库🌊🌊
学习应明为何而学、学了何用 在开始今天的课程之前,我们先谈谈为什么需要自定义类型
结构体(Struct)是一种自定义的数据类型,允许将多个不同类型的数据组合在一起形成一个新的复合数据类型结构体(Struct)主要作用于封装和组织,简化复杂数据的管理,提高代码的可读性和维护性,为面向对象编程打下基础
// 基本语法
struct 结构体标签 {
数据类型 成员1;
数据类型 成员2;
// ...
};
// 示例
struct Student {
int id;
char name[50];
float score;
int age;
};// 方法1: 先定义结构体类型,再声明变量
struct Student {
int id;
char name[50];
float score;
};
struct Student stu1, stu2;
// 方法2: 定义结构体类型的同时声明变量
struct Student {
int id;
char name[50];
float score;
} stu1, stu2;
// 方法3: 匿名结构体
struct {
int id;
char name[50];
} temp_stu;#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
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 = "lisi", .id = "20230818002", .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;
}下面详细讲解C语言结构体传参的各种方式,包括值传递、地址传递、返回结构体等
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}上面的print1 和 print2 函数哪个好些? 肯定首选print2
🧩 一、什么是结构体自引用? 结构体里面有一个成员,它的类型又是这个结构体本身。 但要注意! 结构体不能直接包含自己,只能包含指向自己的指针。

这里 next 的类型是 struct Node,
但我们现在正在定义 struct Node 本身。
它还没定义完!编译器就被问:
“请告诉我struct Node的大小是多少。”
编译器就懵了:
“我还在定义它呢,我也不知道多大啊!” 😵
于是编译器报错。
struct Node {
int data;
struct Node *next;
};这里` next 是一个 指针,指针的大小是固定的: 👉 无论
struct Node内容有多复杂,指针的大小编译器都知道。

所以编译器就能顺利计算出整个结构体的大小。
首先来看一个简单的例子,如何直接将结构体作为函数返回值:
#include <stdio.h>
// 定义一个结构体类型 Point,表示二维平面上的点
struct Point {
int x; // x坐标
int y; // y坐标
};
// 函数:创建并返回一个Point结构体
// 参数:x - 点的x坐标,y - 点的y坐标
// 返回值:包含指定坐标的Point结构体
struct Point createPoint(int x, int y) {
struct Point p; // 在函数内部定义一个Point结构体变量
p.x = x; // 设置结构体的x成员值
p.y = y; // 设置结构体的y成员值
return p; // 返回这个结构体(返回的是结构体的副本)
}
int main() {
// 调用createPoint函数创建结构体实例
struct Point p1 = createPoint(10, 20);
// 打印结构体成员的值
printf("Point: (%d, %d)\n", p1.x, p1.y);
return 0;
}解释:
createPoint函数创建并返回一个struct Point类型的变量。
Point的成员x和y被赋值,并作为返回值返回。
main函数中,p1 得到返回的结构体,直接打印其成员。
节省运行效率 如果需要返回一个结构体的指针,应该返回一个 动态分配的结构体(
malloc或者静态变量)。
struct Point* createPoint() {
struct Point* p = malloc(sizeof(struct Point)); // 动态分配内存
p->x = 10;
p->y = 20;
return p; // 返回指向动态分配内存的指针
}💡 返回的指针指向的是堆区内存,而堆区内存不会随着函数的结束而销毁;也可以避免局部变量的生命周期问题
内存对齐是指计算机内存中数据的存储方式,目的是优化内存访问速度。特别是在 C 语言中,结构体内存对齐是一个至关重要的概念。结构体中每个成员变量会根据其数据类型的大小自动进行对齐,以便更高效地存储和访问数据。
我们先看一段示例代码,再来讲规则
//练习1
struct S1 {
char c1; // 1 字节
int i; // 4 字节
char c2; // 1 字节
};
printf("%d\n", sizeof(struct S1));观察它的内存格式
成员 | 类型 | 大小(字节) | 偏移地址 | 占用内存区间 | 填充(字节) |
|---|---|---|---|---|---|
c1 | char | 1 | 0 | [0] | 3 |
i | int | 4 | 4 | [4, 7] | 0 |
c2 | char | 1 | 8 | [8] | 3 |
尾部填充 | - | - | - | - | 3 |
有了上面的例子,我们再来讲解一下结构体对齐规则: 1. 结构体的第1个成员对齐到和结构体变量起始位置偏移量为0的地址处 2. 从第2个成员变量开始,都要对齐到某个对齐数的整数倍的地址处 对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值
VS 中默认的值为 8Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍 4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
这样一看,是不是上面例子更加浅显易懂了?
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
→ 最终结构体大小:16 字节
//结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
→ 最终结构体大小:32 字节
#pragma这个预处理指令,可以改变编译器的默认对齐数
#include <stdio.h>
// 指示编译器按 1 字节对齐(即不再进行自动填充对齐)
// 结构体中的每个成员紧密排列,节省空间但访问速度可能变慢
#pragma pack(1)
// 定义结构体 S
struct S
{
char c1; // 占 1 字节,偏移量 0
int i; // 占 4 字节,按 pack(1) 不再对齐,紧跟在 c1 后,占偏移量 1~4
char c2; // 占 1 字节,偏移量 5
};
// 当前结构体占 6 字节:c1(1) + i(4) + c2(1)
#pragma pack() // 恢复默认对齐方式(通常是 4 或 8)
int main()
{
// 输出结构体 S 的大小
printf("%d\n", sizeof(struct S)); // 输出 6
return 0;
}成员 | 字节范围 | 说明 |
|---|---|---|
c1 | 0 | char,占 1 字节 |
i | 1–4 | int,占 4 字节(紧接在 c1 后,因为 #pragma pack(1) 取消对齐) |
c2 | 5 | char,占 1 字节 |
位段
(Bit-field)是 C 语言结构体中一种特殊成员,用来在一个整型变量的二进制位级上划分存储空间 它允许你精确控制某个字段占用多少个二进制位,从而节省内存
int、unsigned int 或 signed int,在 C99 中位段成员的类型也可以选择其他整型家族类型,比如:charstruct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};那么A所占的内存是多少?
🧠 位段的关键规则(以 GCC / MSVC 常见实现为准) 位段的类型是
int→ 每个存储单元是4字节(32位)。 同类型的位段会共享同一个32位单元,直到放不下为止。 如果剩余位数不足,新的位段会从下一个32位单元重新开始。 结构体整体大小要满足 最大对齐数(这里是4字节) 的倍数。

→ 最终结果:sizeof(struct A) = 8

联合体的所有成员共享同一内存,联合体的大小等于其最大成员大小(通常再加上对齐填充)
#include <stdio.h>
// 联合类型的声明
union Un
{
char c; // 字符类型成员,占用1字节
int i; // 整型成员,占用4字节
};
int main()
{
// 联合变量的定义,并初始化为0
union Un un = {0};
// 计算联合变量的大小
printf("联合体的大小: %d\n", sizeof(un));
return 0;
}所以联合体也叫:共用体
给联合体其中一个成员赋值,其他成员的值也跟着变化。
#include <stdio.h>
// 定义一个联合体,包含一个整数和一个字符
union Data {
int number;
char letter;
};
int main() {
union Data data;
printf("=== 联合体成员值变化演示 ===\n\n");
// 1. 先给number赋值
data.number = 100;
printf("1. 给number赋值100后:\n");
printf(" data.number = %d\n", data.number);
printf(" data.letter = %c (ASCII: %d)\n", data.letter, data.letter);
printf(" (letter的值被number影响了)\n\n");
// 2. 再给letter赋值
data.letter = 'A';
printf("2. 给letter赋值'A'后:\n");
printf(" data.letter = %c\n", data.letter);
printf(" data.number = %d\n", data.number);
printf(" (number的值被letter改变了!)\n\n");
// 3. 再次给number赋值
data.number = 65; // 65是'A'的ASCII码
printf("3. 给number赋值65后:\n");
printf(" data.number = %d\n", data.number);
printf(" data.letter = %c\n", data.letter);
printf(" (letter又变成了'A')\n");
return 0;
}
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)




代码1输出的三个地址一模一样,代码2的输出,我们发现将i的第4个字节的内容修改为55 我们仔细分析就可以画出,un的内存布局图

枚举提供了类型安全的常量定义方式,提高了代码的可读性和可维护性。选择合适的声明格式取决于具体的使用场景和代码组织需求。
enum 枚举类型名
{
枚举常量1,
枚举常量2,
枚举常量3,
// ...
};enum 枚举类型名
{
枚举常量列表
} 变量名1, 变量名2, ...;enum
{
枚举常量列表
} 变量名1, 变量名2, ...;typedef enum 枚举类型名
{
枚举常量列表
} 类型别名;enum Color // 颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN; // 使用枚举常量给枚举变量赋值我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=7yhfn16q1d2