224. gcc, g++ 和 gdb

提到 gcc/g++,很多初学者的第一反应可能是:
“这不是编译器嘛,不就写 gcc main.c 然后敲个回车?”
但当编译报错时,才发现自己对它的了解就像对前任一样——一知半解

其实,gcc/g++ 不只是一个“会把 C/C++ 代码变成可执行文件”的工具,它还能优化、调试、排错,甚至分析代码!今天,小康就来带你解锁 gcc/g++/gdb 的正确姿势:从编译到调试,通通搞明白!

小贴士

  • gcc:C 编译器,专门编译 C 程序。
  • g++ : C++编译器,专门编译 C++ 程序。
    gcc 和 g++的用法和参数基本相同,主要介绍 gcc!
  • gdb :调试 C/C++ 程序的利器!

什么是 gcc?简单聊聊它的身份

gcc,全称 GNU Compiler Collection,是一款强大的开源编译器,支持多种语言(C、C++、Objective-C 等)。但今天,我们只专注它在 C/C++ 编译领域的表现。

一句话概括 gcc 的工作:
把你写的代码从“人话”翻译成机器能看懂的“机器语言”。即:将你的程序代码编译成计算机能够识别的机器语言(01机器码)。

gcc 的核心流程分为四步:

  1. 预处理:处理宏定义、头文件、条件编译等。
  2. 编译:将预处理的代码转成汇编代码。
  3. 汇编:把汇编代码转成机器代码(生成目标文件)。
  4. 链接:将目标文件生成可执行文件。

GCC 的安装与检查

检查是否已安装

在终端输入以下命令:

1
gcc --version

如果返回版本信息,说明 GCC 已经安装成功。如果提示 command not found,那就继续看下面的安装步骤。

安装 GCC/G++/GDB

  1. Ubuntu/Debian 系统

    1
    2
    3
    sudo apt update
    sudo apt install build-essential -y # 安装 gcc 和 g++
    sudo apt install gdb # 安装 gdb

    这会同时安装 GCC 和其他编译工具链。

  2. CentOS/Red Hat 系统

    1
    2
    3
    sudo yum groupinstall "Development Tools" -y  # 安装 gcc 和 g++

    sudo yum install gdb -y # 安装 gdb
  3. 验证安装:分别运行 gcc --versiong++ --versiongdb --version,确认 GCC/G++/GDB 是否安装成功。

gcc 的基本用法:从入门到熟练

最简单的编译指令

1
gcc main.c -o main
  • main.c 是你的代码文件。
  • -o main 指定生成的可执行文件名为 main。如果不写 -o,默认生成名为 a.out 的文件。

运行程序

1
./main

就这么简单,一行命令搞定编译和运行,是不是挺方便?但这其实是“打包式”的操作,编译和链接一起完成。如果你是刚入门的初学者,可能还不知道 GCC 背后做了些什么。这时,我们可以试试 分步编译,让每一步变得更清晰。

分步编译指令

分步编译可以帮你更好地理解编译器的工作流程。其实,GCC 编译分为两个主要阶段(G++ 类似):

  1. 编译阶段:将源码翻译成机器能理解的中间文件(目标文件,.o 文件)。
  2. 链接阶段:将目标文件链接成最终的可执行文件。

第一步:编译源程序文件

运行以下命令,将 main.c 转换成目标文件 main.o:

1
gcc -c main.c -o main.o
  • -c 参数表示只编译,不链接。
  • main.o 是生成的目标文件,虽然不能直接运行,但它已经包含了 main.c 的翻译结果。

第二步:链接目标文件

接下来,将目标文件 main.o 链接成可执行文件 main:

1
gcc main.o -o main
  • 这一步不再使用 -c,而是使用 -o,因为我们要让 GCC 把目标文件链接成一个完整的程序。

运行程序:

1
./main

同样的输出,经过分步操作生成了结果,是不是感觉自己更专业了?

为什么要分步编译?

你可能会想:“分两步多麻烦啊,我直接一步编译不就行了?”其实,分步编译有它的优势:

  • 更高的灵活性:当项目中有多个文件时,你只需要重新编译修改过的文件,其他部分可以直接复用之前生成的目标文件(.o文件 ),大大提高效率。(下文会提到多个文件编译的情况)
  • 清晰的流程:每一步的工作职责都很明确,便于排查问题。例如,如果某个文件编译不过,可以单独解决,而不用从头来过。

