生命周期的契约

在C语言中,内存管理是程序员的责任。堆内存(Heap)的生命周期从malloc开始,到free结束。如果违背了“谁申请,谁释放”的契约,或者在内存释放后继续引用,就会引发严重的运行时问题。

问题:指针的三大杀手

什么是内存泄露?在嵌入式系统中它为什么比在PC上更致命?

内存泄露:

  • 本质:申请了内存却丢失了指向它的指针,导致无法释放。

  • 危害:在PC上,程序退出后OS会回收内存,泄露可能不明显。但在嵌入式或服务器上长运行程序中,泄露是累积额。随着时间推移,可用内存耗尽,最终导致malloc返回NULL,程序崩溃。

  • 检测与避免:

    • 工具:使用 valgrind或AddressSanitizer(ASan)进行动态检测。

    • 设计:遵循“成对出现”原则,init对应deinit。在复杂系统中,明确资源的“所有权”(Ownership),即明确哪个模块负责释放。

野指针(Wild Pointer)和悬空指针(Dangling Pointer)有什么区别?

  • 野指针:指针从未被初始化,指向一个随机地址。

    • 对策:定义指针时立即初始化为NULL。
  • 悬空指针:指针指向的内存已经被释放(free),但指针变量本身还保留着原来的地址值。

    • 对策:free(p)之后,立即执行 p = NULL。
  • 区别:野指针是“不知道指向哪”,悬空指针是“指向的地方已经不属于我了”。

越界与未定义行为(UB)——看不见的炸弹

C语言的“信任”机制

C语言为了追求极致的效率,放弃了运行时的边界检查。它信任程序员不会越界。一旦越界,C标准不再保证程序的行为,这就是未定义行为。编译器可以针对UB进行激进优化,导致程序表现出完全不符合逻辑的结果。

问题:为什么程序有时“不崩溃”?

int arr[5]; arr[6] = 10; 为什么有时候程序能正常运行,有时候却崩溃?

越界的真相:

  • 现象:访问arr(第6个元素)是越界的。

  • 为什么不崩:CPU和OS通常以“页”(Page,如4KB)为单位管理内存。如果你的数组后面紧挨着的内存地址仍然属于当前进程的合法页,CPU就不会触发段错误。

  • 后果:可能意外修改了另一个变量的值,或者破坏了堆的元数据。这种错误及其隐蔽,可能在很久之后才导致崩溃。

  • 本质:这是定性的未定义行为。

int i = 1;i = i++; 的结果是多少?为什么?

序列点与UB:

  • 结果:不确定。可能是1,也可能是2,甚至更多。

  • 原理:i++包含“读取i”,“加1”,“写回i”三个动作。i = ...也包含“读取右边”,“写回左边”。C标准没有规定这些动作在同一个表达式中的执行顺序(序列点)。

  • 教训:永远不要在同一表达式中多次修改同一个变量。编译器可能会根据你的代码逻辑优化,导致结果完全不可预测。

栈的极限——栈溢出

栈的脆弱性

栈(Stack)是由编译器自动管理的内存区域,用于存储局部变量和函数调用信息。栈的空间通常很小(Linux 下默认几MB,嵌入式可能仅几KB)。

问题:什么时候会爆战

什么是栈溢出?除了死循环递归,还有什么情况会导致栈溢出?

  • 定义:当函数调用层级过深,或者局部变量过大,超过了栈的分配上限,就会覆盖相邻的内存(通常是栈帧的返回地址),导致程序崩溃。

  • 常见场景:

    • 无限递归:没有终止条件。

    • 大数组:在函数内定义char buffer[1024*1024];。

  • 嵌入式特别注意:在RTOS中,每个任务的栈大小是固定的。如果局部变量过大,极易发生Hard Fault。

  • 对策:大内存申请使用malloc(堆),或者将其定义为全局变量(数据段)。

并发与volatile——时间的陷阱

可见性与原子行

在多线程或中断环境中,变量的值可能在任何时刻被修改。编译器为了优化速度,可能会将变量缓存在寄存器中,导致主存中的值更新不及时。

问题:volatitle 是万能的吗?

volatitle关键字的作用是什么?它能保证线程安全吗?

  • 作用:volatitle告诉编译器“不要优化这个变量”。每次使用该变量时,必须从内存中重新读取,而不是使用寄存器中的缓存值。

  • 局限性:volatitle只保证可见性,不保证原子性。

    • 例子:counter++操作包含“读-改-写”三步。如果两个线程同时执行,即使加了volatitle,结果也会出错(数据竞争)。
  • 结论:线程安全需要互斥锁或原子操作。

中断服务函数中修改全局变量,主循环能立刻看到吗?

在中断和主循环共享变量的场景下,必须使用 volatitle。否则,编译器可能会优化掉主循环中的判断逻辑(因为它认为该变量在循环内不会修改),导致程序逻辑死锁。