1. C语言核心结构解析
C语言核心
├─ 程序结构
│ ├─ 预处理指令(#include / #define)
│ ├─ 全局变量(整个程序可见)
│ └─ 函数(唯一执行入口main)
├─ 语法特点
│ ├─ 过程式编程
│ ├─ 手动内存管理
│ └─ 指针直接操作内存
├─ 内存管理
│ ├─ 栈内存(自动回收)
│ ├─ 堆内存(手动malloc/free)
│ └─ 静态区(全局/静态变量)
├─ 核心武器├─ 指针(直接操作内存地址)├─ 结构体(数据聚合)└─ 函数指针(动态行为)
一、程序组成骨架
// 1. 预处理指令(引入头文件)
#include <stdio.h>
#include <stdlib.h>// 2. 全局变量(整个程序可见)
int global_count = 0;// 3. 函数声明(先声明后使用)
void print_message(const char* msg);// 4. 主函数(程序入口)
int main() {// 5. 局部变量(栈内存)int local_num = 42;// 6. 指针操作(直接访问内存地址)int* ptr = &local_num;*ptr = 100; // 通过指针修改值// 7. 函数调用print_message("Hello C World!");// 8. 动态内存分配(堆内存)int* heap_array = malloc(5 * sizeof(int));if (heap_array != NULL) {for (int i=0; i<5; i++) {heap_array[i] = i * 2;}free(heap_array); // 必须手动释放}return 0;
}// 9. 函数定义
void print_message(const char* msg) {printf("%s\n", msg);global_count++;
}
二、语法特点精要
-
过程式编程
通过函数组织代码流,没有类的概念:// 结构体模拟简单对象 struct Point {int x;int y; };// 操作结构体的函数 void move_point(struct Point* p, int dx, int dy) {p->x += dx; // 指针访问成员p->y += dy; }
-
手动内存管理
必须显式申请/释放内存:// 典型错误示例:忘记释放内存 void memory_leak() {char* str = malloc(100);// 没有free(str) -> 内存泄漏! }
-
指针直接操作内存
直接访问硬件地址(嵌入式开发常用):// 通过指针修改数组 void reverse_array(int* arr, int size) {int *start = arr;int *end = arr + size - 1;while (start < end) {int temp = *start;*start = *end;*end = temp;start++;end--;} }
三、工程最佳实践
-
防御性编程
预防空指针和越界访问:// 安全版字符串拷贝 void safe_strcpy(char* dest, const char* src, size_t dest_size) {if (dest == NULL || src == NULL || dest_size == 0) {return;}size_t i;for (i=0; i<dest_size-1 && src[i]!='\0'; i++) {dest[i] = src[i];}dest[i] = '\0'; // 强制终止符 }
-
模块化设计
分离头文件与实现:// math_utils.h(头文件) #ifndef MATH_UTILS_H #define MATH_UTILS_H int add(int a, int b); // 声明接口 #endif// math_utils.c(实现) #include "math_utils.h" int add(int a, int b) {return a + b; // 实际功能 }
-
错误处理范式
通过返回值传递状态:// 文件操作示例 int safe_file_open(const char* filename, FILE** file) {if (filename == NULL || file == NULL) {return -1; // 错误码}*file = fopen(filename, "r");if (*file == NULL) {return -2; // 具体错误类型}return 0; // 成功 }
-
内存管理纪律
使用工具检测泄漏(如Valgrind):// 正确使用动态内存 void process_data() {int* data = malloc(1000 * sizeof(int));if (data == NULL) {// 处理分配失败return;}// 使用数据...for (int i=0; i<1000; i++) {data[i] = i * 3;}free(data); // 确保释放data = NULL; // 防止野指针 }
2. C语言核心武器精讲
一、指针
指针的本质:内存的导航系统
想象你住在一个巨大的小区里,每户人家都有门牌号。指针就像是一个记录门牌号的小纸条,通过这个纸条你能快速找到对应的人家。在计算机中:
• 内存 = 小区的每户房子
• 变量 = 住在房子里的人
• 指针 = 记录门牌号(内存地址)的纸条
int age = 25; // 定义变量(住进一个叫age的人)
int *p = &age; // 定义指针(记录age的门牌号)
指针的核心价值在于直接操作内存,这种能力带来了:
• 高效性:避免数据拷贝(如传递大结构体)
• 灵活性:动态内存分配、硬件访问等
• 底层控制:实现数据结构(链表、树等)
就像一把双刃剑,指针用得好可以让程序飞檐走壁,用不好则会伤及自身。理解内存布局+严谨的代码习惯,是指针使用的王道。
指针的四大核心操作
-
取地址(&)
获取变量的门牌号printf("age的地址:%p\n", &age); // 输出类似0x7ffd1234
-
解引用(*)
根据门牌号找到对应的住户printf("通过指针获取值:%d\n", *p); // 输出25
-
指针赋值
复制门牌号纸条int *p2 = p; // p2和p现在指向同一个地址
-
指针运算
按户型大小移动门牌号int arr[3] = {10, 20, 30}; int *ptr = arr; // 指向第一个元素 printf("%d\n", *(ptr + 1)); // 输出20(移动到第二个元素)
指针类型:户型图的重要性
指针类型决定了如何解读内存中的数据,就像户型图决定了如何划分房间:
int num = 0x12345678;
int *pInt = # // 读取4字节
char *pChar = (char*)pInt; // 读取1字节printf("%x\n", *pInt); // 输出0x12345678
printf("%x\n", *pChar); // 输出0x78(小端模式下)
指针类型 | 户型解释 |
---|---|
int* | 从地址开始读取4字节作为整数 |
char* | 只读取1字节作为字符 |
double* | 读取8字节作为浮点数 |
指针与数组:地址簿与住户列表
数组名本质是指向首元素的指针常量:
int scores[5] = {90, 85, 77, 95, 88};// 以下三种写法等价
printf("%d\n", scores[2]);
printf("%d\n", *(scores + 2));
printf("%d\n", *(2 + scores)); // 甚至可以用2[scores]这种写法
关键区别:
int arr[5];
int *p = arr;sizeof(arr); // 返回20(5个int的总大小)
sizeof(p); // 返回8(指针变量的大小,64位系统)
多级指针:快递驿站的分拣系统
• 一级指针:普通包裹(直接指向数据)
• 二级指针:包裹的分拣柜(指向指针的指针)
• N级指针:多层分拣系统
int num = 100;
int *p = #
int **pp = &p; // 二级指针printf("%d\n", **pp); // 输出100
函数指针:遥控器的按钮
函数指针存储的是函数的入口地址,就像遥控器的按钮对应具体功能:
// 定义函数类型
typedef void (*RemoteButton)(int);void VolumeUp(int level) {printf("音量+%d\n", level);
}int main() {RemoteButton btn = VolumeUp;btn(3); // 输出"音量+3"return 0;
}
指针与动态内存:小区扩建管理
// 申请新房(堆内存)
int *arr = malloc(10 * sizeof(int)); // 装修新房
arr[0] = 100;// 退房(必须手动归还)
free(arr);
内存布局示意图:
┌─────────────┐
│ 栈(stack) │ ← 局部变量(自动管理)
├─────────────┤
│ 堆(heap) │ ← malloc分配(手动管理)
├─────────────┤
│ 数据段(data) │ ← 全局/静态变量
├─────────────┤
│ 代码段(text) │ ← 程序代码
└─────────────┘
常见指针错误
-
野指针(指向未知区域)
int *p; // 未初始化 printf("%d", *p); // 危险操作!
-
空指针解引用
int *p = NULL; *p = 10; // 程序崩溃
-
内存泄漏
void func() {int *p = malloc(100); // 忘记free(p) }
调试技巧
-
GDB查看指针值
(gdb) p p # 查看指针地址 (gdb) x/4wx p # 查看指针指向的4个word(16进制)
-
Valgrind检测内存问题
valgrind --leak-check=full ./your_program
二、函数指针
在C语言中,函数指针是一个变量,存储的是函数的入口地址,通过这个指针可以间接调用函数。
函数指针让你可以动态组装程序行为,而不是写死代码逻辑。
• 解耦:调用方无需知道具体函数实现(如回调函数)
• 扩展性:新增功能无需修改原有逻辑(如策略模式)
• 抽象:用统一接口处理不同操作(如文件读写器)
函数指针的声明与使用
• 普通变量:存储数据(如int a=10;
)
• 函数指针:存储函数(如void (*remote)(void) = &openTV;
)
// 1. 声明函数指针类型(类比:定义遥控器按钮类型)
typedef void (*Button)(int); // 这个按钮接收int参数,无返回值// 2. 定义具体函数(类比:按钮对应的功能)
void VolumeUp(int level) {printf("音量增加至%d\n", level);
}void PowerOff(int code) {printf("关机代码%d\n", code);
}int main() {// 3. 创建函数指针变量并赋值(配对按钮与功能)Button btn1 = VolumeUp; // 等价于 &VolumeUpButton btn2 = PowerOff;// 4. 通过指针调用函数(按下按钮)btn1(5); // 输出:音量增加至5btn2(999); // 输出:关机代码999return 0;
}
语法解析:
// 函数指针声明分解:
// 返回值类型 (*指针变量名) (参数列表)
// ↓ ↓ ↓void (*buttonPtr) (int);
函数指针的典型应用
-
回调函数(Callback)
场景:图书馆通知系统// 定义回调函数类型 typedef void (*Notify)(const char* msg);void checkBook(Notify callback) {if(发现逾期) {callback("您的书已逾期!"); // 触发回调} }// 用户自定义的通知方式 void SMSNotify(const char* msg) {printf("发送短信:%s\n", msg); }int main() {checkBook(SMSNotify); // 注册回调函数return 0; }
-
策略模式
场景:游戏角色切换武器typedef void (*AttackFunc)(void);void SwordAttack() { printf("挥舞剑!\n"); } void BowAttack() { printf("拉弓射箭!\n"); }int main() {AttackFunc currentAttack = SwordAttack;currentAttack(); // 使用剑攻击currentAttack = BowAttack;currentAttack(); // 切换为弓箭攻击return 0; }
-
事件驱动编程
场景:GUI按钮点击事件typedef struct {void (*onClick)(void); // 点击事件处理函数 } Button;Button loginBtn; loginBtn.onClick = &handleLogin; // 绑定点击事件
函数指针底层原理
-
内存视角
• 编译后的函数代码存储在代码段
• 函数指针存储的是函数第一条指令的内存地址
• 调用btn1(5)
时,CPU会跳转到该地址执行指令 -
汇编层面
假设函数VolumeUp
的地址是0x400520
:mov rdi, 5 ; 传递参数 call 0x400520 ; 调用函数(函数指针本质)
常见错误与调试
-
类型不匹配
void func1(int); void (*ptr)(float) = func1; // 错误!参数类型不匹配
-
空指针调用
void (*ptr)(void) = NULL; ptr(); // 段错误(Segmentation fault)
-
错误调试技巧
# 使用GDB查看函数地址 (gdb) p VolumeUp $1 = {void (int)} 0x400520 <VolumeUp>
三、结构体
结构体的本质——内存的「自定义组装」
结构体是程序员对内存的自由拼接,将零散数据按需组合成新类型。
底层实现:编译器按成员顺序,在内存中依次排列各变量,并自动处理对齐(内存空隙)。
struct Student {char name[20]; // 0-19字节int age; // 20-23字节(对齐到4的倍数)float score; // 24-27字节
}; // 总大小:28字节(实测可用sizeof验证)// 创建实例
struct Student s1 = {"Alice", 20, 95.5};
结构体核心玩法
1. 基本操作(对象操作)——「实物快递」式操作
struct Student s1 = {"Alice", 20, 95.5}; // 创建实物包裹
s1.age = 21; // 直接拆开包裹修改
struct Student s2 = s1; // 克隆一个完整包裹
• 内存特点:
- 直接在栈内存中分配空间(自动管理生命周期)
- 每次赋值都是深拷贝(复制整个结构体的二进制数据)
- 结构体较大时(如包含数组),拷贝开销显著
• 典型场景:
小体积结构体、函数内部临时变量、无需跨函数共享数据时
2、指针操作(地址操作)——「遥控器」式操作
struct Student* p = &s1; // 获取包裹的遥控器(地址)
p->age = 22; // 用遥控器远程修改struct Student* p_heap = malloc(...); // 在堆上造新包裹
free(p_heap); // 必须手动销毁
• 内存特点:
- 通过地址访问数据(无数据拷贝,操作原对象)
- 可操作堆内存(动态分配,生命周期由程序员控制)
- 多个指针可指向同一对象(需注意数据一致性)
• 典型场景:
大体积结构体、跨函数共享数据、动态数据结构(链表/树)
特性 | 基本操作(s.age ) | 指针操作(p->age ) |
---|---|---|
内存位置 | 栈内存(自动分配释放) | 栈/堆内存均可操作 |
赋值开销 | 深拷贝(复制所有成员) | 仅复制地址(4/8字节) |
修改影响范围 | 仅影响当前副本 | 影响所有指向该地址的指针 |
生命周期控制 | 函数结束时自动释放 | 堆内存需手动free |
典型应用场景 | 小型临时数据、函数内部使用 | 大型数据、跨函数共享、动态结构 |
场景1:函数传参开销对比
// 基本操作传值(产生拷贝)
void printStudent(struct Student s) { printf("%s", s.name); // 拷贝20字节的name数组
}// 指针操作传址(零拷贝)
void modifyStudent(struct Student* s) {s->score += 5.0; // 直接操作原数据
}// 调用
printStudent(s1); // 拷贝28字节结构体
modifyStudent(&s1); // 仅传递4/8字节地址
场景2:动态数据结构(链表)
struct Node {int data;struct Node* next; // 必须用指针实现链式结构
};// 创建链表节点
struct Node* head = malloc(sizeof(struct Node));
head->data = 10;
head->next = malloc(sizeof(struct Node)); // 动态扩展
基本操作如同直接操作实物,简单安全但效率低;
指针操作如同使用遥控器,灵活高效但需谨慎管理内存。
理解二者的内存本质,才能在栈/堆、拷贝/共享、安全/性能之间做出最佳选择。
结构体经典应用场景
-
文件格式解析
// BMP文件头结构 #pragma pack(1) struct BmpHeader {char signature[2]; // "BM"int file_size;short reserved1;short reserved2;int data_offset; };
-
面向对象模拟
// C语言实现"类" struct Animal {void (*speak)(void); // 函数指针模拟方法int age; };void dogSpeak() { printf("Wang!\n"); } struct Animal dog = {dogSpeak, 3}; dog.speak(); // 调用"方法"
-
网络协议封装
// TCP协议伪首部 struct TcpPseudoHeader {uint32_t src_ip;uint32_t dst_ip;uint8_t zero;uint8_t protocol;uint16_t tcp_length; };
-
典型代码案例(链表实现)
#include <stdio.h>
#include <stdlib.h>// 链表节点结构体
// 每个节点包含数据域和指针域,如同火车车厢:data是货物,next是连接挂钩。
struct Node {int data; // 存储数据struct Node* next; // 指向下个节点的指针
};// 创建新节点
// 类似造新车厢:申请空间 → 装货 → 挂钩置空。
struct Node* create_node(int value) {struct Node* node = malloc(sizeof(struct Node)); // 申请内存node->data = value; // 填入数据node->next = NULL; // 新节点默认无后续return node;
}// 插入到链表末尾
// 参数struct Node** head双指针必要性:直接传struct Node* head时,函数内修改head仅在函数内生效;当链表为空时,需要修改头指针本身让它指向新节点;就像你要换火车头,必须直接操作控制火车头的遥控器void append_node(struct Node** head, int value) {// 双指针作用:当链表为空时,需要修改head指针本身struct Node* new_node = create_node(value);if (*head == NULL) { // 空链表直接作为头节点*head = new_node;return;}struct Node* current = *head; // 从头开始遍历while (current->next != NULL) { // 找到最后一个节点current = current->next; // 类似顺藤摸瓜}current->next = new_node; // 挂上新节点
}// 释放整个链表
// 必须按顺序释放,避免访问已释放内存。如同拆火车:先记录车厢位置 → 走到下一节 → 拆掉前一节。
void free_list(struct Node* head) {struct Node* current = head;while (current != NULL) {struct Node* temp = current; // 暂存当前节点地址current = current->next; // 先跳转到下一个free(temp); // 再释放当前节点}
}int main() {struct Node* list = NULL; // 初始空链表append_node(&list, 10); // 第一次插入修改头指针append_node(&list, 20); // 后续插入只需找末尾append_node(&list, 30);// 遍历输出:10 -> 20 -> 30 -> NULLstruct Node* current = list;while (current != NULL) {printf("%d -> ", current->data);current = current->next;}free_list(list); // 释放所有节点
}
结构体高端玩法: 柔性数组(Flexible Array)
1. 核心思想:结构体与数据的“二合一”
柔性数组的本质是 将结构体与可变长度的数据绑定在连续的内存块中。
传统做法是结构体里用指针指向另一块堆内存(如char *data
),而柔性数组直接让数据“长在”结构体的末尾,形成“头(元数据)+身(数据)”的一体化结构。
柔性数组通过 结构体与数据的“内存绑定”,以底层内存布局的精确控制,换取性能和资源的高效利用。其本质是一种对内存管理的“手工优化”,适用于对性能敏感或资源受限的场景(如嵌入式、网络协议)。
// 传统方法:结构体+指针+数据(三块内存)
struct Data {int length;char *data; // 额外malloc分配
};// 柔性数组:结构体+数据(一块连续内存)
struct DynamicData {int length;char data[]; // 数据直接跟在结构体后面
};
2. 实现原理:手动计算内存布局
• 内存分配:通过malloc
一次性分配 结构体大小 + 数据所需空间。
• 内存布局:结构体成员length
位于内存块开头,data
数组紧随其后。
// 示例:分配结构体 + 100字节数据空间
struct DynamicData *p = malloc(sizeof(struct DynamicData) + 100);
内存布局:
+--------+-------------------+
| length | data[0] ... data[99] |
+--------+-------------------+
• 访问数据:直接通过p->data[i]
访问,无需二次寻址(传统方法需要p->data
跳转到另一块内存)。
3. 核心优势:效率与简洁性
• 内存连续:结构体与数据在物理上连续,减少内存碎片,提升CPU缓存命中率。
• 单次分配:仅需一次malloc
/free
,避免传统方法的多次分配(先分配结构体,再分配数据)。
• 无指针开销:省去指针变量(通常8字节)的内存占用,适合小内存设备。
• 安全便捷:数据与结构体生命周期一致,避免野指针风险。
4. 典型应用场景
- 网络协议包:包头(长度、类型) + 包体(变长数据)。
- 动态字符串:结构体记录长度,柔性数组存储字符。
- 自定义容器:如动态数组、内存池的元信息与数据绑定。
// 示例:网络协议包解析
struct Packet {int type;int length;char payload[]; // 变长数据
};// 分配并填充数据
struct Packet *packet = malloc(sizeof(struct Packet) + data_size);
packet->type = 1;
packet->length = data_size;
memcpy(packet->payload, raw_data, data_size);
特性 | 柔性数组 | 传统指针 |
---|---|---|
内存分配次数 | 1次 | 2次(结构体+数据) |
内存连续性 | 结构体与数据连续 | 结构体与数据可能分散 |
内存占用 | 无指针开销 | 多一个指针变量(8字节) |
访问速度 | 直接访问(无跳转) | 需指针跳转(可能缓存未命中) |
安全性 | 数据与结构体生命周期一致 | 可能野指针、双重释放 |
3. C语言工程技巧
一、编译&连接 过程
编译&链接过程
把C代码变成可执行文件,就像烤蛋糕:
-
预处理:处理宏和头文件(揉面加配料)
gcc -E test.c -o test.i
-
编译:生成汇编代码(定型蛋糕胚)
gcc -S test.i -o test.s
-
汇编:转成机器码(烤箱烘焙)
gcc -c test.s -o test.o
-
链接:合并库文件(加奶油装饰)
gcc test.o -o test
Makefile
Makefile就像掌握自动化工厂的控制系统,它能让你从重复的编译操作中解放出来,专注于核心代码的开发。随着项目规模扩大,这种自动化优势会越发明显。
# 编译器配置
CC = gcc
CFLAGS = -Wall# 最终目标:依赖项
app: main.o utils.o$(CC) $(CFLAGS) -o $@ $^# 各文件的生成规则
main.o: main.c$(CC) $(CFLAGS) -c $<utils.o: utils.c$(CC) $(CFLAGS) -c $<# 清理伪目标
.PHONY: clean
clean:rm -f *.o app
常见问题
• “静态库(.a)和动态库(.so)的区别?”
(答:静态库编译时打包进程序,动态库运行时加载)
二、调试与工具
-
GDB调试三板斧
gcc -g test.c -o test # 编译时加调试信息 gdb ./test # 启动调试 (gdb) break main # 设断点 (gdb) run # 运行 (gdb) print x # 查看变量
-
内存检测神器Valgrind
valgrind --leak-check=full ./program # 检测内存泄漏
-
系统资源监控命令
top -H # 查看线程级别CPU使用
iotop # 监控磁盘IO
strace -p 进程ID # 追踪系统调用
perf stat ./program # 性能分析
4. C语言多线程编程
线程是程序执行的最小单元,一个进程可以包含多个线程,共享内存空间。
为什么需要多线程?
• 提升性能(多核CPU并行计算)
• 提高响应速度(例如:后台任务不阻塞UI)
• 资源复用(共享内存,通信成本低)
一、 线程基础操作
#include <pthread.h>// 线程任务函数(必须返回void*,参数为void*)
void* thread_func(void* arg) { int* num = (int*)arg; // 可接收外部参数printf("Thread received: %d\n", *num);return NULL;
}int main() {pthread_t tid; // 线程IDint arg = 42;// 创建线程:参数说明// 1. &tid : 存储线程ID// 2. NULL : 线程属性(默认属性)// 3. thread_func : 线程要执行的函数// 4. &arg : 传递给函数的参数if (pthread_create(&tid, NULL, thread_func, &arg) != 0) {perror("Thread create failed");}// 等待线程结束:避免主线程先退出pthread_join(tid, NULL); return 0;
}
关键点:
• 参数传递:通过void*
实现任意类型参数传递
• pthread_join
:阻塞等待线程结束,类似"收尸"操作
• 编译命令:gcc -o demo demo.c -lpthread
(必须链接pthread库)
二、四大同步机制(对比+场景)
机制 | 类比场景 | 适用场景 | API注意事项 |
---|---|---|---|
互斥锁 | 厕所单间(一次一人) | 全局变量修改、文件写入 | 必须成对使用,避免死锁 |
条件变量 | 餐厅叫号系统 | 生产者-消费者、任务队列通知 | 必须搭配互斥锁使用 |
信号量 | 停车场空位计数器 | 连接池控制、流量限制 | 可跨进程使用(需设置参数) |
读写锁 | 图书馆阅览室规则 | 配置文件读取(多读少写) | 写锁优先级通常更高 |
三、调试多线程程序(实战技巧)
GDB高级操作
# 1. 启动调试
gdb -p 进程ID# 2. 查看所有线程
(gdb) info threadsId Target Id Frame 1 Thread 0x7f... main() at demo.c:10
* 2 Thread 0x7f... thread_func() at demo.c:5# 3. 切换线程并检查变量
(gdb) thread 2
(gdb) print counter# 4. 设置观察点(监控变量变化)
(gdb) watch counter# 5. 检测死锁:观察线程阻塞位置
Valgrind检测数据竞争
valgrind --tool=helgrind ./demo
四、生产者-消费者模型
想象一个快递仓库:
• 生产者是不断进货的卡车(往仓库放包裹)
• 消费者是快递员(从仓库取包裹)
• 缓冲区就是仓库货架(临时存放包裹)
如果仓库满的时候卡车还在硬塞,或者仓库空了快递员还在空等,都会导致效率低下。这个模型就是要让生产者和消费者高效协作,既不浪费资源,也不出现拥堵。
生产者-消费者模型使用场景:
- 消息队列系统:多个服务实例通过缓冲区通信
- 日志收集系统:日志生产者与落盘线程解耦
- 线程池任务调度:任务队列作为缓冲区,工作线程作为消费者
- 网络爬虫:URL生产者与网页下载器配合
1. 基本数据结构
// 共享缓冲区:固定大小的"货架"
int buffer[BUFFER_SIZE];
int count = 0; // 当前货架上的包裹数量// 互斥锁:相当于仓库大门的钥匙,每次只能一人操作货架
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 两个信号灯:
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER; // 绿灯:货架不空
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER; // 绿灯:货架不满
2. 生产者工作流程
void* producer() {for (int i=0; ;i++) {// 步骤1:先拿仓库钥匙(互斥锁)pthread_mutex_lock(&mutex);// 步骤2:检查货架是否已满(用while不用if的关键原因!)// while循环保证:被唤醒后必须重新检查条件while (count == BUFFER_SIZE) { // 货架满时,挂起等待"货架不满"信号(自动释放钥匙)pthread_cond_wait(¬_full, &mutex);}// 步骤3:放包裹到货架buffer[count++] = i;// 步骤4:点亮"货架不空"绿灯(通知快递员可以取货)pthread_cond_signal(¬_empty);// 步骤5:归还钥匙pthread_mutex_unlock(&mutex);}
}
3. 消费者工作流程
void* consumer() {while (1) {// 步骤1:拿钥匙pthread_mutex_lock(&mutex);// 步骤2:检查货架是否为空while (count == 0) {// 货架空时,挂起等待"货架不空"信号pthread_cond_wait(¬_empty, &mutex);}// 步骤3:取包裹int item = buffer[--count];// 步骤4:点亮"货架不满"绿灯(通知卡车可以补货)pthread_cond_signal(¬_full);// 步骤5:归还钥匙pthread_mutex_unlock(&mutex);// 步骤6:处理包裹(在锁外执行,避免阻塞其他线程)printf("Consumed: %d\n", item);}
}
4. 性能优化:环形队列
int front = 0, rear = 0; // 环形队列指针// 生产者放货
buffer[rear] = item;
rear = (rear + 1) % BUFFER_SIZE;// 消费者取货
int item = buffer[front];
front = (front + 1) % BUFFER_SIZE;
优势对比:
方式 | 数组实现 | 环形队列 |
---|---|---|
空间利用 | 需要数据搬移 | 重复利用空位 |
时间复杂度 | 插入/删除 O(n) | 插入/删除 O(1) |
适用场景 | 小数据量 | 高频生产消费场景 |