思维导图




一、 结构体:打包数据
1.1 语法结构:定义与初始化
定义模板:
c
struct 结构体标签 {
成员1类型 成员1名;
成员2类型 成员2名;
...
}; // <--- 千万别忘了这个分号!
代码示例:学生信息系统
c
#include <stdio.h>
#include <string.h>
// 1. 定义"图纸"
struct Student {
char name[50];
int age;
double gpa;
};
int main() {
// 2. 声明并初始化(按顺序)
struct Student s1 = {"Alice", 20, 3.8};
// 3. 指定初始化 (C99 标准,推荐!不受顺序限制)
struct Student s2 = {
.age = 22,
.name = "Bob",
.gpa = 3.5
};
// 4. 访问成员:使用点号 (.)
printf("姓名: %s, 年龄: %d\n", s1.name, s1.age);
// 5. 修改成员
s1.age += 1;
// s1.name = "Alice Smith"; // 错!数组名不能直接赋值
strcpy(s1.name, "Alice Smith"); // 正确:使用字符串拷贝
return 0;
}
1.2 结构体传参:值传递的代价
核心痛点:
结构体变量和 int 一样,作为参数传递时是值传递 。如果结构体很大例如包含大数组,拷贝过程会极度消耗 CPU 和内存。
最佳实践:
永远优先传递结构体指针。
c
// 慢!发生内存拷贝
void print_student_slow(struct Student s) {
printf("%s", s.name);
}
// 快!只传递 8 字节地址,加 const 防止意外修改
void print_student_fast(const struct Student *s) {
// printf("%s", s->name); // 使用箭头访问
}
二、 结构体指针与 -> 运算符
2.1 指针访问的简写
如果 p 是指向结构体的指针,访问成员有两种写法:
1.
(*p).name(语法繁琐,容易写错优先级,不推荐)2.
p->name(箭头运算符,直观,强烈推荐)
2.2 动态分配结构体(堆上造对象)
这是后面学习链表、树等数据结构的基础。
代码示例:
c
#include <stdlib.h>
struct Point {
int x;
int y;
};
int main() {
// 1. 在堆区申请内存
struct Point *p = (struct Point*)malloc(sizeof(struct Point));
if (p == NULL) return 1; // 严谨:检查分配是否成功
// 2. 赋值(使用箭头)
p->x = 10;
p->y = 20;
printf("坐标: (%d, %d)\n", p->x, p->y);
// 3. 释放内存
free(p);
p = NULL; // 规矩:防止悬空指针
return 0;
}
三、 typedef 的工程意义
每次写 struct Student 都要敲两个词,太累了?typedef 可以给类型起个别名。
3.1 语法对比
写法 A(原生写法):
c
struct Book {
char title[100];
int price;
};
// 声明变量时必须带 struct 关键字
struct Book b1;
写法 B(typedef 优化,工程标准):
c
// 定义的同时起别名
typedef struct {
char title[100];
int price;
} Book; // 现在的 'Book' 等价于 'struct { ... }'
int main() {
Book b1 = {"C Primer Plus", 60}; // 清爽!不用写 struct 了
return 0;
}
四、 枚举:代码可读性救星
4.1 为什么要用枚举?
与其用 0 代表红色,1 代表绿色(魔术数字,难以维护),不如定义一个枚举,让代码自己说话。
4.2 语法与特性
枚举本质上就是
int常量。默认从 0 开始,依次递增。
代码示例:简单的状态机
c
// 定义状态:IDLE=0, RUNNING=1, ERROR=2
typedef enum {
IDLE,
RUNNING,
ERROR
} State;
int main() {
State current_state = IDLE;
// ... 模拟系统运行 ...
current_state = RUNNING;
// 配合 switch 使用非常优雅
switch (current_state) {
case IDLE: printf("系统空闲中...\n"); break;
case RUNNING: printf("正在处理任务...\n"); break;
case ERROR: printf("发生严重错误!\n"); break;
default: printf("未知状态\n");
}
return 0;
}
五、 联合体:内存共享的艺术
5.1 核心机制:一地多用
union 的所有成员共享同一块内存地址。
大小: 至少能容纳最大的那个成员。
特性: 修改其中一个成员,会覆盖其他成员的值。
应用场景: 嵌入式通信协议、寄存器操作。
代码示例:IP 地址转换(小端序验证)
c
typedef union {
unsigned int ip_int; // 视角1:看作一个 32 位整数
unsigned char ip_bytes[4]; // 视角2:看作 4 个独立字节
} IPAddress;
int main() {
IPAddress ip;
ip.ip_int = 0x12345678; // 赋值整数
// 通过字节数组访问同一块内存
// 在常见的小端序机器(x86, ARM)上,低位存在低地址
// 输出: 78 56 34 12
printf("%x %x %x %x\n",
ip.ip_bytes[0], ip.ip_bytes[1],
ip.ip_bytes[2], ip.ip_bytes[3]);
return 0;
}
六、 练习题
题目 1: struct S { int a; char b; };,在 32 位系统默认对齐下,sizeof(struct S) 是多少?是 5 吗?
题目 2: 给定指针 struct Student *p,如何访问成员 age?写出两种方式。
题目 3: 结构体变量可以直接赋值吗?例如 s1 = s2;?它是深拷贝还是浅拷贝?
题目 4: 结构体变量可以直接比较吗?例如 if (s1 == s2)?为什么?
题目 5: 下面枚举中 BLUE 的值是多少?
c
enum Color { RED = 5, GREEN, BLUE };
题目 6: 为什么在定义链表节点时,不能在内部包含同类型的结构体变量,但可以包含同类型的指针?
c
struct Node {
int data;
// struct Node next; // 写法 A
struct Node *next; // 写法 B
};
题目 7: union U { int i; char c; };,如果 i 被赋值为 0x1234,c 的值是多少(假设小端序)?
题目 8: 使用 typedef 定义结构体指针别名(如 typedef struct Node* NodePtr;)是一个好习惯吗?
题目 9: 函数参数传递结构体时,什么时候用值传递,什么时候用指针?
题目 10: 什么是内存对齐?为什么要对齐?
题目 11: 编写一个结构体 Rect 表示矩形(包含宽和高),并写一个函数 area 计算面积,要求使用指针传参。
题目 12: 如何定义一个包含 10 个学生信息的结构体数组?
题目 13: struct { int x; } s1; 这种匿名结构体定义合法吗?有什么局限性?
题目 14: 联合体的大小取决于什么?是所有成员大小之和吗?
题目 15: 下面代码输出什么?
c
struct { int a; int b; } s = {1, 2};
struct { int a; int b; } *p = &s;
printf("%d", p->b);
七、 解析
题 1 解析
答案: 8 字节。
详解:
这是一个经典的内存对齐 问题。
int占 4 字节,char占 1 字节。为了让下一个结构体的int对齐到 4 的倍数地址,编译器会在char后面填充 3 个空字节 (Padding)。4 + 1 + (3) = 8。
题 2 解析
答案: p->age (推荐) 或 (*p).age。
题 3 解析
答案: 可以。
详解:
编译器会生成代码,将 s2 的内存按字节拷贝给 s1。这是一种浅拷贝。如果结构体里有指针,两个指针会指向同一块内存,需要小心。
题 4 解析
答案: 不可以。
详解:
C 语言没有为结构体定义
==运算符。原因在于结构体可能包含填充字节 (Padding),其中的垃圾值不可控,直接比较内存是不可靠的。必须手动编写函数逐个成员比较。
题 5 解析
答案: 7。
详解:
RED=5,后续成员默认递增:GREEN=6,BLUE=7。
题 6 解析
答案: 必须用指针(写法 B)。
详解:
如果包含同类型变量(写法 A),结构体大小会无限递归膨胀(无穷大),编译器无法计算大小。而指针大小是固定的(4或8字节),所以可以作为成员。
题 7 解析
答案: 0x34。
详解:
小端序机器上,低位数据存放在低地址。
0x1234的低字节是0x34。char c对应起始地址(低地址),所以读到的是0x34。
题 8 解析
答案: 视情况而定,但通常不推荐 隐藏指针的星号。
详解:
NodePtr p;会让使用者忘记p本质是个指针,从而忘记检查 NULL,或在对它取地址时产生逻辑混乱。Linux 内核代码规范中明确反对这种做法。
题 9 解析
答案:
结构体极小(如只包含两个 int 的坐标点)可用值传递。一般情况下优先用指针,避免昂贵的内存拷贝。
题 10 解析
答案:
CPU 访问对齐的内存地址(如 4 的倍数)速度最快。不对齐可能导致多次访存甚至硬件异常 (Bus Error)。编译器通过插入填充字节来实现对齐,以空间换时间。
题 11 解析
答案:
c
typedef struct { int w, h; } Rect;
int area(const Rect *r) {
return r->w * r->h;
}
题 12 解析
答案: struct Student class_1[10]; (如果用了 typedef,就是 Student class_1[10];)。
题 13 解析
答案: 合法。
详解:
这定义了一个匿名结构体 类型,并立即创建了变量
s1。缺点是以后没法再定义这种类型的其他变量了(除非用 typeof 等非标扩展)。
题 14 解析
答案: 至少是最大成员的大小。
详解:
并且通常要是最大成员类型的整数倍,以满足数组排列时的对齐要求。
题 15 解析
答案: 2。
详解:
指针
p指向结构体s,p->b访问第二个成员。虽然类型定义写了两遍,但在 C 中它们被视为兼容的布局(在同个作用域通常能跑通,但严格标准下是不同类型)。

日期:2025年2月13日
专栏:C语言