4. GCC 常用编译选项:-O、-Wall、-g、-I 和 -L

GCC 的编译选项,说白了就是你和编译器沟通的「开关」。每个开关控制一个行为,用对了事半功倍,用错了……嗯,我见过不少同事在调试时抓耳挠腮,最后发现只是少了个 -g 选项。

这一章我们聚焦五个最常用的选项组:优化级别、警告控制、调试信息、头文件路径和库路径。它们几乎出现在每一个实际项目中。

4.1 优化级别:-O0、-O1、-O2、-O3、-Os

优化级别控制编译器对代码的「改造程度」。级别越高,生成的可执行文件通常越快,但编译时间更长,调试也更困难。

选项 含义 典型场景
-O0 不优化,保持源码结构 调试阶段,单步跟踪
-O1 轻度优化,减少代码体积 快速验证,轻度性能需求
-O2 中度优化,平衡速度与体积 发布版本,大多数项目首选
-O3 激进优化,追求极致速度 计算密集型,如音视频编码
-Os 优化体积,适合存储受限 嵌入式系统,Flash 空间紧张
我曾经踩过的坑: 有一次调试一个随机崩溃的问题,折腾了两天。最后发现是同事用 -O3 编译的,编译器把某个循环变量优化掉了,导致多线程同步逻辑失效。从那以后,我坚持调试阶段只用 -O0,发布前再切到 -O2 做最终测试。

我个人习惯在 Makefile 里这样写:

# 调试版本
CFLAGS_DEBUG = -O0 -g -DDEBUG

# 发布版本
CFLAGS_RELEASE = -O2 -Wall -Werror

4.2 警告选项:-Wall、-Wextra、-Werror

警告不是错误,但忽视警告往往埋下隐患。我见过太多线上事故,根源就是编译器早就警告过,但没人当回事。

-Wall 并不是「所有警告」,它只开启一组常用的警告。要更全面,可以加上 -Wextra。而 -Werror 会把所有警告当作错误处理——编译直接失败。

我的建议: 项目初期就加上 -Wall -Wextra -Werror。虽然刚开始会多花点时间修警告,但长期看,这能帮你避免大量隐蔽的 bug。

举个例子,未初始化的局部变量:

// test.c
#include <stdio.h>

int main() {
    int x;
    printf("%d\n", x);  // x 未初始化
    return 0;
}

编译看看:

$ gcc -Wall test.c -o test
test.c:5:17: warning: 'x' is used uninitialized [-Wuninitialized]
    5 |     printf("%d\n", x);
      |                 ^

如果没有 -Wall,这个警告根本不会出现。程序可能每次运行结果都不一样,排查起来非常头疼。

4.3 调试信息:-g 与 -g3

-g 选项告诉 GCC 在可执行文件中嵌入调试信息。这些信息包括源码行号、变量名、函数名等,是 GDB 等调试器工作的基础。

-g3 比 -g 包含更多信息,比如宏定义。如果你在代码里用了大量宏,调试时想查看宏展开后的值,就用 -g3。

小技巧: 发布版本也可以带 -g,然后用 strip 命令剥离调试信息。这样既保留了调试符号用于后续分析,又减小了交付体积。

# 编译时带调试信息
gcc -O2 -g app.c -o app

# 剥离调试信息,生成发布版
strip app -o app_release

# 保留调试符号文件
objcopy --only-keep-debug app app.debug

你想想看,如果线上程序崩溃了,没有调试信息,你只能看到一堆地址。有了 .debug 文件,就能定位到具体的源码行。这差别有多大,不用我多说了吧。

4.4 路径指定:-I 和 -L

这两个选项解决的是「编译器去哪里找文件」的问题。

  • -I:指定头文件搜索路径
  • -L:指定库文件搜索路径

为什么需要手动指定?因为编译器默认只搜索系统标准路径(如 /usr/include、/usr/lib)。你自己的项目目录、第三方库目录,都需要显式告诉编译器。

# 头文件在 include/ 目录
# 库文件在 lib/ 目录
gcc -I./include -L./lib main.c -lmylib -o app

这里 -lmylib 链接的是 libmylib.so 或 libmylib.a。GCC 会自动加上 lib 前缀和 .so/.a 后缀。

注意顺序: -I 和 -L 的顺序很重要。GCC 按照从左到右的顺序搜索路径。如果多个目录下有同名文件,先找到的会被使用。我曾经因为路径顺序搞反,链接了旧版本的库,导致一个诡异的内存泄漏查了整整一天。

4.5 综合示例:一个实际项目的编译命令

下面是一个嵌入式项目的典型编译命令,包含了本章所有选项:

arm-none-eabi-gcc \
  -mcpu=cortex-m4 \
  -mthumb \
  -O2 \
  -g3 \
  -Wall -Wextra -Werror \
  -I./include \
  -I./drivers/include \
  -I./third_party/freertos/include \
  -L./lib \
  -T./linker.ld \
  main.c \
  startup.c \
  -lmcu \
  -lfreertos \
  -o firmware.elf

拆开来看:

  • -mcpu=cortex-m4 -mthumb:目标架构选项
  • -O2:发布级优化
  • -g3:带宏定义的调试信息
  • -Wall -Wextra -Werror:严格警告检查
  • -I 三个路径:项目头文件、驱动头文件、FreeRTOS 头文件
  • -L./lib:库文件路径
  • -lmcu -lfreertos:链接 MCU 库和 FreeRTOS 库
  • -T./linker.ld:链接脚本

这个命令基本覆盖了嵌入式开发中 90% 的场景。你可以在自己的项目中以此为模板,按需调整。

4.6 知识结构图

下面这张图总结了本章五个核心选项组的关系:

GCC 常用编译选项体系 GCC 编译选项 -O 优化级别 -Wall 警告控制 -g 调试信息 -I 头文件路径 -L 库文件路径 说明: -O0 用于调试,-O2 用于发布 -g3 包含宏定义信息 -I 和 -L 支持多个路径,顺序敏感 实战建议 调试阶段:-O0 -g3 -Wall -Wextra 发布阶段:-O2 -g -Wall -Werror

核心要点回顾:

  • 优化级别从 -O0 到 -O3,调试用 -O0,发布用 -O2
  • 警告选项 -Wall -Wextra -Werror 是项目质量的「守门员」
  • 调试信息 -g 是 GDB 调试的前提,-g3 包含宏定义
  • 路径指定 -I 和 -L 解决「文件在哪」的问题,顺序很重要