☰
Linux下gcc/g++编译器全指南:从编译四步骤到版本切换与排错
2026/10/2 2:49:08 网站建设 项目流程

很多朋友装好Linux系统之后做的第一件事,就是打开终端敲一下gcc --version,看看这个传说中绕不开的编译器到底在不在。运气好的会看到一串版本号,运气不好的会直接收到一句command not found。我自己最早学Linux的时候也一样,把gcc当成“某个需要安装的软件”,装了就能编译,编译就是一行命令的事。直到后来踩了一堆坑,被“升级完还是旧版本”“明明装了g++却找不到编译器”“链接的时候报一堆undefined reference”狠狠教育过之后,才慢慢把这套东西的脾气摸清楚——gcc/g++这套工具链,原理上和水龙头差不多,看着简单,但真正用顺了,你得先明白它内部那几段管路是怎么走的。

这篇文章我不想写成命令手册,而是按照我实际折腾过的路线来聊:从基本概念、四个编译步骤,到日常参数、版本切换、安装排错,最后说一个很多人卡过的Qt编译器问题。不管你是刚接触Linux的C语言入门者,还是已经能写一点小项目但碰到环境问题就头大的初学者,看完应该都能少走几段弯路。

1. gcc和g++是什么,以及那个版本号背后的含义

先解决一个最基础但也最容易含糊的问题:gcc到底是一个程序,还是一堆程序的合称?

gcc这个名字最开始是GNU C Compiler的缩写,早期确实只负责编译C语言。后来GNU项目把它扩展成了C、C++、Objective-C、Fortran、Ada等多种语言的编译器集合,全称也变成了GNU Compiler Collection,也就是“GNU编译器套件”。所以你在Linux里装的gcc,本质上是一个编译器驱动——它会根据你给的文件后缀名,自动判断该调用哪一套语言前端去处理。gcc这个名字管C语言,g++则是专门管C++的那一面。

这个设计带来的直接后果是什么?是很多人会踩的“装了gcc但g++用不了”的坑。有些精简版Linux发行版或者容器镜像里,默认只装了gcc这个包,没有把g++一起装进去。你在终端里敲g++ --version,系统会告诉你这个命令不存在。这不是什么玄学问题,就是因为你只装了C编译器的那部分,C++编译器还没有装。解决方式也很简单:

sudo apt install g++ # Debian/Ubuntu系 sudo yum install gcc-c++ # CentOS/RHEL系

我见过不少初学者在Windows上用Dev-C++写C++写得很顺,一换到Linux就懵了,以为gcc hello.cpp写错了或者系统坏了。其实gcc和g++的主要差别在于:使用gcc编译.cpp文件时,它虽然也能识别语法,但默认不会自动链接C++标准库,于是你大概率会看到一堆undefined reference to std::cout之类的链接错误;用g++编译则默认把C++标准库一并链接进来,所以写C++的时候直接用g++最省事。

另外一个很多人反复纠缠的点:编译器和编辑器到底有什么区别?编辑器(比如VS Code、Vim、gedit)负责让你在屏幕上敲入代码、修改代码,它不负责生成可执行文件。编译器(gcc/g++、clang、MSVC)负责把写好的源代码翻译成机器能认识的目标文件,再链接成可执行程序。Vim写得再好,没有gcc,你写出来的hello.c也只是一堆文本。反过来,gcc再强,你不想一行行敲代码,它也没法替你写程序。这两个角色是上下游的关系,搞清楚之后,很多“找不到编译器”“编译器不能用”的报错就好理解了——八成是编辑器找到了,但编译器没接上。

2. 一条gcc命令背后其实藏了四个步骤

初学者最容易忽略的一件事是:你敲下的gcc hello.c -o hello,实际不是一步操作,而是四个独立的阶段连续执行的结果。这四个阶段分别是预处理、编译、汇编、链接。我建议每个学C语言的人,至少在命令行里手动把它们拆开跑一遍,因为一旦代码出错,gcc报的信息往往指向的是某个阶段的问题——比如预处理找不到头文件、编译阶段语法报错、链接阶段符号找不到。你搞不清楚阶段,就很难听懂它在说什么。

先准备一个最简单的hello.c:

#include <stdio.h> int main(void) { printf("hello gcc\n"); return 0; }

第一步,预处理。这一步做的事情主要是展开头文件、处理宏定义、删除注释。命令是:

gcc -E hello.c -o hello.i

-E告诉gcc只做预处理,不要往下走。生成的hello.i文件你用文本编辑器打开,会发现开头多了一大堆stdio.h里带进来的声明,可能上千行。这就是预处理真正干的活。头文件“展开”之后,代码才变成一份编译器真正开始分析的文件。

第二步,编译。把hello.i翻译成汇编语言,命令是:

gcc -S hello.i -o hello.s

