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 gC 也可以分步骤编译# 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.hmath.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 -stdc11 -o main main.cC 示例g -stdc17 -o main main.cpp常见 C 标准c11 c14 c17 c20 c23二十五、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 -stdc11 -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 -stdc17 -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 -stdc17 -Wall -Wextra -g -O0 -o main main.cpp含义-stdc17 指定 C17 标准 -Wall 开启常见警告 -Wextra 开启更多警告 -g 生成调试信息 -O0 不进行优化方便调试 -o main 输出文件名为 main main.cpp 输入源文件发布程序时可以使用优化选项gcc -O2 -o main main.c或者g -stdc17 -O2 -o main main.cpp二十八、几个核心概念总结1. gcc main.c 会默认生成 a.outgcc 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 用 gccC 用 ggcc -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、静态库、动态库以及大型项目构建的第一步。