欢迎光临
我们一直在努力

完整且详细的C语言教程,从入门到精通(2.2)--C语言关键字(C89标准下的控制语句、储存类型、类型限定、typedef sizeof、原子操作、内存屏障)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 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 这种技巧。

  • 用 for 模拟 while 的风格
  • // 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 有三种常见形式,容易混淆:
  • 写法含义能改指针指向能改指向的值
    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; // ❌ 错误

  • 修饰函数参数 场景1:防止函数内部意外修改传入的数据
  • 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 的区别

    特性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 下是否做
    省略多余的读取 ❌ 不做
    重排指令顺序 ❌ 不做
    删除无用的赋值 ❌ 不做
    将变量放到寄存器 ❌ 不做(都放内存)

    总结:

    场景volatile 是否必要原因
    -O0 调试模式 作用不大 编译器本来就不做优化,没什么需要阻止的
    -O2 发布模式 必要 编译器会做激进优化,volatile 能阻止错误优化
    跨优化级别移植 推荐始终加上 保证代码在任何优化级别下行为一致

    2.4 原子性与原子操作

    原子性:一个操作要么全部完成,要么完全不执行,不会出现"执行到一半"的状态。 原子操作:具有原子性的操作,即不可中断的、最小单位的操作。 一个具体的交错场景 假设 counter 初始值是 10,理想结果应该是 12(两个线程各加1)。但如果没有原子性,可能发生以下交错:

    时间线程1线程2counter 的值
    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):
  • 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;
    }

  • typedef vs #define
  • 特性typedef#define
    处理阶段 编译期 预处理期(文本替换)
    作用域 遵循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(括号可省略)

  • 常见类型的 sizeof 大小
  • #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;
    }

  • sizeof 是编译时计算(大多数情况)
  • 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)

  • sizeof 的常见陷阱 陷阱1:对字符串字面量
  • 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 获取动态分配的大小

    如果你看到这里,就请给主播点一个关注吧!主播是一个马上粉丝量超10的嵌入式内容主播。谢谢!

    赞(0)
    未经允许不得转载:171主机测评 » 完整且详细的C语言教程,从入门到精通(2.2)--C语言关键字(C89标准下的控制语句、储存类型、类型限定、typedef sizeof、原子操作、内存屏障)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址