很多人在Ubuntu上第一次准备写C/C++程序,都是从一句网上随手搜到的命令开始的:sudo apt install gcc。这句话没法说它是错的,但你照着敲完,往往会在第二步就卡住。写个hello.cpp、输入gcc hello.cpp,编译有时候能过,一用std::cout或者某些标准库功能,乱七八糟的undefined reference就冒出来了;回头看教程,人家又说你缺g++,让你再装一遍,然后你就开始怀疑自己是不是装了个假Linux。
实际上这不是假Linux,而是没有理解GCC和G++之间的关系,也不清楚Ubuntu里软件包、编译器版本、构建工具是怎么协作的。这篇文章就从安装命令出发,把gcc/g++的选择、apt安装方式、版本管理、常见报错、以及和VS Code这类IDE/编辑器配合的问题一次性讲清楚。适合刚开始用Ubuntu学C/C++的人,也适合每次换机器都要重装一遍环境、但总在同一个坑里栽跟头的朋友。
1. 动手之前,先把GCC、G++和Ubuntu的关系理清楚
1.1 GCC不是“一个编译器”,而是一整套工具链
GCC是GNU Compiler Collection的缩写,看起来像个编译器,其实它是一整套编译器工具链的集合。gcc和g++是这套工具链暴露给用户的“前端命令”,它们负责调用预处理、编译、汇编、链接等一系列后台组件,最终帮你把源代码变成可执行文件。
gcc最初用于编译C语言,g++用于编译C++语言,但严格来说两者都能处理C和C++代码。真正的区别在于链接阶段:用g++编译C++程序时,它会自动链接libstdc++(C++标准库)以及相关的运行时库;而直接用gcc去编译一个.cpp文件,如果代码里用了标准库里的东西,链接时通常会报一堆undefined reference。
所以搜“ubuntu安装gcc和gcc c++的命令”这个标题,本质上是想同时搞定两个命令:一个是gcc,一个是g++。很多教程只写装gcc,结果C代码能跑,C++代码一编译就各种报错,原因就在这儿。如果你只打算写纯C,那只装gcc也能凑合;但凡是学C++,g++必须一起装上。
1.2 为什么Ubuntu上优先用apt而不是源码编译
Linux下安装软件的方式很多,可以用apt,也可以自己下载源码编译。对GCC这种基础工具链,绝大多数情况下都应该用apt安装。理由很直白:
- apt能自动处理依赖关系。GCC不是一个孤立的程序,它依赖binutils、libc6-dev、cpp等一堆底层包,手动下载源码时这些依赖都要自己搞定,对新手来说相当折磨。
- apt安装的版本和Ubuntu发行版做过适配,会和系统库的ABI兼容,踩坑概率低。
- 后续升级维护简单,sudo apt upgrade就能统一更新。
从源码编译GCC不是不可以,但那是给有明确需求的人准备的,比如需要某个特定版本、要做交叉编译、或者想修改编译器内部行为。只是想在Ubuntu上写C/C++程序的话,完全没必要自己折腾编译过程,直接交给apt处理就行。
1.3 装之前先确认三件事
安装虽然是一行命令的事,但前提条件最好先查一遍,避免出了错再去排查。
用cat /etc/os-release或lsb_release -a看一下当前系统是哪个版本。Ubuntu 22.04、24.04这类LTS版本源里的GCC版本是不同的,先知道系统版本,后面排查问题会方便很多。
检查软件源状态,执行sudo apt update,把包索引刷一遍。这一步能避免“软件源信息太旧导致找不到包”的问题,也能顺带确认网络状况。
最后用apt list --installed 2>/dev/null | grep -E "^(gcc|g++)"看看系统里现在有没有已经安装过的编译器残留,避免装重复了还以为自己是第一次装。
这几个判断步骤花不了半分钟,但能把后面大量莫名其妙的报错挡在门外。
2. 核心安装:常用命令与安装之后的验证
2.1 正式安装命令
在Ubuntu上安装gcc和g++的命令很简单,核心就两条:
sudo apt update sudo apt install gcc g++ -ysudo apt update是刷新软件包索引,确保你本地记录的包版本信息是最新的。sudo apt install gcc g++ -y里的-y代表自动确认,跳过安装前的“是否继续”提问,适合在终端里连续操作时用。
如果不想一条一条确认,也可以把两个包写在一起,apt支持一次安装多个软件包,空格分隔就行。
这里我强烈建议额外来一个元包:build-essential。
sudo apt install build-essential -ybuild-essential是一个“元包”,它本身不提供功能,但会拉取一套基本的编译工具链,包括gcc、g++、make、dpkg-dev和一些基础开发库。很多情况下,你只装gcc和g++,后面跑make时又会提示找不到make命令,而装了build-essential就把这些基础问题一次性都解决了。
2.2 安装后应该看到什么:版本验证
装完不是直接写代码,先验证一下环境到底有没有生效。在终端里依次执行:
gcc --version g++ --version正常情况下,你会看到类似这样的输出:
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.g++同理,只是名字换成g++。这个版本号就是你当前编译器工具链的版本,不同Ubuntu系统版本输出会不同,比如Ubuntu 24.04上默认可能是13.x,这都很正常。
还可以用which gcc和which g++看一下命令的具体路径:
which gcc # /usr/bin/gcc which g++ # /usr/bin/g++这一条在排查“为什么我明明装了新版,却还是用旧版”的时候特别有用,等会第3节会细说。
2.3 用第一个程序验证编译链路是真的通的
看到版本号还不够,真正要验证的是从源码到可执行文件这条链路是通的。新建一个hello.c:
cat > hello.c << 'EOF' #include <stdio.h> int main() { printf("Hello, GCC!\n"); return 0; } EOF编译并运行:
gcc hello.c -o hello ./hello看到输出Hello, GCC!就说明C语言编译链路正常。
再验证C++,新建hello.cpp:
cat > hello.cpp << 'EOF' #include <iostream> int main() { std::cout << "Hello, G++!" << std::endl; return 0; } EOF编译并运行:
g++ hello.cpp -o hello_cpp ./hello_cpp这里有个小细节:如果你非要用gcc去编译这个C++程序,比如执行gcc hello.cpp -o hello_cpp,很多系统会报错或出现链接错误,类似undefined reference to `std::cout'。原因就是前面说的,gcc不会自动链接C++标准库,所以学C++老老实实用g++,这是很关键的一个好习惯。
-o参数表示指定输出文件名,不带这个参数的话,默认会生成a.out,Linux下那也是个可执行文件,但命名太抽象了,建议每次都明确指定输出名。
2.4 扩展包:gcc-multilib、libc6-dev这些要不要装
默认情况下,gcc/g++装好后能编译当前架构(比如x86_64)的普通程序。但你可能会在一些教程里看到gcc-multilib、g++-multilib这样的包,它们的作用是让你在64位系统上交叉编译32位程序,也就是生成32位架构的可执行文件。
如果你的目标是给嵌入式设备交叉编译,或者需要编译32位的老代码,那就得装multilib相关包;如果只是写普通的应用程序,不需要碰。还有libc6-dev一般是作为依赖被自动装上的,不用特意去管。
判断标准很简单:遇到“bits/libc-header-start.h找不到”这类32位头文件缺失问题时,再考虑装multilib;平时不需要预先安装一堆用不上的包。
3. 版本管理:为什么你“升级”了还是旧版本
3.1 Ubuntu自带的GCC版本本来就不是“最新版”
很多从Windows开发环境转过来的朋友有个习惯,就是追求软件版本越新越好。但Ubuntu源里的GCC版本策略跟这个思路不一样,它更看重稳定性和兼容性。系统预装的GCC版本通常是发布该Ubuntu版本时选定的一个稳定版本,不会频繁跟着上游更新。
比如Ubuntu 22.04 LTS默认自带的是GCC 11系列,Ubuntu 24.04 LTS默认是GCC 13系列。这个版本不是“旧”,而是经过发行版测试、和该版本系统的库文件兼容性最好的一个版本。系统自带的软件包为了整体稳定性牺牲“最新”是很正常的。
3.2 很多人说的“升级后还是旧版本”,其实是这三个原因
第一种情况,你在终端里执行sudo apt upgrade,以为能把GCC升到新大版本,结果一看gcc --version,还是原来的版本。这其实没有错,apt upgrade只会在你已经安装的软件包范围内更新到软件源里的最新修复版,不会把一个软件从GCC 11“升级”成GCC 13,因为那是另一个软件包,比如gcc-13,而不是gcc。跨大版本升级需要显式安装新的大版本包。
第二种情况,你自己装了gcc-12,选择了某个教程里的install gcc-12命令,但在终端敲gcc,得到的还是旧版本。这在Ubuntu里太常见了。原因在于/usr/bin/gcc只是一个软链接,默认指向系统选定的版本。新安装的gcc-12只是多了一个/usr/bin/gcc-12文件,并不会自动改变/usr/bin/gcc这个软链接的指向。这时候你需要查看一下:
ls -l /usr/bin/gcc*你会看到类似这样的输出:
lrwxrwxrwx 1 root root 21 ... /usr/bin/gcc -> /etc/alternatives/gcc lrwxrwxrwx 1 root root 23 ... /usr/bin/gcc-11 -> x86_64-linux-gnu-gcc-11 lrwxrwxrwx 1 root root 23 ... /usr/bin/gcc-12 -> x86_64-linux-gnu-gcc-12也就是说,gcc命令可能通过/etc/alternatives/gcc指向gcc-11,你装的gcc-12根本没被选为默认。
第三种情况,即使你把系统默认版本切到了新版本,某些项目或构建系统还是会调用旧版本。这跟编译器的搜索路径和环境变量有关。make或CMake可能会优先使用CC和CXX这两个变量指定的编译器,或者项目里写死了编译器路径。此时你改了系统的gcc软链接,但项目还是用旧编译器在编译,表现就是“升级了还是旧版本”。
3.3 用update-alternatives做多版本切换
当你安装了多个GCC版本,想手动切换系统默认版本时,最好用的工具是update-alternatives。它专门用来管理系统里同一命令的多版本软链接。
把gcc和g++的各个版本加入备选列表:
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 --install /usr/bin/g++ g++ /usr/bin/g++-11 110 sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 120数字110和120是优先级,数字越大优先级越高。然后执行:
sudo update-alternatives --config gcc sudo update-alternatives --config g++终端会出现一个交互式列表,输入对应的编号回车,就能切换默认版本。
如果不想用update-alternatives,也可以直接改软链接,比如:
sudo rm /usr/bin/gcc sudo ln -s /usr/bin/gcc-12 /usr/bin/gcc但这样比较粗暴,而且以后系统更新可能给你改回去,没有update-alternatives优雅。
3.4 想要更新版本的GCC,怎么装更稳妥
如果你确实需要比较新的GCC,但又不想自己从源码编译,最简单的方法是使用Ubuntu Toolchain测试仓库,也就是社区里常说的toolchain-r/test PPA。这个PPA专门提供比系统源更新的GCC/G++版本,兼容性和打包质量都不错。
sudo add-apt-repository ppa:ubuntu-toolchain-r/test sudo apt update sudo apt install gcc-13 g++-13 -y装完之后,你可以通过/usr/bin/gcc-13这个命令来调用新版本。要不要把它设为默认版本,跟前面的逻辑一样,用update-alternatives去切换。
还有一种更保守的做法:从源码编译一个新版GCC,单独安装到/opt/gcc-14这样的目录,不改动系统默认的任何文件。这样系统编译内核模块或者别的东西都还用原有工具链,你自己编译项目时通过CMAKE_CXX_COMPILER指定新编译器路径就行。不过源码编译GCC的步骤有点多,依赖也要注意,新手不建议一开始就走这条路。
3.5 编译器搜索路径的优先级,搞清楚这个才能根治
不管你怎么切换系统默认版本,还是要理解编译器是怎么被定位的。当你在终端输入gcc,shell会从PATH环境变量里按顺序查找,第一个匹配的gcc就会被执行。通常/usr/bin在PATH比较靠前,而/usr/bin/gcc是个软链接,最终指向谁就是你当前的默认版本。
但在工程构建里,GCC的定位方式会更多样:
- make默认会使用cc,而cc通常也是一个软链接,指向系统默认的gcc。
- CMake在配置项目时会调用编译器来测试特性,并把找到的编译器路径记录在CMakeCache.txt里。你之后切换了系统默认GCC,再重新运行cmake时如果检测到缓存,可能还是会用之前记录的旧编译器。遇到这种情况,删掉build目录或CMakeCache.txt再重新配置一次即可。
- 一些项目会在CMakeLists.txt里直接写set(CMAKE_C_COMPILER "gcc-11"),这就不受系统默认版本影响了,需要手动改配置。
说到底,“升级后还是旧版本”很多时候不是没升级成功,而是调用链上某个环节根本没用到你升级后的那个版本。先在终端敲which gcc、ls -l /usr/bin/gcc、echo $CC、echo $CXX,基本上能定位到大多数问题。
4. 高频问题与排查记录
4.1 提示E: Unable to locate package gcc是什么情况
这个报错有几个常见原因。最常见的是没有先执行sudo apt update,软件源索引太旧,apt根本不知道有gcc这个包。还有就是软件源列表里禁用了某些仓库,或者网络不通,apt更新源失败,索引不完整。
处理办法是:
sudo apt update如果apt update的过程里出现很多连接超时或404错误,那就是源配置的问题。需要检查/etc/apt/sources.list以及/etc/apt/sources.list.d/目录下的源配置文件,确认源地址可用。新手换源时一定要用对应自己Ubuntu版本的源,版本代号对不上就会出现404或找不到包的情况。
网络方面,如果更新源一直失败,先确认网络本身通不通,再考虑换源,但具体用什么镜像源、什么配置,每个网络环境不一样,这里不展开。
4.2 apt安装中途报锁错误:Could not get lock /var/lib/dpkg/lock
这个报错很典型,通常是另一个apt进程还在跑,或者上次安装中断后锁没释放。完整报错类似:
E: Could not get lock /var/lib/dpkg/lock - open (11: Resource temporarily unavailable)处理思路很简单:先看有没有apt或dpkg进程还在运行。如果有,等它结束;如果确认没有其他进程在运行,但锁还是存在,可以删除锁文件(但要先确认没有任何apt/dpkg进程在跑,否则可能损坏包数据库)。
sudo ps aux | grep -E "apt|dpkg"确认没有相关进程后:
sudo rm /var/lib/dpkg/lock sudo rm /var/lib/dpkg/lock-frontend sudo dpkg --configure -a再重新执行sudo apt install。
更安全的方式是等一会儿再试,因为auto-update之类的定时任务可能正在后台运行,杀掉它反而会引发其他问题。我个人的习惯是先看进程,确认没有才删锁。
4.3 IDE自带的GCC和系统GCC是一回事吗
不少嵌入式IDE,比如MounRiver Studio,会在安装目录里自带一套GCC工具链,用于交叉编译RISC-V或者8051等目标平台的固件。这个自带工具链和Ubuntu系统里/usr/bin下的gcc是两套完全独立的东西。
它们的位置也不一样。MounRiver Studio这类Eclipse系IDE,通常会在安装目录的plugin目录下找到一个toolchain相关路径,里面放着riscv-none-embed-gcc等交叉编译器。想在IDE里确认它实际使用的编译器位置,可以在工程的属性(Properties)里看C/C++ Build相关设置,里面一般会写清楚Compiler path。
这两套编译器互不干扰:IDE自带的工具链编译出来的文件是给MCU跑的,Ubuntu系统里的gcc编译出来的程序直接在电脑上运行。所以在Ubuntu上装系统的gcc/g++不影响IDE使用,反过来IDE装没装、装在哪,也不影响你终端里写C++。
4.4 在VS Code里怎么让C/C++代码跑起来
VS Code本身不是一个编译器,它只是一个编辑器。你在VS Code里装好微软的C/C++扩展后,它还需要知道用什么编译器来构建、调试代码。最直接的办法就是指向前面刚装好的/usr/bin/g++。
在VS Code里写C++程序,你需要两个关键文件。一个是.vscode/tasks.json,用于告诉VS Code怎么构建:
{ "version": "2.0.0", "tasks": [ { "label": "build cpp", "type": "shell", "command": "/usr/bin/g++", "args": [ "-g", "-std=c++17", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "group": { "kind": "build", "isDefault": true } } ] }另一个是.vscode/c_cpp_properties.json,让扩展知道编译器的位置和标准版本:
{ "configurations": [ { "name": "Linux", "includePath": ["${workspaceFolder}/**"], "compilerPath": "/usr/bin/g++", "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 }配置完后,按Ctrl+Shift+B就能调用g++编译当前文件,然后按F5进入调试。用命令本身是最佳调试思路,这也是为什么前面坚持要把命令行的GCC环境先弄明白,因为VS Code图形界面背后调用的还是命令行工具。
4.5 一编译就报undefined reference / iostream找不到,是没装好吗
这两个报错也是最容易让新手误以为“GCC没装好”的典型场景。
如果编译C++文件时提示fatal error: iostream: No such file or directory,多半是你确实在用gcc而不是g++来编译,或者系统里g++没装。C++标准库头文件不是C标准库的一部分,gcc默认搜索路径里不包含完整的C++标准库,所以找不到iostream很正常。
如果提示undefined reference to `std::cout'或者一堆libstdc++相关符号找不到,那是链接阶段的问题。gcc编译C++源文件时,不会自动把libstdc++链接进来,导致源代码编译通过,但最后链接失败。解决办法还是那句:C++程序使用g++来编译和链接。
5. 从编译器到开发环境:让GCC真正干活
5.1 一个完整的工具链组合
装完gcc/g++只是拿到了编译器,但一个像样的C/C++开发环境还需要构建工具和调试器。推荐一次性安装:
sudo apt install build-essential gdb make cmake -y- build-essential:包含gcc、g++、make等基础构建工具,前面提过。
- gdb:GNU调试器,程序出问题的时候可以在源代码级别断点调试。
- make:最常见的构建工具,通过Makefile管理编译流程。
- cmake:更高级的跨平台构建工具,广泛用于现代C++项目。
装完之后,你的Ubuntu环境才算真正具备“从源码到调试”的完整能力。
5.2 一个练习级C++小例子,验证整个环境下行
现在来编译一个稍微实用一点的示例,比如判断一个数是不是质数,这个也是搜索热词里出现过的需求。
#include <iostream> bool is_prime(int n) { if (n < 2) return false; for (int i = 2; i * i <= n; ++i) { if (n % i == 0) return false; } return true; } int main() { for (int i = 2; i <= 100; ++i) { if (is_prime(i)) { std::cout << i << " "; } } std::cout << std::endl; return 0; }保存为prime.cpp,编译运行:
g++ -O2 -std=c++17 prime.cpp -o prime ./prime这里用了-O2优化选项,编译器会做循环优化,让运行速度更快。-std=c++17指定C++版本标准,这在写现代C++代码时几乎是必须的,不然有些新特性用不了。
同样,如果你在练冒泡排序、二分查找这些经典算法,流程也都是一样:把代码存成.cpp文件,用g++编译,然后运行。这个流程本身不会因为算法复杂度改变而改变,编译器的主要工作是把你写的逻辑翻译成机器码,算法的好坏靠你自己来优化。
5.3 从单文件到多文件:什么时候需要CMake
当你的项目从一个.cpp文件变成十几个.cpp文件和一个头文件目录时,再手动敲g++命令就不现实了。这时候CMake就派上用场。
一个最简单的CMakeLists.txt长这样:
cmake_minimum_required(VERSION 3.16) project(cpp_demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(demo main.cpp sort.cpp utils.cpp)然后:
mkdir build cd build cmake .. make ./demo这里cmake ..会根据CMakeLists.txt生成Makefile,make会调用g++完成实际编译。在整个流程里,系统里的g++版本和路径决定了最终编译行为。
如果你刚切换过编译器版本,在build目录里重新cmake ..前,建议把build目录删掉或清掉CMakeCache.txt,否则CMake可能还在复用旧编译器缓存,又会遇到前面说的“新版本不生效”问题。
5.4 新手常犯的错觉:编辑器能运行就代表环境没问题
最后说一个很多人容易忽略的认知问题。你用VS Code、Clion或者某些IDE,在编辑器里点一下运行,程序正常运行了,就觉得自己电脑上的GCC环境完全没问题。这个结论在大多数情况下是对的,但要注意,IDE里可能内置了自己的编译器,或者帮你配置了不同于系统默认的编译配置路径。
我自己见过不少情况:在IDE里编译运行一切正常,但打开终端想执行g++编译却发现command not found,就是因为IDE内置了一套工具链,根本不依赖系统的gcc。对日常学习来说,这两者不冲突,但如果你想真正理解C/C++程序的编译流程,还是建议至少能在命令行里完成一次“写代码、编译、运行”的完整闭环。命令行能跑通,说明这个环境的底层是可靠的。
最后补两句踩过坑之后的心得
装GCC/G++这件事,看着是一行命令,真正的门槛在于理解“系统里有哪几套编译器、你现在调用的是哪一套”。在终端里敲which gcc和g++ --version只要几秒钟,但能省掉大半的疑难杂症。
另外,如果你经常需要在不同GCC版本之间切换,除了update-alternatives,还可以在自己的~/.bashrc里加上CC和CXX环境变量,比如:
export CC=gcc-13 export CXX=g++-13这样能保证像CMake这类构建工具会用你指定的版本来编译,而不至于又被系统默认版本带偏。这个办法不算什么高深技巧,但在实际开发里真的能减少很多“版本不对”的焦虑。