常用选项大盘点

  1. 加点料,让错误信息更清晰:

    1
    gcc -Wall -Wextra main.c -o main
    • -Wall:开启常见警告(比如变量声明但没使用 -Wunused-variable
    • -Wextra:开启额外警告(如未使用函数参数 -Wunused-parameter
  2. 为调试准备,加上调试符号:

    1
    gcc -g main.c -o main
    • -g:生成调试信息,方便用 GDB 调试。
  3. 编译多个文件:

    1
    gcc file1.c file2.c -o program
    • 多个源文件会一起编译链接成一个可执行文件。
  4. 优化代码(-O 系列)

    让程序更快?试试优化选项:

    -O0:不优化(默认)

    gcc 默认不会优化代码,生成的程序跟你写的源代码更接近。

    啥时候用?

    • 开发调试时,容易追踪代码逻辑。

      编译示例:

      1
      2
      3
      # 这两个命令效果一样
      gcc hello.c -O0 -o hello
      gcc hello.c -o hello

      -O1:基础优化

      会优化掉无用代码,让程序稍微跑快一点,但调试依然友好。

    • 需要一点性能提升,但还得经常调试代码的时候。

      编译示例:

      1
      gcc hello.c -O1 -o hello

      -O2:常用优化(推荐!

      -O1 的基础上,增加更多优化,比如减少循环次数、改进分支预测等。

    • 程序跑得还可以,但希望它跑得更稳更快。适合大部分场景。

      编译示例:

      1
      gcc hello.c -O2 -o hello

      -O3:更高级优化

      -O2 更激进,开启一些高级优化,比如函数内联和向量化。

    • 追求极限性能的程序,比如科学计算、大型数据处理。但注意,有时候优化过头会导致兼容性问题(比如浮点运算不准)。

      编译示例:

      1
      gcc hello.c -O3 -o hello

总结:选择适合的优化级别

  • 开发阶段-O0-O1,方便调试。
  • 生产环境-O2 是最平衡的选项,跑得快又稳。
  • 极限性能-O3 ,但要注意兼容性和精度问题。

根据场景选个合适的优化级别,你的代码就能跑得既稳又快!

多文件项目的编译

在实际项目中,代码往往分成多个文件,比如:

  • main.c
  • utils.c
  • utils.h

方法一:一次性编译

1
gcc main.c utils.c -o my_program

优势:

  • 简单粗暴:一条命令搞定所有文件,适合小项目。
  • 快速省事:文件少的时候,用起来方便快捷。

方法二:分步编译再链接

1
2
3
gcc -c main.c -o main.o  
gcc -c utils.c -o utils.o
gcc main.o utils.o -o my_program

优势:

  • 效率高:只编译修改过的文件,不用每次全编译。
  • 更灵活:大项目用分步编译更好管理,还能配合自动化工具用。

建议:

文件少就用方法一,文件多或者想提高效率就用方法二,两个都得会,随场景切换!

gcc 编译流程深入解析:搞懂每一步

如果只知道用 gcc 编译,算是入门;但要真正搞懂 gcc,必须了解它的四步工作流程。

预处理:先搞定头文件和宏

1
gcc -E main.c -o main.i
  • -E:只执行预处理,输出结果是 main.i
  • 预处理会替换 #include 的头文件内容,展开宏定义,去掉注释。
  • 打开生成的文件,你能看到“裸露”的预处理后代码。

编译:从人话到汇编

1
gcc -S main.i -o main.s
  • -S:将预处理后的代码转成汇编代码,结果是 main.s
  • 汇编代码是介于高级语言和机器语言之间的一种语言,更接近机器。

汇编:把汇编转成机器码

1
gcc -c main.s -o main.o
  • -c:只执行编译到汇编的这一步,生成目标文件(main.o)。
  • 目标文件是二进制的,但还不能直接运行。

链接:生成可执行文件

1
gcc main.o -o main 
  • 链接器负责将目标文件和系统库一起链接,生成最终的可执行文件。

各文件内容对比:

文件名 内容类型 cat /vim 查看结果 更合适的查看方式
main.i 预处理后的源码 源码,可读 无需额外工具,直接查看
main.s 汇编代码 汇编指令,可读 无需额外工具,直接查看
main.o 二进制目标文件 乱码,不可读 objdumpreadelf
main 可执行文件,机器码 乱码,不可读 objdumpreadelf

PS: gcc 和 g++ 的用法及参数基本相同。要编译 C++ 程序,只需把命令中的 gcc 换成 g++,比如编译 main.cppg++ main.cpp -o main。C++ 程序的文件通常以 .cpp 为后缀,如 main.cpp

调试利器:GDB 上线了

写代码,最怕的是:程序挂了,但根本不知道为什么挂。
这时,调试工具 GDB 就派上用场了。

用 gcc 编译时加调试信息

1
gcc -g main.c -o main
  • -g 选项主要是生成调试信息,方便用 GDB 调试。

常用 GDB 命令

  1. 启动 GDB:

    1
    gdb ./main
    • 进入 GDB 调试模式。
  2. 设置断点:

    1
    break <行号>
    • 比如 break 10,在代码第 10 行 设置断点 。
  3. 运行程序

    1
    run
    • 运行程序,停在断点处。

4. 单步执行

  • 单步执行,不进入函数

    1
    next
  • 单步执行,进入函数。

    1
    step

    选择 next 跳过函数,step 进入函数,按需使用即可!

  1. 查看变量值:

    1
    print <变量名>
    • 比如 print x,显示当前变量 x 的值。
  2. 查看当前代码(上下文代码)

    • 查看当前执行位置的代码:
    1
    list

    默认显示当前断点附近的代码。

    • 指定显示某行附近的代码:
    1
    list <行号>

    比如 list 20,显示第 20 行附近的代码。

  3. 打印函数调用栈

    • 查看调用栈信息:

      1
      backtrace
    • 显示当前函数被哪个函数调用,调用者又是谁,一层层往上追溯。

    • 对于分析崩溃点(coredump)特别有用。

  4. 查看所有断点

  • 列出当前所有断点:

    1
    info breakpoints

可以看到每个断点的编号、位置等信息。

  1. 删除断点

    • 删除某个断点:

      1
      delete <断点编号>

    比如 delete 1,删除编号为 1 的断点。

    • 删除所有断点:

      1
      delete
  2. 继续运行程序

  • 从当前断点继续运行程序:

    1
    continue
  • 程序会从当前断点继续跑,直到遇到下一个断点或结束。

  1. 退出调试:
1
quit

常见问题排查

缺少头文件

报错stdio.h: No such file or directory

原因:简单说,编译器找不到 stdio.h 这个标准头文件,可能是系统里没装编译工具包,缺了开发相关的库。

解决方法

  • 在 Ubuntu/Debian 系统上,安装必备工具包:使用命令 sudo apt install build-essential 这会把 gcc、g++ 和相关头文件都装上。

  • 在 CentOS/Red Hat 系统上,安装开发工具:sudo yum groupinstall "Development Tools"

    这样能确保编译环境完整无缺。

“段错误”

报错:Segmentation fault (core dumped)

原因:简单说,程序想访问一块不该碰的内存,比如:

  • 用了“野指针”(指针没初始化,随便指向了某个未知地址)。
  • 数组越界了,访问了数组的第“10086”个元素,而数组长度只有 100 个。
  • 使用了已经释放的内存。

解决方法

1、检查指针和数组:

  • 确保指针初始化,比如:

    1
    2
    int *ptr ; 
    *ptr = 10; // 未初始化就赋值,肯定会报错:Segmentation fault (core dumped)
  • 不要访问超出数组范围的元素,比如访问 arr[10086],而数组只有 100 个元素。

  • 如果是动态内存分配(malloc/free),检查是否释放了两次。

2、用 GDB 调试:

  • 编译时加上 -g 参数生成调试信息:

    1
    gcc -g main.c -o main
  • 然后用 GDB 跑程序:

    1
    2
    gdb ./main
    run
  • 程序崩溃时,输入:

    1
    bt

GDB 会告诉你出错在哪一行。

调试时别慌,找到那行代码,慢慢改,段错误就能搞定!

链接错误

报错undefined reference to ‘某函数’

原因:这句话的意思很简单,你的代码用到了一个函数,但是编译器在链接阶段找不到它的实现。可能是:

  • 忘了加实现的文件:函数写在另一个 .c 文件里,编译时没有包含进去。
  • 漏了库的链接:用到了外部库的函数,但没告诉编译器要用哪个库。
  • 函数声明没问题,函数实现却没写,编译器不知道该去哪里找它。

解决方法

  • 如果函数是你自己写的,确保编译时包含了所有相关文件:

    1
    gcc main.c func.c -o main

    没加就补上!

  • 如果是库函数,比如用到了数学库的 sqrt,需要链接对应的库,加上 -lm

    1
    gcc main.c -lm -o main

    这里的 -lm 表示链接数学库(math library)。

  • 检查函数实现是否真的写了!如果只是声明了函数:

    1
    void my_function();

    但实现忘了写,肯定会报错。赶紧补上实现!

    这个链接错误其实很常见,仔细检查文件和库就能解决!

未定义的函数引用

报错undefined reference to ‘某函数’

原因:简单说,这个报错就是你的代码用到了某个函数,但编译器找不到它的实现。可能是你没把实现的文件加到编译命令里,或者用到了外部库却忘了链接。

解决方法

1、如果是你自己写的函数,确认它的实现文件是否加到编译命令里,比如:

1
gcc main.c func.c -o main

没加就补上!

2、如果是用的库函数,比如数学库里的 sqrt,就加上对应的库链接,比如:

1
gcc main.c -lm -o main

这个 -lm 是告诉编译器“我要用数学库”。

GDB 提示没有调试信息

报错No debugging symbols found

原因:这个报错意思很简单:你的程序没带“调试信息”。GDB 说,“你让我调试代码,可你不给我地图(调试信息),我咋知道问题在哪?”

解决方法

编译的时候记得加上 -g 参数,这是让 GCC 帮你把调试信息打包进去,比如:

1
gcc -g main.c -o main

这个 -g 就是那个“地图”。

然后再用 GDB 调试:

1
gdb ./main

这下 GDB 才知道代码怎么走的,可以帮你查问题了!

总结:gcc/g++/gdb 不是魔法,用熟了像开挂

gcc/g++ 就像开发中的“瑞士军刀”,功能全面却不复杂,用熟了它们,开发效率会直线上升:

  • 从编译到优化,轻松搞定,让程序又快又稳。
  • 从调试到排错,有了 gdb,分析问题更加清晰直观。
  • 掌握了 gcc/g++ 和 gdb,任何 C/C++ 程序编译调试都不在话下!