-S让gcc把预处理后的文件编译成汇编代码,生成的是文本文件。你可以直接打开hello.s看,能看到main被拆成mov、call、ret这些指令。汇编是给人看的中间表达,机器还不能直接执行。这一步如果报错,通常是语法错误,比如少了分号、括号不匹配。

第三步,汇编。把hello.s转换成机器码,得到目标文件:

gcc -c hello.s -o hello.o

-c的意思是只编译不链接。到了这一步生成的hello.o已经是二进制的机器码文件了,但它还不能运行——因为调用的printf函数还没有和实际的实现连接起来。.o里只记载了一个标记:外部符号printf等待链接。你在Linux上用nm hello.o还能看到这些未解析的符号。

第四步,链接。这一步才是把我们的hello.o和C标准库里的printf实现拼在一起,生成最终的可执行文件:

gcc hello.o -o hello

这里-o指定输出文件名字为hello。链接阶段报错最常见的样式就是undefined reference to 'xxx',意思是你在代码里用了某个函数或变量,但链接时在所有库和目标文件中都没找到它的定义。比如你调用了数学库的sqrt(),编译能过,但链接阶段如果不加-lm,就会报undefined reference to 'sqrt'。这就是因为C标准里数学库是单独分出来的,默认不会自动链上。

实际工作中,没有多少人会每次手动分四步走,但这四步的产物名称和报错特征你得认得。文件后缀也很说明问题:.i是预处理后的文件,.s是汇编文件,.o是目标文件,不认这些后缀,以后看构建脚本的中间产物时会很晕。

3. 高频编译参数:我平时写C/C++最常用的几组

gcc的选项特别多,像一本厚字典,但你真正日常要用的其实就是固定的一小撮。我把这几年写C/C++用的最多的参数整理成一张表,每个都说一下它解决什么痛点。

参数作用什么时候用
-o指定输出文件名几乎所有场景
-Wall -Wextra打开绝大多数常规警告写代码时建议常年开着
-Werror把警告当成编译错误项目规范严格、CI构建用
-g生成调试信息要用GDB调试时必加
-O0 -O1 -O2 -O3优化等级日常调试用-O0,发布用-O2
-std=c11/-std=c++17指定语言标准要使用新标准语法时
-I目录添加头文件搜索路径引用了非系统目录下的头文件
-L目录添加库文件搜索路径链接非系统目录下的库
-l库名链接指定库文件使用pthread、m等库时
-D宏名定义宏配合条件编译#ifdef使用

一块一块说。最基本的-g -O组合我几乎每次编译都要用:-g是为了万一程序崩了,GDB还能告诉你崩在第几行;-O2是让编译器做优化,但调试的时候-O2会把代码顺序打乱,变量也可能被优化掉,导致GDB看到的行号和源代码对不上,所以调试阶段我用-O0,发布前再切-O2重新编一次。

再比如-Wall -Wextra。很多人写C语言,警告一堆却不当回事。但警告往往是潜在bug的预告——变量声明了没用、比较类型不一致、忘了写返回值等等。强烈建议从一开始就把-Wall -Wextra写上,让编译器帮你把问题提前暴露出来。严格一点的项目甚至加-Werror,让警告直接变成错误,逼着代码在进入仓库前就是干净的。这个习惯能帮你省下大量调试时间。

-std这个参数也很有说头。有的老系统自带gcc版本偏低,默认标准可能是旧的C11之前的规则,你用新语法会报错或者行为和大家预期不一样。这时候显式指定-std=c11、-std=c++17,能让你写的代码按你预期的规则编译。例如C++里写auto、范围for循环(for (auto x : vec)),在默认标准较老的编译器上可能不认识,敲上-std=c++11以上就正常了。顺带说一句,gcc在较新版本里已经默认使用GNU标准的某个级别(比如gnu17),它在标准的严格规则之上额外开放了一些扩展,你要是追求严格标准合规,就要显式用-std=c11而不是-std=gnu11。

关于“gcc日志输出到文件”这个操作,日常也会碰到。有时候编译输出特别长,终端一下子就滚屏了,你想翻找信息和报错就麻烦,可以把输出重定向到文件里:

gcc -Wall -Wextra -O2 hello.c -o hello 2>&1 | tee build.log

2>&1把标准错误输出并到标准输出,管道接到tee上,终端能看到的同时,内容也写进了build.log。下次只看日志文件就行了:

cat build.log

如果你是默认配置的bash或zsh,也可以直接让bash自己把编译输出追加进日志,比如:

gcc -Wall -Wextra -O2 hello.c -o hello > build.log 2>&1

这种方式安静一些,没有终端输出,适合脚本里批量编译时用。

4. 版本管理的大坑:升级之后还是旧版本

