前言
上一章咱们彻底攻克了高级指针的难点,搞懂了二级指针的核心场景与复杂声明的右左法则,终于走出了指针迷宫。这一章咱们切入《C和指针》里自定义数据类型的核心内容——结构体与联合的底层逻辑。 很多同学学结构体,只记住了struct的定义格式,却踩了无数坑:期末考结构体大小计算题永远算不对、结构体传参总导致程序效率低下、分不清struct和union的本质区别、用结构体指针总出现内存访问错误。核心原因就是你只记住了结构体的语法,却没搞懂它在内存里的真实布局,没理解结构体与指针结合的底层逻辑。 这一章咱们全程保姆式拆解,从结构体的内存封装本质,到期末100%会考的内存对齐规则,再到结构体指针、柔性数组、联合与枚举的核心用法,把《C和指针》里的自定义类型知识点拆成零基础也能懂的内容,同时覆盖期末考、校招面试的所有高频考点。
目录
- 一、本章学习目标
- 二、核心知识点
- 2.1 结构体的本质:自定义数据类型的内存封装【理解的核心前提】
- 2.2 结构体的内存对齐规则【期末选择考《C和指针》核心】
- 2.3 结构体指针:地址与间接访问【指针核心联动】
- 2.4 结构体作为函数参数:值传递vs地址传递【期末编程必考】
- 2.5 柔性数组:结构体的动态内存技巧【校招高频考点】
- 2.6 联合union的底层本质与核心用途【期末选择必考】
- 2.7 枚举enum的基础用法与避坑指南
- 三、Dev-C++保姆式实操
- 3.1 实操1:结构体的定义、初始化与内存对齐验证
- 3.2 实操2:结构体指针的定义与成员访问
- 3.3 实操3:结构体函数传参的两种方式对比
- 3.4 实操4:柔性数组的动态内存实现
- 3.5 实操5:联合union的内存共享特性验证
- 3.6 保姆式排错指南
- 四、我的踩坑记录
- 五、课后小练习
- 基础必刷题(期末选择/填空考点)
- 进阶高频题(校招面试/期末编程题)
- 六、期末/面试考点速记
- 七、小结
一、本章学习目标
二、核心知识点
2.1 结构体的本质:自定义数据类型的内存封装【理解的核心前提】
C语言内置的int、char、double等类型,只能描述单一数据,而我们现实中需要描述“一个学生”“一本书”这种包含多个不同类型属性的复杂对象,结构体就是为了解决这个问题诞生的。
1. 结构体的本质
结构体,就是把多个不同类型的变量打包封装在一起,形成的一个自定义的复合数据类型。它本质上是一块连续的内存空间,用来存放多个不同类型的成员变量。
通俗比喻:结构体就像一个快递打包盒,里面可以放不同大小的物品(不同类型的成员变量),打包盒本身就是一个整体,有自己的地址和大小。
2. 结构体的定义与初始化
标准定义格式
// struct是结构体关键字,Student是结构体标签(类型名)
struct Student {
// 结构体的成员变量,可以是任意类型
char name[20]; // 姓名
int age; // 年龄
double score; // 分数
}; // 分号绝对不能少!
三种初始化方式
// 1. 定义变量时按顺序初始化
struct Student s1 = {"张三", 18, 90.5};
// 2. 指定成员初始化(C99标准,推荐,可读性高)
struct Student s2 = {
.name = "李四",
.age = 19,
.score = 95.0
};
// 3. 定义结构体的同时定义变量(不推荐,可读性差)
struct Teacher {
char name[20];
char subject[20];
} t1 = {"王老师", "C语言"};
3. 结构体成员的访问
普通结构体变量,用.运算符访问成员:
struct Student s = {"张三", 18, 90.5};
// 访问成员
printf("姓名:%s\\n", s.name);
printf("年龄:%d\\n", s.age);
// 修改成员
s.score = 92.0;
2.2 结构体的内存对齐规则【期末选择100%考·《C和指针》核心】
这是《C和指针》里重点讲解的核心知识点,也是期末选择题的必考题,更是新手90%会算错的内容。很多同学以为结构体的大小就是所有成员大小的总和,实则完全不是!
结构体的内存大小,不是简单的成员大小相加,而是遵循内存对齐规则,编译器会自动给结构体的成员之间、末尾填充空白字节,这就是内存对齐。
1. 为什么要内存对齐?
- 提升CPU访问效率:CPU读取内存不是逐字节读的,而是按“字长”(32位系统4字节,64位系统8字节)块读取的。如果数据没有对齐,CPU需要多次读取内存才能拿到完整数据,效率极低;
- 平台兼容性:部分硬件平台只能访问对齐的内存地址,未对齐的数据会直接导致程序崩溃。
2. 内存对齐的4大核心规则(期末必考,必须背熟)
我们以默认对齐数(编译器默认的对齐单位,Dev-C++、VS默认是8字节)为基础,规则如下:
3. 保姆式计算示例(32位系统,默认对齐数8)
// 计算这个结构体的大小
struct Test {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
分步计算:
最终结果:这个结构体的大小是12字节,不是1+4+1=6字节!
2.3 结构体指针:地址与间接访问【指针核心联动】
结构体指针,就是指向结构体变量的指针,存储的是结构体变量的起始内存地址,这是结构体与指针结合的核心用法,也是后续链表实现的基础。
1. 结构体指针的定义与初始化
struct Student s = {"张三", 18, 90.5};
// 结构体指针p,指向结构体变量s
struct Student *p = &s;
2. 结构体指针的成员访问:->运算符
结构体指针访问成员,有两种等价方式,推荐用->运算符,更简洁直观:
struct Student s = {"张三", 18, 90.5};
struct Student *p = &s;
// 方式1:先解引用,再用.访问(不推荐,括号不能少)
printf("姓名:%s\\n", (*p).name);
// 方式2:用->运算符访问(推荐,《C和指针》标准写法)
printf("年龄:%d\\n", p->age);
// 修改成员
p->score = 92.0;
【核心区别】普通结构体变量用.访问成员,结构体指针用->访问成员,期末选择题必考两者的使用场景。
2.4 结构体作为函数参数:值传递vs地址传递【期末编程必考】
结构体作为函数参数,有两种传参方式,两者的效率、效果天差地别,期末编程题必考,也是新手最容易踩的效率坑。
1. 值传递:传递结构体本身
值传递会把整个结构体完整拷贝一份,传给函数的形参,函数里修改形参,完全不影响外面的实参。
// 值传递:形参是结构体本身
void printStudent(struct Student s) {
printf("姓名:%s,年龄:%d,分数:%.1lf\\n", s.name, s.age, s.score);
s.age = 20; // 修改的是拷贝的形参,外面的实参不变
}
【致命缺点】如果结构体很大,拷贝整个结构体会消耗大量栈内存,执行效率极低,实际开发中几乎不用值传递传结构体。
2. 地址传递:传递结构体指针(推荐,最优方式)
地址传递只需要把结构体的起始地址传给函数,形参是结构体指针,不会拷贝整个结构体,只拷贝4/8字节的地址,效率极高,还能通过指针修改外面的实参。
// 地址传递:形参是结构体指针,const保护结构体不被意外修改
void printStudent(const struct Student *p) {
printf("姓名:%s,年龄:%d,分数:%.1lf\\n", p->name, p->age, p->score);
}
// 需要修改结构体的场景,不加const
void modifyScore(struct Student *p, double new_score) {
p->score = new_score; // 通过指针直接修改实参
}
【《C和指针》推荐规范】结构体传参,优先用地址传递,不需要修改的话,一定要加const保护,避免意外修改结构体内容。
2.5 柔性数组:结构体的动态内存技巧【校招高频考点】
柔性数组是C99标准引入的特性,指的是结构体中最后一个元素是大小未知的数组,它可以实现结构体的动态内存分配,是校招面试的高频考点。
1. 柔性数组的定义格式
struct FlexArray {
int len; // 数组长度
// 柔性数组:放在结构体最后,[]里不写大小,或者写0
int arr[];
};
2. 柔性数组的核心特点
3. 核心优势
相比在结构体里放一个指针,再给指针malloc内存,柔性数组有两个核心优势:
- 内存连续:整个结构体和柔性数组的内存是连续的,访问效率更高,只需要一次free就能释放所有内存,避免内存泄漏;
- 没有内存碎片:一次malloc申请所有内存,不会产生多次malloc的内存碎片。
2.6 联合union的底层本质与核心用途【期末选择必考】
联合(也叫共用体),和结构体的定义格式很像,但内存逻辑完全不同,是期末选择题的必考点,新手很容易和结构体搞混。
1. 联合的底层本质
联合的所有成员,共享同一块内存空间,同一个时间点,联合只能存储一个成员的值,修改其中一个成员,会覆盖其他成员的值。联合的总大小,等于所有成员中最大的那个成员的大小。
通俗比喻:联合就像一个酒店的共享房间,同一时间只能住一个人(一个成员),不同时间可以住不同的人,房间的大小由最高的那个人(最大的成员)决定。
2. 联合的定义与使用
// 联合的定义,关键字union
union Test {
char a; // 1字节
int b; // 4字节
double c;// 8字节
};
这个联合的大小是8字节,等于最大的成员double的大小,所有成员共享这8字节的内存。
3. 联合的核心用途
2.7 枚举enum的基础用法与避坑指南
枚举就是把一个变量可能的所有取值,一一列举出来,限定变量的取值范围,让代码的可读性更高,比用#define宏定义更安全,有完整的类型检查。
1. 枚举的定义与使用
// 枚举定义,关键字enum
enum Week {
// 枚举常量,默认从0开始,依次递增1
Mon, // 0
Tue, // 1
Wed, // 2
Thu, // 3
Fri, // 4
Sat, // 5
Sun // 6
};
int main() {
// 定义枚举变量,只能赋值枚举里的常量
enum Week today = Mon;
if (today == Sat || today == Sun) {
printf("周末\\n");
} else {
printf("工作日\\n");
}
return 0;
}
2. 自定义枚举常量的值
enum Season {
Spring = 1, // 手动指定为1
Summer, // 2
Autumn = 10,// 手动指定为10
Winter // 11
};
3. 避坑指南
三、Dev-C++保姆式实操
3.1 实操1:结构体的定义、初始化与内存对齐验证
#include <stdio.h>
// 测试结构体
struct Test {
char a;
int b;
char c;
};
// 嵌套结构体
struct Nest {
short s;
struct Test t;
double d;
};
int main() {
struct Test t = {'A', 100, 'B'};
printf("===== 结构体内存对齐验证 =====\\n");
printf("char a的偏移量:%d\\n", (int)&t.a – (int)&t);
printf("int b的偏移量:%d\\n", (int)&t.b – (int)&t);
printf("char c的偏移量:%d\\n", (int)&t.c – (int)&t);
printf("struct Test总大小:%d 字节\\n", sizeof(struct Test));
printf("\\n===== 嵌套结构体验证 =====\\n");
printf("struct Nest总大小:%d 字节\\n", sizeof(struct Nest));
getchar();
return 0;
}
运行结果示例(32位系统)
===== 结构体内存对齐验证 =====
char a的偏移量:0
int b的偏移量:4
char c的偏移量:8
struct Test总大小:12 字节
===== 嵌套结构体验证 =====
struct Nest总大小:24 字节
亲手运行一遍,观察成员的偏移量和结构体总大小,彻底理解内存对齐规则,期末计算题100%考。
3.2 实操2:结构体指针的定义与成员访问
#include <stdio.h>
#include <string.h>
// 学生结构体
struct Student {
char name[20];
int age;
double score;
};
int main() {
struct Student s = {"张三", 18, 90.5};
// 结构体指针指向s
struct Student *p = &s;
printf("===== 结构体指针访问 =====\\n");
// 两种访问方式
printf("姓名:%s\\n", (*p).name);
printf("年龄:%d\\n", p->age);
printf("分数:%.1lf\\n", p->score);
// 通过指针修改成员
strcpy(p->name, "李四");
p->age = 19;
p->score = 95.0;
printf("\\n修改后的信息:\\n");
printf("姓名:%s\\n", p->name);
printf("年龄:%d\\n", p->age);
printf("分数:%.1lf\\n", p->score);
getchar();
return 0;
}
运行结果
===== 结构体指针访问 =====
姓名:张三
年龄:18
分数:90.5
修改后的信息:
姓名:李四
年龄:19
分数:95.0
亲手敲一遍,记住.和->的使用区别,这是后续链表的基础。
3.3 实操3:结构体函数传参的两种方式对比
#include <stdio.h>
struct Student {
char name[20];
int age;
double score;
};
// 方式1:值传递,拷贝整个结构体
void printByValue(struct Student s) {
printf("值传递:姓名=%s,年龄=%d,分数=%.1lf\\n", s.name, s.age, s.score);
s.age = 20; // 修改不影响实参
printf("值传递内部修改后年龄:%d\\n", s.age);
}
// 方式2:地址传递,只传地址,const保护不被修改
void printByPointer(const struct Student *p) {
printf("地址传递:姓名=%s,年龄=%d,分数=%.1lf\\n", p->name, p->age, p->score);
// p->age = 20; // 加了const,修改会直接编译报错,安全
}
// 地址传递修改结构体
void modifyScore(struct Student *p, double new_score) {
p->score = new_score;
}
int main() {
struct Student s = {"张三", 18, 90.5};
printf("原数据年龄:%d\\n", s.age);
printByValue(s);
printf("值传递后,实参年龄:%d\\n\\n", s.age);
printByPointer(&s);
modifyScore(&s, 98.0);
printf("地址传递修改分数后:%.1lf\\n", s.score);
getchar();
return 0;
}
运行结果
原数据年龄:18
值传递:姓名=张三,年龄=18,分数=90.5
值传递内部修改后年龄:20
值传递后,实参年龄:18
地址传递:姓名=张三,年龄=18,分数=90.5
地址传递修改分数后:98.0
期末编程题必考,亲手运行一遍,理解两种传参方式的区别,记住优先用地址传递。
3.4 实操4:柔性数组的动态内存实现
#include <stdio.h>
#include <stdlib.h>
// 包含柔性数组的结构体
struct FlexArray {
int len;
int arr[]; // 柔性数组
};
int main() {
int n = 5;
// 动态申请内存:结构体固定大小 + 柔性数组n个int的大小
struct FlexArray *p = (struct FlexArray*)malloc(sizeof(struct FlexArray) + n * sizeof(int));
if (p == NULL) {
printf("内存申请失败!\\n");
return 1;
}
p->len = n;
// 给柔性数组赋值
for (int i = 0; i < p->len; i++) {
p->arr[i] = i + 1;
}
printf("===== 柔性数组内容 =====\\n");
for (int i = 0; i < p->len; i++) {
printf("%d ", p->arr[i]);
}
// 一次free释放所有内存
free(p);
p = NULL;
getchar();
return 0;
}
运行结果
===== 柔性数组内容 =====
1 2 3 4 5
校招高频考点,亲手敲一遍,理解柔性数组的内存连续特性。
3.5 实操5:联合union的内存共享特性验证
#include <stdio.h>
// 联合定义
union Test {
char a;
int b;
double c;
};
// 用联合判断大小端字节序
int isLittleEndian() {
union EndianTest {
int num;
char ch;
} test;
test.num = 0x12345678;
// 小端:低地址存低位,ch是0x78;大端:ch是0x12
return test.ch == 0x78;
}
int main() {
union Test t;
printf("union Test总大小:%d 字节\\n", sizeof(union Test));
// 验证内存共享
t.b = 0x12345678;
printf("给b赋值后,a的值:0x%X\\n", t.a);
t.a = 0xAA;
printf("给a赋值后,b的值:0x%X\\n", t.b);
// 判断大小端
if (isLittleEndian()) {
printf("当前系统是小端字节序\\n");
} else {
printf("当前系统是大端字节序\\n");
}
getchar();
return 0;
}
运行结果示例(32位小端系统)
union Test总大小:8 字节
给b赋值后,a的值:0x78
给a赋值后,b的值:0x123456AA
当前系统是小端字节序
期末选择题必考struct和union的区别,校招面试常考大小端判断,亲手运行一遍,理解联合的内存共享本质。
3.6 保姆式排错指南
| 编译报错“incomplete type is not allowed” | 结构体定义后忘记加分号,或者使用了未定义的结构体标签 | 结构体定义末尾必须加分号,使用前必须先定义结构体 |
| 结构体大小计算永远不对 | 没考虑内存对齐规则,直接把成员大小相加 | 严格按照4大内存对齐规则分步计算,注意偏移量和总大小的补齐 |
| 编译报错“expression must have struct or union type” | 结构体指针用了.访问成员,或者普通结构体变量用了-> | 普通变量用.,结构体指针用->,严格区分使用场景 |
| 结构体传参后,外面的实参没变化 | 用了值传递,修改的是形参的拷贝 | 需要修改实参必须用地址传递,传结构体指针给函数 |
| 程序崩溃/内存泄漏 | 柔性数组忘记malloc,或者多次free | 柔性数组必须动态申请内存,整个结构体只需要一次malloc和一次free |
| 联合的成员值错乱 | 同时使用了联合的多个成员,覆盖了内存 | 联合同一时间只能使用一个成员,修改一个成员会覆盖其他成员 |
四、我的踩坑记录
五、课后小练习
基础必刷题(期末选择/填空考点)
题1
要求:计算下面结构体的总大小(32位系统,默认对齐数8),并写出分步计算过程。
struct Test {
char a;
short b;
int c;
double d;
};
答案:总大小16字节
分步计算:
1. char a放在偏移量0,占1字节;
2. short b自身大小2,对齐数8,取2,偏移量必须是2的倍数,填充1字节,偏移量2开始,占2字节,到偏移量3;
3. int c自身大小4,取4,偏移量4开始,占4字节,到偏移量7;
4. double d自身大小8,取8,偏移量8开始,占8字节,到偏移量15;
5. 总大小16字节,是最大成员大小8的整数倍,无需额外填充。
题2
要求:简述struct和union的3个核心区别。
答案:
1. 内存布局不同:struct的每个成员有独立的内存空间,union的所有成员共享同一块内存空间;
2. 大小计算不同:struct的大小是所有成员大小之和+对齐填充,union的大小是最大成员的大小+对齐填充;
3. 使用方式不同:struct可以同时使用多个成员,union同一时间只能使用一个成员,修改一个成员会覆盖其他成员。
题3
要求:定义一个图书结构体Book,包含书名、作者、价格三个成员,定义结构体变量并初始化,用结构体指针访问并输出成员信息。
#include <stdio.h>
struct Book {
char name[50];
char author[20];
double price;
};
int main() {
struct Book book = {"C和指针", "肯尼斯·里科", 59.0};
struct Book *p = &book;
printf("书名:%s\\n", p->name);
printf("作者:%s\\n", p->author);
printf("价格:%.2lf元\\n", p->price);
getchar();
return 0;
}
进阶高频题(校招面试/期末编程题)
题4
要求:用结构体实现学生信息管理,定义学生结构体,包含姓名、学号、3门课成绩,写函数实现:输入学生信息、计算平均分、输出学生信息。
#include <stdio.h>
#include <string.h>
#define N 3 // 学生人数
struct Student {
char name[20];
char id[20];
double score[3];
double avg;
};
// 输入学生信息
void inputStudent(struct Student *p, int n) {
for (int i = 0; i < n; i++) {
printf("请输入第%d个学生信息(姓名 学号 3门课成绩):\\n", i+1);
scanf("%s %s %lf %lf %lf", (p+i)->name, (p+i)->id, &(p+i)->score[0], &(p+i)->score[1], &(p+i)->score[2]);
// 计算平均分
(p+i)->avg = ((p+i)->score[0] + (p+i)->score[1] + (p+i)->score[2]) / 3.0;
}
}
// 输出学生信息
void printStudent(const struct Student *p, int n) {
printf("\\n===== 学生信息表 =====\\n");
printf("姓名\\t学号\\t平均分\\n");
for (int i = 0; i < n; i++) {
printf("%s\\t%s\\t%.1lf\\n", (p+i)->name, (p+i)->id, (p+i)->avg);
}
}
int main() {
struct Student stus[N];
inputStudent(stus, N);
printStudent(stus, N);
getchar();
getchar();
return 0;
}
题5
要求:用联合实现,判断int类型的4个字节中,每个字节的值。
#include <stdio.h>
union IntByte {
int num;
unsigned char byte[4];
};
int main() {
union IntByte test;
test.num = 0x12345678;
printf("int值:0x%X\\n", test.num);
printf("每个字节的值:\\n");
for (int i = 0; i < 4; i++) {
printf("byte[%d] = 0x%X\\n", i, test.byte[i]);
}
getchar();
return 0;
}
六、期末/面试考点速记
七、小结
本章咱们彻底啃透了结构体与联合的底层逻辑,从结构体的内存封装、期末必考的内存对齐规则,到结构体指针、函数传参、柔性数组,再到联合的内存共享特性、枚举的用法,终于掌握了C语言自定义数据类型的所有核心内容。 结构体是《C和指针》里的重要章节,更是期末编程题、校招面试的高频考点,也是后续实现链表、树等数据结构的基础。我踩过的内存对齐算错、传参效率低、.和->搞混这些坑,大家千万别再踩!还是那句贯穿整个专栏的老话:学C语言没有捷径,敲代码、敲代码、敲代码才是硬道理!亲手把每个实操代码敲一遍,比看十遍都管用。 下一章咱们学习动态内存管理:malloc与free全解,结合结构体实现更灵活的动态数据存储,为下一章链表的实现打下基础,继续冲~



