Linux 下 gcc / g++ 编译过程详解:从编译到链接
前言
在 Linux 下学习 C / C++,一定绕不开两个编译命令:
gcc和:
g++很多初学者第一次接触 Linux 编译 C 语言程序时,可能会看到这样的命令:
gcc main.c执行之后,当前目录下会生成一个文件:
a.out这个a.out就是编译生成的可执行程序。
运行它:
./a.out如果程序没有问题,就可以看到程序的运行结果。
但是很多人刚开始学习时,会对这些问题感到疑惑:
gcc 是什么? g++ 又是什么? gcc main.c 为什么会生成 a.out? a.out 是什么? 为什么运行程序要写 ./a.out? gcc -o main main.c 是什么意思? -o 后面的 main 是输入文件还是输出文件? gcc 编译程序时到底经历了哪些步骤? 什么是预处理、编译、汇编、链接? 为什么有时候会出现 undefined reference? 为什么找不到头文件?本文就从最简单的gcc main.c开始,按照从浅到深、从简单到复杂的顺序,带你完整理解 Linux 下 gcc / g++ 从源代码到可执行程序的整个过程。
一、gcc 和 g++ 是什么?
1. gcc 是什么?
gcc原本表示 GNU C Compiler,也就是 GNU C 语言编译器。
现在的 gcc 通常指 GNU Compiler Collection,也就是 GNU 编译器套件。
它不仅可以编译 C 语言,也可以支持 C++、Objective-C、Fortran 等多种语言。
不过在刚开始学习时,可以先简单理解为:
gcc 主要用于编译 C 语言程序例如有一个 C 源文件:
main.c就可以使用:
gcc main.c来编译它。
2. g++ 是什么?
g++是 GNU C++ 编译器,主要用于编译 C++ 程序。
例如有一个 C++ 源文件:
main.cpp就可以使用:
g++ main.cpp来编译它。
3. gcc 和 g++ 的区别
简单记忆:
.c 文件一般使用 gcc 编译 .cpp 文件一般使用 g++ 编译例如:
gcc main.c用于编译 C 程序。
g++ main.cpp用于编译 C++ 程序。
对于 C++ 程序,建议使用g++,因为g++会自动按照 C++ 的方式进行编译和链接,并且会自动链接 C++ 标准库。
例如下面这个 C++ 程序:
#include <iostream> using namespace std; int main() { cout << "Hello C++" << endl; return 0; }推荐使用:
g++ main.cpp如果使用:
gcc main.cpp可能会出现类似下面的链接错误:
undefined reference to `std::cout' undefined reference to `std::endl'原因是gcc默认不会像g++那样自动链接 C++ 标准库。
所以初学阶段可以先记住一句话:
写 C 用 gcc,写 C++ 用 g++。二、从最简单的命令开始:gcc main.c
先准备一个最简单的 C 程序。
文件名:
main.c代码内容如下:
#include <stdio.h> int main() { printf("Hello gcc\n"); return 0; }现在使用 gcc 编译:
gcc main.c这条命令的意思是:
使用 gcc 编译 main.c 这个源文件执行完成后,查看当前目录:
ls可以看到类似结果:
a.out main.c可以发现,执行gcc main.c之后,系统生成了一个新的文件:
a.out这个a.out就是编译出来的可执行程序。
运行它:
./a.out输出结果:
Hello gcc三、为什么生成的文件叫 a.out?
当我们执行:
gcc main.c时,只告诉了 gcc 一件事情:
我要编译 main.c但是我们没有告诉 gcc:
最终生成的可执行文件叫什么名字所以 gcc 会使用默认的输出文件名:
a.out也就是说:
gcc main.c可以理解为:
编译 main.c,并默认生成名为 a.out 的可执行文件a.out是 Unix / Linux 系统中比较传统的默认可执行文件名。
四、为什么运行程序要写 ./a.out?
在 Windows 中,我们可能习惯双击运行程序。
但是在 Linux 命令行中,要运行当前目录下的程序,通常需要写:
./程序名所以运行a.out时,需要写:
./a.out这里的:
. 表示当前目录 / 表示路径分隔符所以:
./a.out表示:
运行当前目录下的 a.out 文件如果直接输入:
a.out可能会提示:
command not found原因是 Linux 默认不会直接从当前目录查找可执行程序。
五、使用 -o 指定输出文件名
每次编译都生成a.out并不方便。
例如我们希望生成的可执行文件名叫:
main就可以使用-o参数。
命令如下:
gcc -o main main.c这条命令的意思是:
使用 gcc 编译 main.c,并把生成的可执行文件命名为 main其中:
-o main表示:
指定输出文件名为 main后面的:
main.c表示:
输入的源文件是 main.c执行后查看当前目录:
ls可以看到:
main main.c运行程序:
./main输出:
Hello gcc六、一定要理解:-o 后面跟的是输出文件名
-o是 gcc / g++ 中非常常用的参数,它的作用是指定输出文件名。
例如:
gcc -o main main.c可以拆开理解为:
gcc 使用 gcc 编译器 -o main 指定输出文件名为 main main.c 输入源文件是 main.c这里一定要注意:
-o 后面的 main 是输出文件名,不是源文件名。所以,下面这种写法是非常危险的:
gcc -o main.c main.c这条命令的意思不是“编译 main.c”。
它的真实含义是:
把 main.c 编译后的结果输出成 main.c也就是说,输出文件名和源文件名重名了。
这样可能会导致原来的main.c源代码被覆盖成一个二进制可执行文件。
此时看起来就像:
源文件被删了但更准确地说,是:
源文件被编译生成的二进制文件覆盖了所以一定要记住:
不要把 -o 后面的输出文件名写成已有的源文件名。正确写法:
gcc -o main main.c错误且危险的写法:
gcc -o main.c main.c七、gcc -o main main.c 和 gcc main.c -o main 的区别
下面两种写法都可以:
gcc -o main main.c也可以写成:
gcc main.c -o main它们的作用是一样的,都是:
编译 main.c,并生成名为 main 的可执行文件但是对于初学者来说,更推荐先使用:
gcc -o main main.c因为这种写法更容易理解:
gcc 编译器 -o main 输出文件名 main.c 输入源文件而:
gcc main.c -o main虽然也正确,但是初学者容易把main.c和main的关系看混。
所以本文后续主要采用:
gcc -o main main.c作为示例。
八、gcc 常见命令格式
gcc 最基础的使用格式是:
gcc 源文件例如:
gcc main.c表示:
编译 main.c,默认生成 a.out如果想指定输出文件名,可以写成:
gcc -o 输出文件名 源文件例如:
gcc -o main main.c表示:
编译 main.c,生成可执行文件 main多个源文件也可以一起编译:
gcc -o main main.c add.c表示:
编译 main.c 和 add.c,最后生成可执行文件 main九、gcc main.c 背后到底做了什么?
虽然我们只执行了一条命令:
gcc main.c但是 gcc 在背后并不是只做了一件事。
它实际上完成了四个主要阶段:
预处理 -> 编译 -> 汇编 -> 链接完整过程可以理解为:
main.c ↓ 预处理 main.i ↓ 编译 main.s ↓ 汇编 main.o ↓ 链接 a.out如果指定输出文件名:
gcc -o main main.c那么最终生成的就是:
main整体过程就是:
main.c ↓ 预处理 main.i ↓ 编译 main.s ↓ 汇编 main.o ↓ 链接 main简单来说:
gcc main.c或者:
gcc -o main main.c这一条命令背后其实自动完成了:
预处理 + 编译 + 汇编 + 链接下面我们一步一步拆开来看。
十、第一步:预处理
预处理是整个编译过程的第一步。
可以使用-E参数让 gcc 只执行预处理:
gcc -E main.c -o main.i这条命令的意思是:
只对 main.c 进行预处理,并把结果输出到 main.i预处理阶段主要做这些事情:
1. 展开头文件 2. 替换宏定义 3. 处理条件编译 4. 删除注释例如有这样一段代码:
#include <stdio.h> #define MESSAGE "Hello gcc" int main() { printf("%s\n", MESSAGE); return 0; }其中:
#include <stdio.h>会在预处理阶段被展开。
宏定义:
#define MESSAGE "Hello gcc"会被替换。
所以:
printf("%s\n", MESSAGE);经过预处理后,大致会变成:
printf("%s\n", "Hello gcc");预处理之后生成的文件通常使用.i后缀:
main.i这个文件依然是 C 代码,只是已经完成了头文件展开、宏替换、条件编译处理等工作。
十一、第二步:编译
编译阶段会把预处理后的 C 代码转换成汇编代码。
可以使用-S参数让 gcc 只编译到汇编阶段:
gcc -S main.i -o main.s也可以直接从 C 源文件生成汇编文件:
gcc -S main.c -o main.s执行后会生成:
main.s.s文件里面是汇编代码。
内容可能类似这样:
.file "main.c" .text .globl main .type main, @function main: pushq %rbp movq %rsp, %rbp汇编代码已经比 C 语言更接近 CPU 能理解的指令了。
但是它仍然不是最终可以直接运行的程序。
十二、第三步:汇编
汇编阶段会把汇编代码转换成目标文件。
可以使用-c参数生成目标文件:
gcc -c main.s -o main.o也可以直接从 C 源文件生成目标文件:
gcc -c main.c -o main.o执行后会生成:
main.o.o文件叫做目标文件,也叫 object file。
它已经包含了机器指令,但是还不能直接运行。
例如你不能直接这样运行:
./main.o因为main.o只是一个中间文件,还没有完成最后的链接。
十三、第四步:链接
链接是生成最终可执行程序的最后一步。
如果已经有了目标文件:
main.o可以使用下面的命令进行链接:
gcc -o main main.o这条命令表示:
把 main.o 链接成最终可执行文件 main执行后生成:
main运行:
./main输出:
Hello gcc链接阶段主要做这些事情:
1. 合并多个目标文件 2. 解析函数和变量的引用关系 3. 链接标准库或第三方库 4. 生成最终可执行文件例如程序中使用了:
printf("Hello gcc\n");但是printf的真正实现并不在我们自己写的main.c里面,而是在 C 标准库中。
在链接阶段,链接器会找到printf的实现,并把它和我们的程序关联起来,最终生成可以运行的可执行文件。
十四、完整分步骤编译过程
原本一条命令:
gcc -o main main.c可以拆成下面四步:
# 1. 预处理:main.c -> main.i gcc -E main.c -o main.i # 2. 编译:main.i -> main.s gcc -S main.i -o main.s # 3. 汇编:main.s -> main.o gcc -c main.s -o main.o # 4. 链接:main.o -> main gcc -o main main.o最终运行:
./main输出:
Hello gcc也就是说:
gcc -o main main.c等价于 gcc 帮我们自动完成了:
预处理 main.c 生成 main.i 编译 main.i 生成 main.s 汇编 main.s 生成 main.o 链接 main.o 生成 main平时我们通常不需要手动拆开这四步,因为 gcc 会自动完成。
但是理解这四步非常重要,因为很多编译错误、链接错误都和这些阶段有关。
十五、C++ 程序的编译过程
C++ 程序的编译过程和 C 程序类似,也分为:
预处理 -> 编译 -> 汇编 -> 链接假设有一个 C++ 文件:
main.cpp代码如下:
#include <iostream> using namespace std; int main() { cout << "Hello g++" << endl; return 0; }最简单的编译方式:
g++ main.cpp默认也会生成:
a.out运行:
./a.out输出:
Hello g++如果想指定输出文件名:
g++ -o main main.cpp运行:
./main输出:
Hello g++C++ 也可以分步骤编译:
# 1. 预处理:main.cpp -> main.ii g++ -E main.cpp -o main.ii # 2. 编译:main.ii -> main.s g++ -S main.ii -o main.s # 3. 汇编:main.s -> main.o g++ -c main.s -o main.o # 4. 链接:main.o -> main g++ -o main main.oC++ 预处理后的文件通常可以使用:
.ii作为后缀。
C 语言预处理后的文件通常使用:
.i作为后缀。
十六、头文件和源文件的关系
很多初学者会把头文件和源文件混在一起。
先看一个简单例子。
项目结构如下:
project/ ├── add.h ├── add.c └── main.cadd.h内容如下:
#ifndef ADD_H #define ADD_H int add(int a, int b); #endifadd.c内容如下:
#include "add.h" int add(int a, int b) { return a + b; }main.c内容如下:
#include <stdio.h> #include "add.h" int main() { int ret = add(10, 20); printf("ret = %d\n", ret); return 0; }其中:
int add(int a, int b);是函数声明。
它的作用是告诉编译器:
有一个函数叫 add 它接收两个 int 参数 它返回一个 int 类型的结果但是这只是声明,不是实现。
真正的实现是在add.c里面:
int add(int a, int b) { return a + b; }所以可以简单理解为:
头文件 .h:主要放声明 源文件 .c:主要放实现十七、多文件直接编译
对于上面的项目,可以直接这样编译:
gcc -o main main.c add.c这条命令的意思是:
同时编译 main.c 和 add.c,并链接生成可执行文件 main运行:
./main输出:
ret = 30如果只编译:
gcc -o main main.c可能会出现:
undefined reference to `add'原因是main.c里面调用了add函数,但是add函数的实现写在add.c里面。
如果编译命令中没有带上add.c,链接器就找不到add的实现。
所以正确写法是:
gcc -o main main.c add.c十八、多文件分步编译
大型项目中,通常不会每次都把所有.c文件从头编译一遍。
更常见的方式是先分别生成.o目标文件,再统一链接。
例如:
gcc -c main.c -o main.o gcc -c add.c -o add.o这两条命令分别生成:
main.o add.o然后再链接:
gcc -o main main.o add.o运行:
./main输出:
ret = 30这种方式的好处是:
如果只修改了 add.c,只需要重新编译 add.c 不需要重新编译 main.c 最后重新链接即可例如:
gcc -c add.c -o add.o gcc -o main main.o add.o这也是 Makefile 和大型项目构建系统的基础。
十九、头文件路径:-I
如果头文件不在当前目录,而是在单独的include目录中,就需要使用-I参数指定头文件搜索路径。
例如项目结构如下:
project/ ├── include/ │ └── add.h └── src/ ├── add.c └── main.c此时main.c中可能这样写:
#include "add.h"但是编译时如果直接写:
gcc -o main src/main.c src/add.c可能会报错:
fatal error: add.h: No such file or directory意思是:
找不到 add.h 这个头文件这时候需要告诉 gcc 去哪里找头文件:
gcc -o main src/main.c src/add.c -I include其中:
-I include表示:
把 include 目录加入头文件搜索路径也可以写成:
gcc -I include -o main src/main.c src/add.c-I的位置一般比较灵活,但要保证路径写正确。
二十、库文件是什么?
头文件只提供声明,真正的函数实现可能来自源文件,也可能来自库文件。
库文件可以简单理解为:
已经提前编译好的代码集合Linux 下常见库文件有两类:
静态库:.a 动态库:.so例如:
libm.a libm.so libpthread.so头文件和库文件的区别可以这样理解:
头文件:告诉编译器函数长什么样 库文件:告诉链接器函数真正在哪里例如:
#include <math.h>math.h只是提供数学函数的声明。
而数学函数真正的实现,需要在链接阶段链接数学库。
二十一、链接库:-L 和 -l
编译程序时,如果需要链接第三方库,常见参数有两个:
-L:指定库文件所在目录 -l:指定要链接的库名例如:
gcc -o main main.c -L ./lib -lxxx其中:
-L ./lib表示:
到 ./lib 目录下查找库文件而:
-lxxx表示:
链接名为 xxx 的库这里要注意,-lxxx并不是去找一个叫xxx的文件。
它实际会去找类似这样的库文件:
libxxx.so libxxx.a也就是说:
-lm实际对应的可能是:
libm.so libm.a-lpthread实际对应的可能是:
libpthread.so libpthread.a二十二、数学库示例:-lm
看下面这个程序:
#include <stdio.h> #include <math.h> int main() { double ret = sqrt(16.0); printf("%f\n", ret); return 0; }文件名:
main.c如果直接编译:
gcc -o main main.c在一些环境下可能会出现链接错误:
undefined reference to `sqrt'原因是:
sqrt 函数的声明在 math.h 中 但 sqrt 函数的实现需要链接数学库 libm正确写法:
gcc -o main main.c -lm其中:
-lm表示链接数学库。
需要注意,库参数通常建议放在源文件或目标文件后面。
推荐:
gcc -o main main.c -lm不推荐:
gcc -lm -o main main.c因为在某些链接器规则下,库的顺序会影响符号解析。
二十三、编译错误和链接错误的区别
学习 gcc / g++ 时,一定要区分:
编译错误 链接错误1. 编译错误
编译错误通常发生在源代码变成目标文件之前。
常见原因有:
语法错误 变量未声明 类型错误 找不到头文件 函数声明不匹配例如:
#include "add.h"但是编译器找不到add.h,就会出现:
fatal error: add.h: No such file or directory这是编译阶段的问题。
2. 链接错误
链接错误通常发生在.o目标文件生成之后,链接成可执行文件的时候。
常见错误:
undefined reference to `xxx'例如:
undefined reference to `add'这说明:
编译器知道 add 这个函数存在声明 但是链接器找不到 add 函数的真正实现比如只写了:
gcc -o main main.c但是没有把add.c一起编译进去,就可能出现这个错误。
正确写法:
gcc -o main main.c add.c或者:
gcc -c main.c -o main.o gcc -c add.c -o add.o gcc -o main main.o add.o二十四、常见 gcc 参数总结
1.-o:指定输出文件名
gcc -o main main.c表示:
编译 main.c,生成可执行文件 main不要写成:
gcc -o main.c main.c因为这样可能会覆盖源文件。
2.-E:只进行预处理
gcc -E main.c -o main.i表示:
只进行预处理,不编译、不汇编、不链接3.-S:只生成汇编文件
gcc -S main.c -o main.s表示:
生成汇编文件 main.s4.-c:只生成目标文件
gcc -c main.c -o main.o表示:
生成目标文件 main.o,不进行链接5.-I:指定头文件搜索路径
gcc -I include -o main src/main.c src/add.c表示:
让 gcc 到 include 目录下查找头文件6.-L:指定库文件搜索路径
gcc -o main main.c -L ./lib -lxxx表示:
让链接器到 ./lib 目录下查找库文件7.-l:指定链接库
gcc -o main main.c -lm表示:
链接数学库 libm8.-Wall:开启常见警告
gcc -Wall -o main main.c表示:
开启常见编译警告建议平时写代码时加上。
9.-Wextra:开启更多警告
gcc -Wall -Wextra -o main main.c表示:
在 -Wall 的基础上开启更多警告10.-g:生成调试信息
gcc -g -o main main.c表示:
生成调试信息,方便使用 gdb 调试例如:
gdb ./main11.-O:开启优化
常见优化等级:
-O0 -O1 -O2 -O3开发调试阶段常用:
gcc -g -O0 -o main main.c发布程序时可以使用:
gcc -O2 -o main main.c12.-std:指定语言标准
C 语言示例:
gcc -std=c11 -o main main.cC++ 示例:
g++ -std=c++17 -o main main.cpp常见 C++ 标准:
c++11 c++14 c++17 c++20 c++23二十五、gcc 常用命令总结
# 编译 C 程序,默认生成 a.out gcc main.c # 运行默认生成的程序 ./a.out # 指定输出文件名 gcc -o main main.c # 运行指定名称的程序 ./main # 开启常见警告 gcc -Wall -o main main.c # 开启更多警告 gcc -Wall -Wextra -o main main.c # 生成调试版本 gcc -g -O0 -o main main.c # 生成优化版本 gcc -O2 -o main main.c # 指定 C 标准 gcc -std=c11 -o main main.c # 只预处理 gcc -E main.c -o main.i # 只生成汇编 gcc -S main.c -o main.s # 只生成目标文件 gcc -c main.c -o main.o # 根据目标文件链接生成可执行文件 gcc -o main main.o # 多文件直接编译 gcc -o main main.c add.c # 多文件分步编译 gcc -c main.c -o main.o gcc -c add.c -o add.o gcc -o main main.o add.o # 指定头文件路径 gcc -I include -o main src/main.c src/add.c # 指定库路径并链接库 gcc -o main main.c -L ./lib -lxxx # 链接数学库 gcc -o main main.c -lm二十六、g++ 常用命令总结
# 编译 C++ 程序,默认生成 a.out g++ main.cpp # 运行默认生成的程序 ./a.out # 指定输出文件名 g++ -o main main.cpp # 运行指定名称的程序 ./main # 开启常见警告 g++ -Wall -o main main.cpp # 开启更多警告 g++ -Wall -Wextra -o main main.cpp # 生成调试版本 g++ -g -O0 -o main main.cpp # 生成优化版本 g++ -O2 -o main main.cpp # 指定 C++ 标准 g++ -std=c++17 -o main main.cpp # 只预处理 g++ -E main.cpp -o main.ii # 只生成汇编 g++ -S main.cpp -o main.s # 只生成目标文件 g++ -c main.cpp -o main.o # 根据目标文件链接生成可执行文件 g++ -o main main.o # 多文件直接编译 g++ -o main main.cpp add.cpp # 多文件分步编译 g++ -c main.cpp -o main.o g++ -c add.cpp -o add.o g++ -o main main.o add.o # 指定头文件路径 g++ -I include -o main src/main.cpp src/add.cpp # 指定库路径并链接库 g++ -o main main.cpp -L ./lib -lxxx二十七、推荐的编译命令
对于 C 语言学习阶段,推荐使用:
gcc -Wall -Wextra -g -O0 -o main main.c含义:
-Wall 开启常见警告 -Wextra 开启更多警告 -g 生成调试信息 -O0 不进行优化,方便调试 -o main 输出文件名为 main main.c 输入源文件对于 C++ 学习阶段,推荐使用:
g++ -std=c++17 -Wall -Wextra -g -O0 -o main main.cpp含义:
-std=c++17 指定 C++17 标准 -Wall 开启常见警告 -Wextra 开启更多警告 -g 生成调试信息 -O0 不进行优化,方便调试 -o main 输出文件名为 main main.cpp 输入源文件发布程序时可以使用优化选项:
gcc -O2 -o main main.c或者:
g++ -std=c++17 -O2 -o main main.cpp二十八、几个核心概念总结
1. gcc main.c 会默认生成 a.out
gcc main.c默认生成:
a.out运行:
./a.out2. 使用 -o 可以指定输出文件名
gcc -o main main.c表示:
编译 main.c,生成 main3. -o 后面是输出文件名
一定不要写成:
gcc -o main.c main.c因为这样可能会把源文件main.c覆盖掉。
4. 一条 gcc 命令背后有四个阶段
预处理 -> 编译 -> 汇编 -> 链接对应文件变化:
main.c -> main.i -> main.s -> main.o -> main5. 头文件不是库文件
头文件主要放声明:
函数声明 宏定义 类型定义 结构体声明 类声明库文件或源文件中才有真正的实现。
6. 编译错误和链接错误不是一回事
编译错误常见于:
语法错误 找不到头文件 类型错误 声明错误链接错误常见于:
undefined reference 找不到函数实现 找不到库文件7. C 用 gcc,C++ 用 g++
gcc -o main main.c用于 C 程序。
g++ -o main main.cpp用于 C++ 程序。
结语
刚开始学习 Linux 下 C / C++ 编译时,不要一上来就死记复杂命令。
可以先从最简单的命令开始:
gcc main.c理解它会默认生成:
a.out然后再学习如何指定输出文件名:
gcc -o main main.c接着再逐步理解这条命令背后的完整过程:
main.c ↓ 预处理 main.i ↓ 编译 main.s ↓ 汇编 main.o ↓ 链接 main当你理解了:
预处理 编译 汇编 链接这四个阶段之后,再看 Makefile、多文件编译、静态库、动态库、头文件路径、库文件链接等内容,就会清晰很多。
最终需要记住的核心命令是:
gcc main.c默认生成:
a.out指定输出文件名:
gcc -o main main.cC++ 程序:
g++ -o main main.cpp理解这些基础内容,是继续学习 Linux C / C++ 开发、Makefile、CMake、静态库、动态库以及大型项目构建的第一步。