接下来聊一个我在实践里碰到最多次的问题,也是在搜索里高频出现的:“gcc升级后为啥还是旧版本”。表象是这样:你用sudo apt install gcc-12装好了新版本,甚至看/usr/bin/下也有gcc-12这个文件了,但终端里执行gcc --version,显示的依旧是老的版本号。很多人到这里就懵了,怀疑是安装没生效,或者包坏了。

其实很正常。你要搞清楚一件事:终端里敲gcc命令时,系统是怎么找到它的?

Linux下,命令查找依靠PATH环境变量。PATH里列出了一些目录,系统按顺序从前往后找,第一个同名可执行文件就算数。系统安装gcc的老版本绑定在/usr/bin/gcc这个路径上,而这个目录通常是PATH里优先出现的,于是哪怕你装好了gcc-12这个二进制,只要没有某个机制让gcc这个名字指向它,最终执行gcc命令时还是会用旧的/usr/bin/gcc。目录列表可以用which -a gcc看,会列出所有能搜到的gcc以及顺序。

新手最容易犯的错误是“装了就行”。装好一个软件,和把系统默认命令绑定到新软件,是两件不同的事。那么怎么正确切换?我常用的办法是update-alternatives,这是Debian系用来管理同一命令多个版本的系统级工具。

第一步,把自己装的gcc-11和gcc-12注册进alternatives:

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 120

每条命令最后的数字是优先级,优先级高的作为默认。然后执行:

sudo update-alternatives --config gcc

系统会列出所有已注册的gcc版本,让你按数字选择。选完之后,gcc --version才会真正显示你选中的版本。g++也是一样的操作套路,把命令名从gcc换成g++再注册一遍,注意/usr/bin/g++-12这些文件在装了g++包后才会存在。

有人会觉得,那我直接改软链接不就行了?比如:

sudo ln -sf /usr/bin/gcc-12 /usr/bin/gcc

其实也行,但自己做符号链接,一来容易把系统原本的/usr/bin/gcc覆盖得乱糟糟,二来升级系统时可能被软件包管理器重置或者产生冲突。update-alternatives由系统统一管理,以后想回滚也方便,不会再出现“不知道谁把gcc换掉了”的灵异事件。

顺带多提一句:如果你发现gcc --version显示的数字和g++ --version不一致,不用太紧张,这是正常的——gcc和g++是各自独立注册的命令,版本不一定完全同步。真正要注意的是你自己的项目构建脚本里,如果写明“要求gcc >= 12”,你不但要切换gcc版本,还要同步切换g++版本,否则编译C++时还是会用旧版。

5. Ubuntu环境安装失败的现场复盘

另一个高频问题是“Ubuntu安装gcc失败”。这个报错一出来,很多人的第一反应是换软件源、重装系统,其实大部分情况下并不需要走到那一步。先说说我遇到过的典型故障和排查顺序。

第一种情况,直接apt install gcc,提示找不到软件包。常见原因是本地软件包列表太旧,apt根本不知道有哪些新版本可用。第一步一定是:

sudo apt update

这个命令会从软件源拉取最新的软件包索引。跑完之后再执行apt install gcc,命中概率会高很多。如果还是报找不到包,那就检查/etc/apt/sources.list里的软件源配置是否可用,比如镜像源坏了、网络不通、源地址本身过期了。换个稳定可用的Ubuntu镜像源,再sudo apt update一次,通常就解决了。

第二种情况,安装过程中提示依赖关系损坏,比如:

gcc : 依赖: cpp (= 版本号) 但无法安装它

这种情况多半是因为系统里有部分dpkg状态异常,或者之前某个包安装到一半被中断。先执行两个经典命令:

sudo apt --fix-broken install sudo dpkg --configure -a

第一个命令让apt自动修复损坏的依赖;第二个命令重新配置那些安装到一半停止的包。跑完之后再回过来装gcc,很多时候依赖问题就消失了。我曾经在一台自己折腾过很多库的机器上安装编译环境,跑了这三个命令之后才算恢复正常,重装系统纯属不必要的极端手段。

第三种情况,你装的是非常新的Ubuntu版本,或者反过来是特别老但没升级的版本,默认软件源里的gcc版本很低,你想用更新的gcc-12但apt里没有。此时可以考虑添加Ubuntu Toolchain PPA:

sudo add-apt-repository ppa:ubuntu-toolchain-r/test sudo apt update sudo apt install gcc-12 g++-12

PPA是第三方维护的软件源,用的时候多留个心眼,别随便添加不知名来源的PPA。但ubuntu-toolchain-r这个PPA在Ubuntu社区里使用范围很广,专门用于提供新版本的工具链,相对靠谱。

还有一种情况,你不想用apt,而是从源码编译安装gcc,那我得提醒一下:gcc的源码包编译本身的依赖链很长,需要GMP、MPFR、MPC等几个库,而且configure的参数如果不熟悉,很容易中途失败。新手不建议一上来就搞源码安装。源码编译适合明确需要某个特定版本、并且系统包管理器确实给不了的情况,否则老老实实走apt或PPA是效率最高的路线。

