提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- C89标准下的控制语句(12个)、存储类型(4个)、类型限定(2个)、其他(2个)
- 一、控制语句(12个)
-
- 1. 条件语句(4个)
-
- 1.1 if — 如果条件成立,则执行
- 1.2 else — 否则(与if配对)
- 1.3 switch — 多分支选择(基于整数值)
- 1.4 case — switch中的分支标签(见上例)
- 1.5 default — switch中的默认分支(见上例)
- 2. 循环语句(3个)
-
- 2.1 while — 当型循环(先判断,后执行)
- 2.2 do — 直到型循环(先执行,后判断,至少执行一次)
- 2.3 for — 计数型循环(最常用)
-
- 2.3.1 for 循环的灵活性
- 2.3.1 i++与++i的区别
- 3. 跳转语句(4个)
-
- 3.1 break — 跳出当前循环或switch
- 3.2 continue — 跳过本次循环剩余部分,进入下一次迭代
- 3.3 goto — 无条件跳转(谨慎使用)
- 3.4 return — 从函数返回
- 二、存储类型(4个):extern static auto register
-
- 1.extern —— 外部链接
- 2.static —— 静态存储期(最常用、最重要)
- 3.auto —— 自动存储期
- 4.register —— 寄存器变量(已过时/弃用)
- 三、类型限定(2个)
-
- 1.const —— 只读,不可修改
-
- 1.1 const 的使用场景
- 1.1 const 与 #define 的区别
- 2.volatile —— 易变的,禁止优化
-
- 2.1 编译器优化演示
- 2.2 volatile的使用场景
- 2.3 volatile 的限制
- 2.4 原子性与原子操作
- 2.5 内存屏障
- 四、其他(2个)
-
- 1.typedef —— 类型重命名(别名)
- 2.sizeof —— 计算大小(编译时运算符)
- 如果你看到这里,就请给主播点一个关注吧!主播是一个马上粉丝量超10的嵌入式内容主播。谢谢!
C89标准下的控制语句(12个)、存储类型(4个)、类型限定(2个)、其他(2个)
控制语句(12个): 条件:if else switch case default 循环:while do for 跳转:break continue goto return 存储类型(4个):extern static auto register 类型限定(2个):const volatile 其他(2个):typedef sizeof
| 控制语句(12个) | if else switch case default while do for break continue goto return |
| 存储类型(4个) | extern static auto register |
| 类型限定(2个) | const volatile |
| 其他(2个) | typedef sizeof |
一、控制语句(12个)
| 条件 | if else switch case default |
| 循环 | while do for |
| 跳转 | break continue goto return |
1. 条件语句(4个)
1.1 if – 如果条件成立,则执行
语法:
if (条件表达式) {
// 条件为真(非0)时执行
}
示例:
int score = 85;
if (score >= 60) {
printf("及格了\\n");// score是85,大于60,所以会执行这一行代码
}
1.2 else — 否则(与if配对)
语法:
if (条件) {
// 真时执行
} else {
// 假时执行
}
示例:
int age = 17;
if (age >= 18) {
printf("成年人\\n");//age是17,小于18,所以不会执行这一行代码
} else {
printf("未成年人\\n");//age是17,小于18,所以会执行这一行代码
}
else if 多分支:
int score = 75;
if (score >= 90) {
printf("优秀\\n");// score是75,小于90,所以不会执行这一行代码
} else if (score >= 60) {
printf("及格\\n");// score是75,大于60,所以会执行这一行代码
} else {
printf("不及格\\n");
}
1.3 switch — 多分支选择(基于整数值)
语法:
switch (整型表达式) {
case 常量1:
语句;
break;
case 常量2:
语句;
break;
default:
语句;
}
示例:
int day = 3;
switch (day) {
case 1:
printf("Monday\\n");
break;
case 2:
printf("Tuesday\\n");
break;
case 3:
printf("Wednesday\\n");// day是3,所以不会执行这一行代码
break;
default:
printf("其他日子\\n");
}
1.4 case — switch中的分支标签(见上例)
1.5 default — switch中的默认分支(见上例)
switch 高级技巧(穿透):
int month = 2;
int days;
switch (month) {
case 1: case 3: case 5: case 7:
case 8: case 10: case 12:
days = 31;
break;
case 4: case 6: case 9: case 11:
days = 30;
break;
case 2:
days = 28;
break;
default:
days = 0;
}
printf("%d 月有 %d 天\\n", month, days);
注意:忘记 break 会导致“穿透”(继续执行下一个case)。
2. 循环语句(3个)
2.1 while – 当型循环(先判断,后执行)
语法:
while (条件) {
// 条件为真时重复执行
}
示例:计算1到100的和
int i = 1, sum = 0;
while (i <= 100) {
sum += i;
i++;
}
printf("sum = %d\\n", sum); // 5050
2.2 do – 直到型循环(先执行,后判断,至少执行一次)
语法:
do {
// 至少执行一次
} while (条件);
示例:输入密码直到正确
int password;
do {
printf("请输入密码(6位数字): ");
scanf("%d", &password);
} while (password != 123456);
printf("登录成功!\\n");
| while | 可能一次都不执行 |
| do-while | 至少执行一次 |
2.3 for — 计数型循环(最常用)
语法:
for (初始化; 条件; 更新) {
// 循环体
}
示例:遍历数组
int arr[] = {10, 20, 30, 40, 50};
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\\n", i, arr[i]);
}
for 循环的执行流程是:
1.初始化(int i = 0) 2.判断条件(i < 5) 3.条件为真,执行循环体({ }) 4.执行增量表达式(i++) 5.回到第 2 步 … 6.直到条件为佳=假,退出for循环
2.3.1 for 循环的灵活性
// 经典写法:全部省略
for (;;) {
// 需要 break 或 return 退出
if (condition) break;
}
// 等价的 while 循环
while (1) {
if (condition) break;
}
for (;;)在底层通常不会产生条件判断指令(直接生成无条件跳转),比 while(1) 在某些编译器上更高效。
// 遍历二维数组时同时控制行和列
for (int row = 0, col = 0; row < rows && col < cols; row++, col++) {
printf("diagonal[%d][%d] = %d\\n", row, col, matrix[row][col]);
}
// 同时操作两个指针
for (char *src = str, *dst = buffer; *src; src++, dst++) {
*dst = toupper(*src);
}
*dst = '\\0';
注意:用逗号运算符时,所有变量声明类型必须相同(都是 int),不能混用 int i=0, double x=0.0。
// 使用外部初始化的变量
int i = 0;
for (; i < n; i++) {
process(data[i]);
}
// 配合链表遍历
Node *p = head;
for (; p != NULL; p = p->next) {
printf("%d ", p->data);
}
// 读取输入直到遇到特定值
int val;
for (;;) {
scanf("%d", &val);
if (val == –1) break;
sum += val;
}
// 在循环体内部完成修正
for (int i = 0; i < 10; ) {
// 复杂逻辑控制步长
if (some_condition) {
i += 2;
} else {
i++;
}
}
// 手动管理迭代器
for (Iterator it = begin(); it != end(); ) {
if (should_remove(it)) {
it = erase(it); // erase 返回下一个有效位置
} else {
++it;
}
}
// 纯粹的无限循环,配合内部跳转
for (;;) {
char cmd = get_command();
switch (cmd) {
case 'q': return; // 退出函数
case 'r': reset(); break;
case 'p': print(); break;
}
}
// 同时做两件事:打印当前值并累积
for (int i = 0; i < 10; i++, printf("i=%d, sum=%d\\n", i, sum), sum += i) {
// 注意:printf 和 sum+=i 的执行顺序取决于写法
}
// 不推荐这样写,可读性差
建议:修正表达式里只放简单的递增/递减操作,复杂的逻辑放到循环体内。
// 查找数组中第一个满足条件的元素
int target_pos = –1;
for (int i = 0; i < size; i++) {
if (arr[i] == target) {
target_pos = i;
break;
}
}
// 简化写法(不推荐,可读性差)
int pos;
for (pos = 0; pos < size && arr[pos] != target; pos++);
// 如果 pos == size 则未找到
// 正向遍历
for (int i = 0; i < N; i++) { }
// 反向遍历
for (int i = N – 1; i >= 0; i—) { }
// 反向遍历(无符号整数陷阱)
for (unsigned int i = N; i > 0; i—) { } // 安全,不会下溢
注意:用 unsigned int 时,i >= 0 永远为真,所以反向遍历要用 i > 0 这种技巧。
// while 风格
int i = 0;
while (i < 10) {
// …
i++;
}
// 等价 for 风格(把修正写在循环体内)
int i = 0;
for (; i < 10; ) {
// …
i++;
}
2.3.1 i++与++i的区别
i++ 是“后置自增”:先使用 i 的当前值,然后 i 自增 1。 ++i 是“前置自增”:先让 i 自增 1,然后使用 i 的新值。 无论前置还是后置,最终 i 自己都会增加 1。区别在于这个表达式的结果值(即“使用”时的值)不同。
// 这三个循环完全等价
for (int i = 0; i < 10; i++) { }
for (int i = 0; i < 10; ++i) { }
for (int i = 0; i < 10; i = i + 1) { }
详情解释参考2.3 for 循环的执行流程
// 后置:先用 i 的值,再自增
for (int i = 0; i < 5; ) {
printf("%d ", i++); // 输出: 0 1 2 3 4
}
// 前置:先自增,再用新值
for (int i = 0; i < 5; ) {
printf("%d ", ++i); // 输出: 1 2 3 4 5
}
int a, b, i = 0;
a = i++; // a = 0, i = 1
b = ++i; // i = 2, b = 2
// 危险代码!不要这样写
int i = 0;
int arr[] = {i++, ++i, i++}; // 未定义行为
3. 跳转语句(4个)
3.1 break — 跳出当前循环或switch
作用: 在循环中:立即终止整个循环 在switch中:跳出switch结构
示例:查找元素
int arr[] = {3, 7, 2, 9, 5};
int target = 2;
int found = 0;
for (int i = 0; i < 5; i++) {
if (arr[i] == target) {
printf("找到了!索引: %d\\n", i);
found = 1;
break; // 找到后立即退出循环
}
}
if (!found) {
printf("未找到\\n");
}
break 只能跳出一层循环:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break; // 只跳出内层循环,外层继续
}
printf("%d,%d ", i, j);
}
printf("\\n");
}
// 输出:0,0 0,1 0,2
// 1,0
// 2,0 2,1 2,2
3.2 continue — 跳过本次循环剩余部分,进入下一次迭代
示例:输出奇数
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue; // 跳过偶数
}
printf("%d ", i);
}
// 输出:1 3 5 7 9
break vs continue:
for (int i = 1; i <= 5; i++) {
if (i == 3) {
break; // 输出:1 2(遇到3整个循环结束)
}
printf("%d ", i);
}
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 输出:1 2 4 5(跳过3继续)
}
printf("%d ", i);
}
3.3 goto — 无条件跳转(谨慎使用)
语法:
goto 标签名;
...
标签名:
语句;
示例:跳出多层嵌套循环
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
for (int k = 0; k < 10; k++) {
if (i == 5 && j == 5 && k == 5) {
goto found; // 一次性跳出所有循环
}
}
}
}
found:
printf("找到了目标位置\\n");
错误处理中的经典用法:
FILE *fp = NULL;
char *buffer = NULL;
fp = fopen("data.txt", "r");
if (fp == NULL) goto cleanup;
buffer = malloc(1024);
if (buffer == NULL) goto cleanup;
cleanup:
if (buffer) free(buffer);
if (fp) fclose(fp);
在本段代码中,如果fp = NULL会导致直接跳转到cleanup:,而导致buffer = malloc(1024);if (buffer == NULL) goto cleanup;不执行。
警告:
goto 滥用会导致“面条代码”(spaghetti code)
现代编程中,尽量用 break、continue、函数调用替代
仅在统一清理资源或跳出深层嵌套时谨慎使用
3.4 return — 从函数返回
语法:
return; // 无返回值函数(void类型)
return 表达式; // 返回表达式的值
return 返回值类型基本语法:
返回值类型 函数名(参数列表) {
// 函数体
return 表达式; // 表达式的类型应与返回值类型匹配
}
示例:
// 无返回值
void print_hello() {
printf("Hello\\n");
return; // 可省略,函数结束时会自动返回
}
// 有返回值
int max(int a, int b) {
if (a > b)
return a;
else
return b;//返回值类型为int型
}
// 提前返回(守卫语句)
int divide(int a, int b) {
if (b == 0) {
printf("错误:除数为0\\n");
return –1; // 提前返回错误码
}
return a / b;
}
int main() {
int m = max(10, 20);
printf("max = %d\\n", m); // 20
int result = divide(10, 0);
if (result == –1) {
printf("计算失败\\n");
}
return 0; // main函数返回0表示程序正常退出
}
二、存储类型(4个):extern static auto register
1.extern —— 外部链接
核心特征 用途:声明一个变量或函数在其他文件中定义 不分配内存:只是声明,不是定义 作用域:跨文件共享全局变量/函数 用法示例 global.h
#ifndef GLOBAL_H
#define GLOBAL_H
extern int shared_counter; // 声明(不分配内存)
extern void increment_counter(void);// 修饰函数时可省略extern,例如只写void increment_counter(void);
#endif
注:在.h文件中函数的声明默认就是 extern,可以省略。
global.c
#include "global.h"
int shared_counter = 0; // 定义(分配内存)
void increment_counter(void) {
shared_counter++;
}
main.c
#include <stdio.h>
#include "global.h"
int main() {
printf("初始值: %d\\n", shared_counter); // 0
increment_counter();
increment_counter();
printf("调用后: %d\\n", shared_counter); // 2
return 0;
}
2.static —— 静态存储期(最常用、最重要)
static 有两个完全不同的用途:修饰局部变量 和 修饰全局变量/函数。
生命周期(Lifecycle)指的是变量从诞生(分配内存)到消亡(释放内存)的时间段
| 普通局部变量 (int a;) | 自动存储期 | 程序执行到变量定义处 | 程序退出其所在的花括号 {}(函数/代码块) |
| static 局部变量 | 静态存储期 | 程序启动时(或第一次执行到定义前) | 程序结束时 |
关键区别:static 局部变量不会在函数返回时销毁,它会一直"活着",保留它最后一次被修改的值,直到整个程序运行结束。 作用域(Scope)指的是变量在源代码中可以被直接访问的区域
| 普通局部变量 (int a;) | 块作用域:从定义点到其所在的最内层 {} 结束。 |
| static 局部变量 | 块作用域(完全相同):也只能在定义它的函数/代码块内部被访问。 |
关键区别:static 并没有让局部变量变成全局的。函数外部和其他函数依然看不见它,无法直接使用。
#include <stdio.h>
void counter() {
static int count = 0; // 只初始化一次,再次调用时保留上次的值
count++;
printf("函数被调用了 %d 次\\n", count);
}
int main() {
counter(); // 输出:函数被调用了 1 次
counter(); // 输出:函数被调用了 2 次
counter(); // 输出:函数被调用了 3 次
return 0;
}
对比普通局部变量:
void normal_counter() {
int count = 0; // 每次调用都重新初始化为 0
count++;
printf("%d\\n", count); // 永远输出 1
}
file1.c
static int hidden_var = 100; // 只能在本文件访问
static void hidden_func() { // 只能在本文件调用
printf("这是内部函数\\n");
}
int public_var = 200; // 其他文件可以通过 extern 访问
void public_func() {
printf("这是公开函数\\n");
hidden_func(); // 内部可以调用隐藏函数
}
file2.c
extern int public_var; // 声明外部变量
extern void public_func(); // 声明外部函数
// extern int hidden_var; // 错误!hidden_var 是 static 的,无法访问
int main() {
public_func(); // OK
printf("%d\\n", public_var); // OK
// hidden_func(); // 错误!无法访问
return 0;
}
实际应用场景 单例模式:函数内 static 变量保存唯一实例 模块封装:用 static 隐藏模块内部实现细节 常量字符串:函数内 static const char* msg = “error” 避免重复构造
3.auto —— 自动存储期
核心特征 生命周期:进入代码块时创建,退出时销毁 作用域:块作用域(通常是大括号 {} 内) 存储位置:栈(stack) 默认行为:局部变量不加任何关键字时,默认就是 auto
用法
#include <stdio.h>
int main() {
auto int x = 10; // 完全等价于 int x = 10;
int y = 20; // 和上面的写法一模一样
{
int z = 30; // 这也是 auto 变量
printf("z = %d\\n", z);
}
// printf("%d\\n", z); // 错误!z 已销毁
return 0;
}
为什么几乎没人用 auto? 因为 auto 是默认行为,写不写都一样。C++11 之后 auto 被赋予了"自动类型推导"的新含义,但在C语言中它仍表示这个过时的存储类说明符。
注意:C语言中的 auto 和 C++11 的 auto(自动类型推导)完全不同。 在 C 语言中,auto 只能修饰局部变量,但它是多余的(默认就是),所以几乎不用。 在 C++11 之后,auto 变成了类型推导关键字,可以修饰局部变量、函数返回值、函数参数等,但依然不能修饰全局变量。
4.register —— 寄存器变量(已过时/弃用)
核心特征 意图:建议编译器将变量存储在CPU寄存器中,以加快访问速度 限制:不能取地址(& 操作符) 现状:现代编译器优化能力极强,register 通常被忽略,仅作为提示
用法
#include <stdio.h>
int main() {
register int counter = 0; // 建议放在寄存器中
for (counter = 0; counter < 1000000; counter++) {
// 频繁使用的循环计数器
}
// int *p = &counter; // 错误!不能对 register 变量取地址
return 0;
}
为什么不推荐使用? 编译器自动优化比程序员手动标记更聪明 CPU寄存器数量有限,编译器可能忽略你的建议 C++17 已正式废弃 register 关键字,C语言中虽保留但无实际作用
三、类型限定(2个)
1.const —— 只读,不可修改
核心概念 const 修饰的变量是只读的,其值在初始化后不能被修改。任何试图修改 const 变量的行为都会导致编译错误。
1.1 const 的使用场景
const int MAX_SIZE = 100; // 或 int const MAX_SIZE = 100;
MAX_SIZE = 200; // ❌ 编译错误:不能修改 const 变量
使用场景:定义常量(替代 #define,具有类型检查)
const double PI = 3.14159;
const char ERROR_MSG[] = "Something went wrong";
| const int *p; | 指向常量整数的指针 | ✅ 可以 | ❌ 不可以 |
| int * const p; | 常量指针,指向整数 | ❌ 不可以 | ✅ 可以 |
| const int * const p; | 指向常量的常量指针 | ❌ 不可以 | ❌ 不可以 |
记忆技巧:const 在 * 左边 → 指向的值不可变;const 在 * 右边 → 指针本身不可变。
示例:
int a = 10, b = 20;
// 情况1:指向常量的指针
const int *p1 = &a;
// *p1 = 30; // ❌ 错误:不能通过 p1 修改 a 的值
p1 = &b; // ✅ 正确:p1 可以指向其他变量
// 情况2:常量指针
int * const p2 = &a;
*p2 = 30; // ✅ 正确:可以修改 a 的值
// p2 = &b; // ❌ 错误:p2 永远指向 a,不能改变
// 情况3:指向常量的常量指针
const int * const p3 = &a;
// *p3 = 30; // ❌ 错误
// p3 = &b; // ❌ 错误
void print_array(const int arr[], int size) {
// arr[0] = 100; // ❌ 编译错误:不能修改
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]); // ✅ 只读访问
}
}
int main() {
int nums[] = {1, 2, 3};
print_array(nums, 3); // 安全:print_array 不会修改 nums
return 0;
}
场景2:修饰指针参数
// 函数不会修改字符串内容
size_t my_strlen(const char *str) {
size_t len = 0;
while (str[len] != '\\0') {
len++;
}
return len;
}
// 函数不会修改指向的结构体
void draw_point(const struct Point *p) {
printf("(%d, %d)\\n", p->x, p->y);
// p->x = 100; // ❌ 错误
}
// 通常放在头文件中,整个工程共享
const int VERSION_MAJOR = 1;
const int VERSION_MINOR = 0;
// 字符串常量本质是 const char[],修改会导致未定义行为
const char *msg = "Hello"; // 正确
char *msg2 = "World"; // ⚠️ 危险:旧代码允许,但实际不应修改
// msg2[0] = 'w'; // 未定义行为(可能崩溃)
1.1 const 与 #define 的区别
| 处理阶段 | 编译期 | 预处理期(文本替换) |
| 类型检查 | ✅ 有 | ❌ 无 |
| 调试信息 | ✅ 有符号 | ❌ 无符号 |
| 内存占用 | 通常分配内存 | 不分配(直接替换) |
| 作用域 | 遵循C作用域规则 | 全局(直到 #undef) |
建议:C语言中定义常量优先使用 #define(不占内存、效率高),但 const 在需要类型安全和调试时更好。
2.volatile —— 易变的,禁止优化
核心概念 volatile 告诉编译器:这个变量的值可能在程序无法察觉的情况下被改变。因此,编译器不能对该变量的访问进行优化(如缓存到寄存器、重排序等),每次使用都必须从内存中重新读取。
2.1 编译器优化演示
假设有以下代码:
int flag = 1;
void test() {
while (flag) {
// 循环体
}
}
开启优化 -O2 后,编译器可能优化为:
void test() {
if (flag) {
while (1) {
// 无限循环
}
}
}
因为编译器认为 flag 在循环内不会改变,所以只读取一次。但实际 flag 可能被硬件或其它线程修改,导致逻辑错误。
加上 volatile 后:
volatile int flag = 1;
void test() {
while (flag) {
// 每次循环都重新读取 flag 的内存值
}
}
2.2 volatile的使用场景
// 假设某个硬件状态寄存器地址为 0x4000 0000
volatile int *status_reg = (volatile int *)0x40000000;
while ((*status_reg & 0x01) == 0) {
// 等待硬件状态位变为1
// 如果不加 volatile,编译器可能优化成:
// 只读取一次 status_reg,然后无限循环
}
如果没有使用volatile相当于:
int *status_reg = (int *)0x40000000;
int temp = *status_reg; // 只读一次
while ((temp & 0x01) == 0) {
// 无限循环!因为 temp 永远不变
}
结果:如果硬件一开始 bit0 = 0,程序就会永远卡死,即使后来硬件把它变成了 1。 2. 多线程共享变量(无锁编程)
volatile int flag = 0;
// 线程1
void thread1() {
// 执行一些计算…
flag = 1; // 通知线程2
}
// 线程2
void thread2() {
while (flag == 0) {
// 等待 flag 变为 1
// volatile 保证每次循环都重新读取 flag
}
printf("被通知了!\\n");
}
在此段代码中,thread1() 可能会随时改变flag的值,进而影响while (flag == 0)的执行。如果没有volatile进行修饰,while (flag == 0)相当于if (flag) {while (1) {// 无限循环 },只会读取一次flag的值,而导致线程2的flag无法读取到线程1flag值的改变。 注意:volatile 不保证原子性,复杂场景应使用互斥锁或原子操作。 3. 信号处理函数中访问的变量
#include <signal.h>
#include <stdio.h>
volatile sig_atomic_t interrupted = 0; // sig_atomic_t 是原子类型
void handle_signal(int sig) {
interrupted = 1; // 信号处理函数中修改
}
int main() {
signal(SIGINT, handle_signal);
while (!interrupted) {
// 正常工作
}
printf("收到中断信号,退出\\n");
return 0;
}
这段代码的完整流程 注册阶段(main 函数中):
signal(SIGINT, handle_signal);
这行代码告诉操作系统:"当进程收到 SIGINT 信号时,请帮我调用 handle_signal 这个函数。"
等待阶段(main 函数中):
while (!interrupted) {
// 正常工作
}
程序在这里循环,什么事都不做(或者做其他工作)。
信号触发(用户操作): 你在终端按下 Ctrl+C 操作系统检测到这个按键 操作系统向正在运行的程序发送 SIGINT 信号
异步调用(操作系统内核): 操作系统看到进程收到了 SIGINT 信号 操作系统查阅之前注册的信号处理函数(通过 signal 注册的) 操作系统暂停主程序当前正在执行的代码(哪怕是 while 循环的中间) 操作系统调用 handle_signal(SIGINT) handle_signal 执行,将 interrupted 改为 1 handle_signal 返回 操作系统恢复主程序的执行
退出阶段(main 函数中): 主循环继续执行 再次检查 while (!interrupted) 这次 interrupted 已经是 1,循环退出 打印 “收到中断信号,退出”
4.const 和 volatile 的组合使用 两者可以同时使用,表示变量是只读的,但可能被外部改变(有点矛盾?实际上很常见)。
场景:只读硬件寄存器
// 硬件状态寄存器:程序只读,但硬件会改变它的值
const volatile uint32_t *hw_status = (const volatile uint32_t *)0x40001000;
uint32_t status = *hw_status; // 每次从内存读取,但不能修改
// *hw_status = 0x01; // ❌ 编译错误:const 禁止写入
场景:只读的、可能被中断修改的变量
const volatile int ticks = 0; // 中断服务程序会修改它,但主程序只读
2.3 volatile 的限制
1.不保证原子性:
volatile int counter;
counter++; // 不是原子操作!可能被中断
2.不保证内存屏障(不限制指令重排序):
volatile int a, b;
a = 1; // 可能被重排序到 b=2 之后
b = 2;
3.仅在禁用优化时有效:调试模式(-O0)下 volatile 作用不大。 volatile 的定位:它是一个优化抑制器。调试模式(-O0)下,编译器本来就不优化,既然 -O0 下编译器什么都不优化,那 volatile 自然没什么需要阻止的。 在 -O0 模式下(默认,无优化),编译器的行为是:
| 省略多余的读取 | ❌ 不做 |
| 重排指令顺序 | ❌ 不做 |
| 删除无用的赋值 | ❌ 不做 |
| 将变量放到寄存器 | ❌ 不做(都放内存) |
总结:
| -O0 调试模式 | 作用不大 | 编译器本来就不做优化,没什么需要阻止的 |
| -O2 发布模式 | 必要 | 编译器会做激进优化,volatile 能阻止错误优化 |
| 跨优化级别移植 | 推荐始终加上 | 保证代码在任何优化级别下行为一致 |
2.4 原子性与原子操作
原子性:一个操作要么全部完成,要么完全不执行,不会出现"执行到一半"的状态。 原子操作:具有原子性的操作,即不可中断的、最小单位的操作。 一个具体的交错场景 假设 counter 初始值是 10,理想结果应该是 12(两个线程各加1)。但如果没有原子性,可能发生以下交错:
| T1 | 读取 counter (10) 到寄存器 | 10 | |
| T2 | 读取 counter (10) 到寄存器 | 10 | |
| T3 | 寄存器 +1 (变成 11) | 10 | |
| T4 | 寄存器 +1 (变成 11) | 10 | |
| T5 | 写回寄存器值 (11) 到内存 | 11 | |
| T6 | 写回寄存器值 (11) 到内存 | 11 |
最终结果:counter = 11 ❌(正确结果应该是 12)。这就是典型的数据竞争,丢失了一次自增操作。问题的根源就在于 counter++ 不是原子操作。
总结对比表
| 原子性 | 操作不可分割,不会执行到一半 | 数据竞争、丢失更新 | counter++ 不是原子的 |
| 原子操作 | 具有原子性的操作 | 安全的并发访问 | std::atomic::operator++ |
| volatile | 禁用编译器优化,强制读内存 | 可见性(不保证原子性) | volatile int flag; |
| 互斥锁 | 保护临界区,一次只允许一个线程进入 | 原子性 + 可见性 | std::mutex, pthread_mutex |
2.5 内存屏障
内存屏障(Memory Barrier,也叫内存栅栏)是一种指令,告诉 CPU 和编译器:在这条指令之前和之后的内存操作,不能越过这条线重排序。 示例:
// 没有屏障的情况
a = 1; // 可能被重排到后面
b = 2; // 可能被重排到前面
// 有屏障的情况
a = 1;
内存屏障(); // 禁止 a=1 和 b=2 越过这条线
b = 2;
// 现在 a=1 一定在 b=2 之前执行
CPU 和编译器为了提高执行效率,可能会打乱代码的执行顺序,只要不改变单线程的最终结果
// 你写的代码
a = 1; // 第1行
b = 2; // 第2行
CPU 实际执行可能是
b = 2; // 先执行第2行
a = 1; // 后执行第1行
因为从单线程角度看,这两行互相独立(不依赖对方的结果),先执行谁结果都一样。但 CPU 可能觉得先执行 b=2 更快(比如缓存命中率更高)。
一个具体例子:重排序导致的 bug 假设两个线程共享变量:
// 初始值
int data = 0;
int ready = 0;
// 线程1(生产者)
void producer() {
data = 42; // 准备数据
ready = 1; // 标记就绪
}
// 线程2(消费者)
void consumer() {
while (ready == 0); // 等待就绪
printf("%d", data); // 期望输出 42
}
如果没有重排序,结果一定是 42。
但如果 CPU 重排序了:
// 线程1实际执行顺序(重排后)
ready = 1; // 先执行!
data = 42; // 后执行!
线程2可能看到 ready == 1,但 data 还没被赋值(还是 0),于是输出 0 ❌ 这就是重排序导致的数据竞争 bug。
嵌入式/内核中的手动屏障
// Linux 内核
smp_mb(); // 多核内存屏障
smp_wmb(); // 写屏障(只限制写操作)
smp_rmb(); // 读屏障(只限制读操作)
// 使用示例
data = 42;
smp_wmb(); // 确保 data 写完成后再写 ready
ready = 1;
总结对比表
| volatile | 禁止编译器优化,强制从内存读写 | 编译器(不限制 CPU) | 硬件寄存器、信号处理变量 |
| 内存屏障 | 限制 CPU/编译器对内存操作的重排序 | 编译器 + CPU | 无锁编程、多线程同步 |
| 原子操作 | 保证读-改-写操作的不可分割性 | CPU 硬件 | 计数器、标志位 |
| 互斥锁 | 保证临界区的互斥执行 | 多个维度 | 复杂共享数据 |
四、其他(2个)
1.typedef —— 类型重命名(别名)
核心概念 typedef 用于为已存在的数据类型创建一个新的名字(别名)。它不创建新类型,只提供更易读/简写的名字。
typedef 原类型 新名字;
typedef unsigned int uint32; // 常用:定义无符号32位整数
typedef unsigned char byte; // 字节类型
uint32 count = 100;
byte data = 0xFF;
为什么这样做? 提高可移植性、自文档化:
// 跨平台时,用 typedef 定义固定大小类型
typedef unsigned long long uint64; // Linux 64位
// typedef unsigned __int64 uint64; // Windows 64位
struct Point {
int x;
int y;
};
struct Point p1; // 必须写 struct
struct Point p2;
用 typedef 简化:
typedef struct {
int x;
int y;
} Point; // 现在 Point 就是类型名
Point p1; // 不需要 struct 关键字
Point p2 = {10, 20};
联合体示例:
typedef union {
int i;
float f;
} Number;
Number n;
n.i = 42;
枚举示例:
typedef enum {
RED,
GREEN,
BLUE
} Color;
Color c = RED;
typedef int* IntPtr; // IntPtr 是 int* 的别名
IntPtr p1, p2; // 等价于 int *p1, *p2;(注意:不是 int *p1, p2)
int a = 10;
p1 = &a;
注意陷阱:
typedef int* IntPtr;
IntPtr p1, p2; // p1 和 p2 都是 int*,正确
// 如果不用 typedef
int* p1, p2; // 只有 p1 是 int*,p2 是 int(容易出错)
// 定义一个函数指针类型:指向参数为 (int, int),返回 int 的函数
typedef int (*Operation)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a – b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }
int main() {
Operation op; // op 是一个函数指针变量
op = add;
printf("%d\\n", op(5, 3)); // 8
op = sub;
printf("%d\\n", op(5, 3)); // 2
// 函数指针数组
int (*ops[])(int, int) = {add, sub, mul, div};
for (int i = 0; i < 4; i++) {
printf("%d\\n", ops[i](10, 2));
}
// 输出: 12, 8, 20, 5
return 0;
}
拓展:指针函数
#include <stdio.h>
int* getCounter() {
static int counter = 0; // 静态变量,生命周期贯穿整个程序
counter++;
return &counter; // ✅ 安全
}
int main() {
int* p = getCounter();
printf("%d\\n", *p); // 输出: 1
p = getCounter();
printf("%d\\n", *p); // 输出: 2
return 0;
}
| 处理阶段 | 编译期 | 预处理期(文本替换) |
| 作用域 | 遵循C作用域规则 | 全局(直到文件结束) |
| 复杂类型 | 可正确处理(如函数指针) | 容易出错 |
| 分号 | 需要 | 不需要 |
对比示例:
#define PTR_INT int*
typedef int* PtrInt;
PTR_INT p1, p2; // 展开为 int* p1, p2; → p2 是 int,不是指针
PtrInt p3, p4; // p3 和 p4 都是 int*
// #define 无法正确处理函数指针(需要多层括号)
#define FuncPtr int(*)(int, int) // 容易出错
typedef int (*FuncPtr)(int, int); // 清晰正确
2.sizeof —— 计算大小(编译时运算符)
核心概念 sizeof 是一个编译时运算符(不是函数!),返回某个类型或变量所占的字节数,结果类型是 size_t(无符号整数)。
sizeof(类型) // 括号必须,用于类型
sizeof 表达式 // 括号可选,用于变量/表达式
示例:
int a = 10;
size_t size1 = sizeof(int); // 4(通常)
size_t size2 = sizeof(a); // 4
size_t size3 = sizeof a; // 4(括号可省略)
#include <stdio.h>
int main() {
printf("char: %zu\\n", sizeof(char)); // 1
printf("short: %zu\\n", sizeof(short)); // 2
printf("int: %zu\\n", sizeof(int)); // 4
printf("long: %zu\\n", sizeof(long)); // 4 或 8
printf("long long: %zu\\n", sizeof(long long)); // 8
printf("float: %zu\\n", sizeof(float)); // 4
printf("double: %zu\\n", sizeof(double)); // 8
printf("指针: %zu\\n", sizeof(void*)); // 8(64位系统)
int arr[10];
printf("数组: %zu\\n", sizeof(arr)); // 40 = 4*10
printf("数组元素: %zu\\n", sizeof(arr[0])); // 4
printf("数组长度: %zu\\n", sizeof(arr)/sizeof(arr[0])); // 10
return 0;
}
int arr[10] = {0};
printf("%zu\\n", sizeof(arr)); // 40(整个数组的大小)
printf("%zu\\n", sizeof(arr)/sizeof(arr[0])); // 10(数组元素个数)
// 注意:当数组作为函数参数时,会退化为指针
void func(int arr[10]) {
// 参数名也叫 arr,在 func 内部遮蔽全局 arr,遵循局部优先原则,这里的参数int arr[10]与全局变量int arr[10]无关
printf("%zu\\n", sizeof(arr)); // 8(指针大小,不是数组大小!)
}
原因:数组作为函数参数时,会"退化"为指针 虽然写了 int arr[10],但编译器会把它当作 int *arr 处理。
// 这三种写法完全等价
void func(int arr[10]) { ... }
void func(int arr[]) { ... }
void func(int *arr) { ... }
所以 sizeof(arr) 计算的是指针的大小,而不是数组的大小。
struct A {
char c; // 1 字节
int i; // 4 字节
};
// 实际大小可能不是 1+4=5,而是 8(内存对齐填充)
struct B {
int i; // 4 字节
char c; // 1 字节
// 后面可能填充 3 字节
};
// 大小也可能是 8
printf("sizeof(struct A) = %zu\\n", sizeof(struct A));
printf("sizeof(struct B) = %zu\\n", sizeof(struct B));
注:“可能是 8” 是因为 C 标准没有强制规定对齐大小,实际大小取决于具体的编译器和平台。在常见的 x86/ARM 桌面平台上,int类型是4个字节,内存对其后默认是 8;但在嵌入式平台或使用打包指令时,可能是 5、6 或其他值。 5. 对指针的行为
int a = 10;
int *p = &a;
char *str = "Hello";
printf("%zu\\n", sizeof(p)); // 8(64位系统)/ 4(32位系统)(指针大小)
printf("%zu\\n", sizeof(a)); // 4(int 类型大小)
printf("%zu\\n", sizeof(str)); // 8(指针大小,不是字符串长度)
printf("%zu\\n", sizeof(*str)); // 1(char 类型大小)
在 64 位系统上,任何指针的大小都是 8 字节。在32位系统上,任何指针的大小都是 4 字节。 注意:sizeof 对字符串指针不会计算字符串长度,要获取长度用 strlen()。对字符串时正常计算字符串长度。
拓展:strlen基本用法
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello";
char *str2 = "World";
char str3[20] = "C Programming";
printf("%zu\\n", strlen(str1)); // 5(H,e,l,l,o)
printf("%zu\\n", strlen(str2)); // 5(W,o,r,l,d)
printf("%zu\\n", strlen(str3)); // 13(C,空格,P,r,o,g,r,a,m,m,i,n,g)
return 0;
}
int a = 10;
size_t s = sizeof(a++); // a++ 不会执行!
printf("%d\\n", a); // 仍然是 10
// 解释:sizeof 只在编译时计算类型大小,不计算表达式
例外:C99 引入的变长数组(VLA) 在运行时计算大小:
int n = 10;
int arr[n];
size_t s = sizeof(arr); // 运行时计算 = n * sizeof(int)
char str[] = "Hello";
printf("%zu\\n", sizeof(str)); // 6(包括末尾的 '\\0')
printf("%zu\\n", strlen(str)); // 5(不包括 '\\0')
陷阱2:对表达式结果类型
short a = 10, b = 20;
printf("%zu\\n", sizeof(a + b)); // 可能是 4(int),不是 2(short)
// 原因:整型提升,a+b 结果转为 int
整型提升是 C 语言中的一个隐式类型转换规则:当使用 short、char 等小于 int 的整数类型进行运算时,它们会被自动提升为 int 类型(或 unsigned int),然后再执行运算。 示例
short a = 10, b = 20;
short c = a + b; // 实际过程:
// 1. a 和 b 被提升为 int
// 2. 执行 int 类型的加法
// 3. 结果(int)再隐式转换回 short
陷阱3:对动态分配的内存
int *p = malloc(100 * sizeof(int));
printf("%zu\\n", sizeof(p)); // 8(指针大小,不是 400)
// 无法通过 sizeof 获取动态分配的大小


