C/C++ GCC


基本概念

基础语法

gcc/g++ [选项] [源文件] [选项] [目标文件]

常用选项

基本选项

选项作用
-o <file>指定输出文件名
-c只编译不链接,生成目标文件(.o)
-E只进行预处理不编译
-S编译到汇编语言,不汇编
-v显示编译过程的详细信息
输出默认文件名
gcc main.c
  • 没有-o参数
  • GCC默认会把可执行文件叫a.out(这是Unix系统的传统名字)
输出指定文件名
gcc main.c -o main

gcc内部进行以下四个阶段

  1. 预处理:处理宏定义、头文件包含等
  2. 编译:将预处理后的文件编译成汇编代码
  3. 汇编:将汇编代码转换成机器码
  4. 链接:将目标文件链接成可执行文件

在链接完成后,中间文件会被删除掉

预处理

不指定文件名

gcc -E main.c

这会执行标准输出,将结果打印到屏幕上

指定文件名

gcc -E main.c -o main.i

main.i这个名字可以自定义,扩展名也可以自定义,比如.txt, .abc

常见约定

扩展名含义常用性
.i预处理后的C文件最常用
.ii预处理后的C++文件最常用
.txt文本文件可用
无扩展名任意文件名不推荐

同理对于.s, .o文件也是一样的,这对于gcc来说是可以的,但对于make或其他通过扩展名判断文件类型的工具来说未必行得通

  • 命令:GCC内部调用cpp
  • 作用:
    • 处理#include -> 把头文件内容插入进来
    • 处理宏定义#define
    • 处理条件编译#if/#ifdef
  • 输出一个扩展后的C文件
编译
gcc -S main.i -o main.s
  • 预处理后的文件 -> GCC转换称汇编代码
  • 这一步就是C -> 汇编

main.s是gcc编译阶段最后的人类可读的文件

汇编
gcc -c main.s -o main.o
  • 汇编代码 -> 汇编器(as) -> 目标文件.o
  • .o文件是机器码二进制,基本不可读
  • 每个.c文件都会生成一个.o文件

.o文件里有

  • 函数的机器指令
  • 数据段、符号表
  • 调用其他库函数的信息
链接
gcc main.o -o main
  • 链接器ld会把.o文件,系统库(libc, libm等),其他.o问阿金,合并成最终的可执行文件main,这时printf等函数才真正找到实现

警告选项

选项作用
-Wall显示所有常见警告
-Wextra显示额外警告
-Werror将警告视为错误
-w禁止所有警告
-pedantic遵循标准C,提醒非标准用法