还有一个小坑来自“linux和gcc环境搭建”这个场景:很多人在桌面版Ubuntu里开终端,发现自己写不了C代码,以为gcc没装好,但其实只是缺了个build-essential这个元包。它的作用是一次性把gcc、g++、make等一整套编译构建工具装齐。可以用:

sudo apt install build-essential

如果你想确认某个库是否已经就位,可以用dpkg -l | grep gcc看包列表,或者直接看版本号。装好之后,从hello world到调用Makefile的小项目,基本环境就齐了。

6. “g++编译器不见了”和Qt环境配置的真实案例

最后一个部分,聊一个把Linux用户和Windows用户都折腾过的经典问题:Qt Creator界面里报“cannot run compiler 'g++'”。这行报错看着吓人,但拆开来看其实是两个层面的事:一是你的系统里到底有没有可用的g++可执行文件,二是Qt有没有正确配置到它。

先说Linux下的情况。你用的是Ubuntu或其他Debian系系统,界面化的Qt Creator可以创建项目,但一点编译就报找不到g++。此时首要排查的就是g++是否真的装了。只装了gcc没装g++,在Linux上很常见。直接用:

g++ --version

如果提示command not found,那就装上:

sudo apt install g++

装完之后回到Qt Creator,一般它会自动识别到新出现的编译器。如果不能自动识别,就去菜单里的Tools → Options → Kits里看当前构建套件的Compiler栏,把C++编译器手动选择成/usr/bin/g++。如果还有问题,检查一下Qt安装路径本身是不是出了问题,比如有些人是通过Ubuntu软件中心装的Qt,路径和默认设置不一致,导致Qt内部的工具链扫描范围没覆盖到系统编译器。

再说Windows下的情况。这个标题看着是Linux问题,但搜索里大量出现“window系统下qt出现cannot run compiler 'g++'”、“qt安装完mingw编译器后怎么安装msvc编译工具链”——说明很多人是跨平台开发,一换环境就卡在编译器配置上。Windows下Qt Creator用的编译器主要有两类:一类是MinGW,也就是gcc/g++在Windows上的移植版;另一类是MSVC,微软自家的C++编译器。选择哪套,取决于你要做什么:想在Windows下用开源工具链、不想折腾Visual Studio的,选MinGW;要做Windows原生API开发、或者项目依赖了MSVC特有东西的,选MSVC。

“装了MinGW却还是找不到g++”,最常见的两个原因:第一,MinGW的bin目录没有加到系统PATH环境变量里。Qt Creator虽然是独立的一个程序,但它找编译器的时候,很多情况下也是直接执行系统里的g++命令,如果你的PATH没有指向MinGW的bin目录,它自然找不到。第二,你安装的Qt安装包和MinGW版本不匹配。Qt官方在安装器里会让你勾选编译器套件,如果只装了Qt库没装配套的MinGW工具链,Qt是不会凭空找到编译器的——在Windows上,MinGW是一个独立安装的软件,一般是mingw-w64项目提供的版本。

至于“怎么安装msvc编译工具链”,最简单的路径是安装Visual Studio Build Tools,安装时勾选“使用C++的桌面开发”工作负载,装完后回到Qt Creator,在Kits设置里选择自动检测到的MSVC编译器。MSVC编译器不能在Linux上跑,所以做Linux开发还是老老实实用gcc/g++;Windows上两份工具链可以共存,Qt Creator会根据你选择的Kit自动调用对应的那一份。

再延伸一个链接相关的经典问题,就是前面说的“undefined reference”。比如你在代码里用到了线程库pthread_create,编译阶段没问题,链接阶段报:

undefined reference to `pthread_create'

这通常不是编译器坏了,而是没有告诉链接器要链接pthread库。解决办法就是在编译命令里加:

gcc -O2 -pthread test.c -o test

-pthread这个选项同时起到了定义宏和链接线程库的作用,写-lpthread也能达到链接的目的,但官方推荐的用-pthread覆盖得更全面。类似的还有数学库-lm、实时库-lrt,这些经验都是典型“编译过了但链接失败”的场景,遇到的时候先别急着重装,回忆一下自己是不是漏了哪个库。

如果后面你开始接触更大的项目,比如用Makefile或者CMake管理构建,就会发现这些基础概念会反复用到:CMake在配置阶段就会自动探测gcc/g++并检查能否编译一个测试程序,报错样式五花八门,但只要你能看懂检查编译器能不能用这一层,排查方向就不会岔太远。我自己从最初“装好gcc就以为万事大吉”,到现在每次看到编译报错,第一反应是先分清是预处理、编译还是链接各阶段的问题,这个过程也就是C/C++开发者走向成熟的一条必经之路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询