GCC只会报真正严重的语法错误或链接错误

  • 语法错误(如缺括号、括号不匹配)
  • 类型错误(如int *p = "abc";
  • 链接错误(如调用不存在的函数)

但像初始化、隐式转换、可能丢失精度等警告,默认不报

GCC设计之初遵循“兼容老代码”原则,C语言历史上很多老程序有未初始化、隐式转换等现象,默认警告太多导致就代码编译报满屏,用户体验很差

因此,开发中推荐的做法是

gcc -Wall -Wextra -Werror main.c

这样可以保证代码更安全、更规范,尤其是底层/库开发、游戏引擎开发、嵌入式开发等

-Wall

开启常用警告,比如

  • 未使用的函数或变量
  • 不匹配的格式化字符串(printf/scanf)
  • 可能的类型转换问题

但不会开启一些“更严格的检查”

-Wextra

常见警告示例

类型示例代码警告
未使用参数int f(int x){return 0;}unused parameter 'x'
空函数体void f(){}empty body (depends)
不完全初始化数组int arr[5] = {1,2};missing initializer for element 2
多余的逗号int arr[] = {1,2,};extra comma
隐式int转换char c = 200;overflow in implicit constant conversion
return没有值int f(){}control reaches end of non-void function

不同版本GCC会有差异

优化选项

选项级别特点适用场景
-O0不优化(默认)编译最快,调试信息完整开发调试阶段
-O1基本优化基本优化,不增加编译时间快速测试,平衡模式
-O2推荐优化常用优化,不增加代码体积生产环境默认选择
-O3激进优化高级优化,可能增加代码体积性能更关键代码
-Os体积优化优化代码体积嵌入式、移动设备
-0fast极致优化突破标准限制的优化数值计算场景
-O0
gcc -O0 program.c -o program

特点:

  • 编译速度最快
  • 代码直接对应源代码,不优化
  • 调试体验最好(变量不会被优化掉)
  • 适合开发和调试阶段
-O1
gcc -O1 program -o program

开启的优化:

  • 删除未使用的变量和函数
  • 简单的常量传播
  • 基本的指令调度
  • 不增加编译时间

示例:

int test() {
    int a = 10;
    int b = 20;
    int c = a + b; // O1会直接计算为30
    return c;
}
-O2
gcc -O2 program.c -o program

在-O1的基础上增加:

  • 函数内联(小函数)
  • 循环展开
  • 更激进的指令调度
  • 寄存器分配优化
  • 死代码消除

这是生产环境最常用的选项

-O3
gcc -O3 program.c -o program

在-O2基础上增加:

  • 所有函数内联(即使是大的)
  • 循环完全展开
  • 预测分支优化
  • 向量化(SIMD指令)

潜在问题:

  • 可能增加代码体积
  • 可能降低指令缓存命中率
  • 极少情况下反而更慢
-Os
gcc -Os program.c -c program

特点:

  • 以-O2为基础
  • 额外优化代码体积
  • 不进行会增大代码体积的优化
  • 适合嵌入式系统、移动应用
-Ofast
gcc -Ofast program.c -o program

特点:

  • 包含-O3所有优化
  • 加上-ffast-math(浮点运算优化)
  • 可能不符合IEEE/ANSI标准
  • 适合数值计算、科学计算
具体优化选项
控制内联
# 禁止内联
-fno-inline

# 限制内联函数大小
--param inline-unit-growth=20

# 内联所有函数(即使没标记inline)
-finline-functions
循环优化
# 循环展开
-funroll-loops

# 循环合并
-fmerge-constant

# 循环向量化
-ftree-vectorize
浮点优化
# 快速数学运算(不保证精度)
--ffast-math

# 不进行浮点优化
-fno-fast-math

# 允许重排浮点运算
-funsafe-math-optimizations
内存优化
# 消除未使用的变量
-fdead-code-elimination

# 合并全局常量
-fmerge-all-constants

# 地址空间布局随机化
-fpie
性能对比示例

测试程序

// benchmark.c
#include <stdio.h>
#include <time.h>

#define ITERATIONS 10000000000 // 10^10

int main() {
    clock_t start = clock();
    long long sum = 0;

    for (long long i = 0; i < ITERATIONS; i++) {
        sum += i * i; // 浮点运算
    }

    clock_t end = clock();
    double time = (double)(end - start) / CLOCKS_PER_SEC;
    printf("Time: %.3f seconds\n", time);
    printf("Sum: %lld\n", sum);
    return 0;
}

编译指令

gcc -O0 main.c -o main_O0 && time ./main_O0
gcc -O1 main.c -o main_O1 && time ./main_O1
gcc -O2 main.c -o main_O2 && time ./main_O2
gcc -O3 main.c -o main_O3 && time ./main_O3
gcc -Ofast main.c -o main_Ofast && time ./main_Ofast

运行结果

Time: 19.453 seconds
Sum: 7032546979563742720

real    0m19.459s
user    0m19.452s
sys     0m0.001s
Time: 7.349 seconds
Sum: 7032546979563742720

real    0m7.334s
user    0m7.350s
sys     0m0.000s
Time: 3.671 seconds
Sum: 7032546979563742720

real    0m3.646s
user    0m3.670s
sys     0m0.001s
Time: 3.659 seconds
Sum: 7032546979563742720

real    0m3.639s
user    0m3.660s
sys     0m0.000s
Time: 3.665 seconds
Sum: 7032546979563742720

real    0m3.650s
user    0m3.665s
sys     0m0.001s

分析

-O0 ~ -O1 有2.7x的提升
-O1 ~ -O2 有2x的提升
-O2 ~ -O3 的提升不明显
-Ofast 轻微提升

sum值在所有优化选项下相同,说明

1. 所有优化都保持了计算结果一致
2. 溢出是确定的(无符号/有符号溢出是正确行为)
3. 编译器优化没有改变计算结果

理论计算公式(取模2^64)
sum = (N-1) * N * (2N-1) / 6
N = 10 ^ 10
结果应该是一个64位整数

详细优化可以对比编译后的汇编文件进行分析

调试选项

调试选项本质上是在编译阶段保留足够的信息,从而让调试器能够还原源码级的执行过程

-g
gcc -g main.c -o main

作用:

  • 生成调试信息(debug symbols)
  • 包括:
    • 变量名
    • 函数名
    • 源码行号
    • 类型信息

没有-g,gdb只能看到汇编,看不到源码和变量名

-g的不同等级
-g0 // 不生成调试信息
-g1 // 最少(只有函数/行号)
-g2 // 默认
-g3 // 最详细(包含宏)

一般用-g,如果调试宏用-g3

-ggdb

含义:

  • 生成GDB专用增强调试信息
  • -g更GDB友好
增强调试能力的选项
  • -fno-inline
    • 禁止函数内联,否则在GDB中打不到断点、无调用栈
  • -fno-omit-frame-pointer
    • 推荐在linux下使用,可保留rbp(x86_64),让GDB,perf,火焰图更加准确
  • -fvar-tracking
    • 提高变量追踪能力
  • -fvar-tracking-assignments
    • 更强的变量追踪
调试信息格式 DWARF
-gdwarf-4
-gdwarf-5

DWARF是调试信息标准

  • v4版本:稳定
  • v5版本:更现代(压缩更好)
Sanitizer
AddressSanitizer
-fsanitize=address -g

检查越界,user-after-free, stack overflow

UndefinedBehaviorSanitizer
-fsanitize=undefined -g

检测UB

优化与调试的关系

-O会破坏调试体验

-O0 // 无优化,最适合调试
-O2 // 常规优化
-O3 // 激进优化

优化会导致:

  • 变量被优化掉(optimized out)
  • 代码重排
  • 内联函数
  • 行号错乱
-Og

-Og可以理解成“为调试而设计的优化级别”,它介于-O0-O1之间,但目标不是性能,而是在保证可调式性的前提下做一些“安全优化”

-O0最好调试,但代码很原始,执行慢;-O2性能好,但变量消失,行号错乱,断点跳来跳去
gcc引入-Og的目的就是在不破坏调试体验的前提下,让程序稍微接近真实执行状态

-Og

  • 简单常量传播
  • 死代码消除(不会影响调试语义的)
  • 基本块简化
  • 一些轻量SSA优化

不会做:

  • 激进内联
  • 复杂循环优化
  • 重排导致行号错乱
  • 变量寄存器化过度

语言标准选项

-std

gcc -std=c99 file.c
  • c99
  • c11
  • gnu99
  • c++11
  • c++20

使用示例

基本编译

# 编译单个文件
gcc main.c -o main
g++ main.cpp -o main

# 编译并运行
gcc main.c -o main && ./main

多文件编译

# 方式1:直接编译多个文件
gcc file1.c file2.c file3.c -o program

# 方式2:分步编译(适合大型项目)
gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
gcc file1.o file2.o -o program

# 方式3:Makefile

链接库文件

链接静态库(.a)
gcc -o program mani.c -L库路径 -l库名
  • -L:指定库文件所在的目录
  • -l:指定库名(去掉前缀lib和后缀.a
  • 多个库可以多次使用-l-lmath_utils -lm -lpthread
链接动态库(.so)

编译时链接

# 基本语法(同静态库)
gcc -o program main.c -L库路径 -l库名

运行时需要指定库路径,动态库需要在运行时能找到,有三种方法

设置LD_LIBRARY_PATH 环境变量
# 临时设置
LD_LIBRARY_PATH=./lib ./program

# 或者导出环境变量
export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH
./program
编译时指定rpath
# 使用$ORIGIN 表示可执行文件所在目录
gcc -o program main.c -L./lib -lmath_utils -Wl, -rpath, '$ORIGIN/lib'

# 或者使用绝对路径
gcc -o program main.c -L./lib -lmath_utils -Wl,-rpath=/path/tp/lib

# 多个rpath
gcc -o program main.c -L./lib -lmath_utils -Wl,-rpath,'$ORIGIN/lib:/usr/local/lib'
将库安装到系统路径
# 复制到系统库目录(需要root权限)
sudo cp lib/libmath_utils.so /usr/local/lib/
sudo ldconfig # 更新库缓存

# 然后直接运行
./program
优先级和搜索顺序
库搜索顺序
  1. -L指定的目录
  2. 环境变量LIBRARY_PATH(编译时)
  3. 系统默认目录(/lib, /usr/lib, /usr/local/lib)
动态库运行时搜索顺序
  1. LD_LIBRARY_PATH环境变量
  2. rpath编译时指定的路径
  3. runpath编译时指定的路径
  4. 系统默认目录
  5. /etc/ld.so.conf配置的目录
示例

项目结构

link-compile
|-- include/
|      |__ math_utils.h # 库头文件
|-- src/
|    |-- math_utils.c # 库源码
|    |__ add.c # 额外功能
|-- main.c # 测试程序
|-- Makefile # 编译脚本
|__ lib/ # 生成的库文件(编译后)

指令

# 编译库源文件为目标文件
gcc -c -Iinclude src/math_utils.c -o src/math/utils.o
gcc -c -Iinclude src/add.c -o src/add.o

# 创建静态库
ar rcs lib/libmath_utils.a src/math_utils.o src/add.o

# 编译主程序并链接静态库
gcc -o program main.c -Iinclude -Llib -lmath_utils

# 或
# 创建动态库
gcc -shared -o lib/libmath_utils.so src/math_utils.o src/add.o

# 编译主程序并链接动态库
gcc -o program main.c -Iinclude -Llib -lmath_utils

# 运行(需要指定库路径)
LD_LIBRARY_PATH=./lib ./program

包含头文件

# 添加头文件搜索路径
g++ program.cpp -o program -I/path/to/include

编译过程

gcc/g++编译分为四个阶段

gcc vs g++

Release & Debug

GCC 本身没有Debug/Release模式,这是构建系统层面的概念

GCC只有一堆编译选项,而Debug/Release只是这些选项的组合约定

Debug/Release通常来自:CMake, Makefile, IDE,它们自己约定

  • Debug = 一组“好调试”的编译参数
  • Release = 一组“高性能”的编译参数

GCC只关心三件事

  1. 是否生成调试信息
  2. 优化级别
  3. 宏控制行为

所谓Debug/Release,就是这三者的组合

  • Debug = 有调试信息 + 低优化
  • Release = 无调试信息 + 高优化 + 关闭断言

示例

Debug

-g -O0

或者更现代的

-g -Og

特点

  • 有完整调试信息
  • 几乎不优化
  • 变量可见
  • 断点稳定

Release

-O2 -DNDEBUG

特点:

  • 高优化(性能好)
  • 去掉调试信息
  • 去掉断言
DNDEBUG
#include <assert.h>

assert(x > 0);

Debug:

-g

assert生效

Release

-DNDEBUG

assert被优化掉

// 等价于
((void)0

CMake行为

在Cmake里

cmake -DCMAKE_BUILD_TYPE=Debug

等价于

-g
cmake =DCMAKE_BUILD_TYPE=Release

等价于

-O3 -DNDEBUG
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo

等价于

-O2 -g

静态库

动态库