1. 先确认在干活的是哪个编译器:g++ 版本、gcc 版本、Ubuntu 版本三件事要分开看
前阵子一个朋友给我发来一段 C++20 代码,说在 Ubuntu 上编译直接报错,怎么改都不对。我第一反应不是帮他把代码改成 C++17,而是先问了一句:你现在用的默认 g++ 到底是哪个版本?他愣了一下,回头查了一下,发现系统里确实装了新版本,但每次直接敲g++编译时,走的一直是老的 9.4.0。这就是典型的“你知道有新版,但编译命令根本没用它”。
要在 Linux 上搞清楚“编译器支持 C++11、C++14、C++17 还是 C++20”,不能只看某篇文章里的支持表格,必须结合三样东西一起判断:当前默认的 g++ 版本、gcc 版本、Ubuntu 版本。后面两个会影响你能装到什么编译器,第一个才是真正参与编译的那个程序。
1.1 三条命令快速定位当前环境
我自己的习惯是先敲这三条命令,把环境底牌摸清楚:
g++ --version gcc --version cat /etc/os-release如果你用的是 Ubuntu,也可以加上lsb_release -a,不过有时候系统没装 lsb-release,报command not found。所以最稳妥的是直接看/etc/os-release,基本所有主流发行版都有。
以 Ubuntu 20.04 为例,输出大概是:
$ g++ --version g++ (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 Copyright (C) 2019 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.cat /etc/os-release里重点看VERSION_ID="20.04"或VERSION_CODENAME=focal,就知道系统底子有多老、官方源里默认给的编译器大概是哪个版本。
还有一个容易忽略的点:g++和gcc版本在正常情况下是配套的,但如果你只装了gcc,没装build-essential,那g++很可能直接不存在。反过来,有人通过源码手动编了一个新 gcc,但没有同步更新 g++,也会出现版本对不上的情况。所以你确认 C++ 编译器能力时,必须看g++ --version,不能只拿gcc --version替代。
1.2 g++ 和 gcc 的关系,为什么 C++ 必须看 g++
gcc 是 GNU Compiler Collection 里的 C 编译器,g++ 是 C++ 编译器前端。它们共用底层的 cc1/cc1plus 等组件,但在日常使用里,编译.cpp文件我强烈建议直接用g++。
原因很简单:g++在编译 C++ 源码后会默认链接 C++ 标准库libstdc++,而裸的gcc编译 C++ 源码不会自动带-lstdc++。你非要用gcc编 C++,常常会看到一堆undefined reference to std::cout这类链接错误。
所以下面所有关于 C++ 标准支持的验证,都以g++为准。gcc 版本可以作为参考,但别拿它替 g++ 做判断。
1.3 Ubuntu 版本基本决定了系统默认支持的极限
Ubuntu 的默认编译器版本是跟发行版走死的。Ubuntu 18.04 默认 g++ 7.5,Ubuntu 20.04 默认 g++ 9.4,Ubuntu 22.04 默认 g++ 11.4,Ubuntu 24.04 默认 g++ 13.3。这个表格的意义在于:如果你装完系统不额外折腾,直接apt install build-essential,装到的就是这些默认版本。
很多人项目报错,不是代码写得有问题,而是 Ubuntu 20.04 上的默认 g++ 9 对 C++20 支持还不完整。你硬要拿-std=c++20编译,确实能过一部分简单代码,但遇到std::ranges、std::span这类新标准库组件,或者协程、模块等语法,就会暴露各种版本问题。所以先判断系统版本,再判断编译器版本,然后才能判断该用哪个 C++ 标准,这个顺序不能乱。
2. 怎么直接测出编译器支持 C++11 / C++14 / C++17 / C++20
查 g++ 支持哪些 C++ 标准,最笨但也最可靠的办法就是“拿编译选项去试”。不同编译器对标准支持列表不一样,网上表格再全,也不如你本机跑一个命令来得准。
2.1 用空文件试探标准选项
Linux 下不需要真的写一个完整 C++ 源文件,直接用管道给 g++ 喂一个空程序就行:
echo 'int main(){}' | g++ -std=c++20 -x c++ -fsyntax-only -命令的意思是:把 stdin 当成 C++ 源代码,用-std=c++20选项做语法检查,不生成可执行文件。如果 g++ 支持这个标准选项,命令会安静地返回,退出码为 0;如果不支持,会报错,比如:
error: unrecognized command line option '-std=c++20'这就是最直接的结论:你的 g++ 连这个标准选项都不认,那它对这个标准的支持就是“完全不在考虑范围内”。
同样的方法可以依次测c++11、c++14、c++17:
echo 'int main(){}' | g++ -std=c++11 -x c++ -fsyntax-only - echo 'int main(){}' | g++ -std=c++14 -x c++ -fsyntax-only - echo 'int main(){}' | g++ -std=c++17 -x c++ -fsyntax-only -一条条敲太麻烦,我一般用循环跑一遍:
for std in c++11 c++14 c++17 c++20; do if echo 'int main(){}' | g++ -std=$std -x c++ -fsyntax-only - 2>/dev/null; then echo "$std: ok" else echo "$std: no" fi done输出会清楚告诉你当前 g++ 认哪些标准。
2.2 老版本编译器要试临时标准名
这里有个坑,很多人没意识到:GCC 在标准还没有正式定名之前,使用的是类似c++1y、c++1z、c++2a这样的临时名字。
- C++14 早期叫
c++1y - C++17 早期叫
c++1z - C++20 早期叫
c++2a - C++23 早期叫
c++2b
比如 Ubuntu 20.04 自带的 g++ 9.4,你直接试-std=c++20,可能报“unrecognized command line option”,但尝试-std=c++2a却能正常通过。这时候不能说“你的编译器完全不支持 C++20”,应该说“它只支持 C++20 的实验阶段选项,而且支持不完整”。
所以完整一点的测试脚本应该把临时名也纳入:
for std in c++11 c++14 c++1y c++17 c++1z c++20 c++2a; do if echo 'int main(){}' | g++ -std=$std -x c++ -fsyntax-only - 2>/dev/null; then echo "$std: ok" else echo "$std: no" fi done2.3 标准选项通过,不等于“完整支持”
这里必须说清楚一个容易误解的地方:g++ -std=c++20能通过语法检查,只能说明编译器认这个标准选项,不能说明标准库和语言特性的所有功能都完整实现了。
C++20 的功能跨度很大:概念(concepts)、协程(coroutines)、模块(modules)、std::span、std::ranges、std::format这些都是 C++20 的内容,但 GCC 11 和 GCC 13 对它们的支持程度完全不在一个档次。所以“支持 C++20”这句话,要分“编译器认识这个选项”“语言核心特性基本能编译”“标准库完整实现”三个层级来看。
3. 更硬的证据:用__cplusplus宏看编译器实际启用的标准
编译选项能测出来“编译器认不认”,但有时候你写 Makefile 时没有指定任何-std,这时 g++ 用的是默认标准。默认标准是什么?老版本 GCC 可能是 C++14,GCC 11 之后是 C++17。你光查“支持哪些标准”还不够,还得知道“当前编译命令实际用了哪个标准”。
__cplusplus宏就是干这个用的。它由编译器内置定义,是判断当前编译模式最可靠的证据。
3.1 用 -dM -E 直接导出宏
Linux 下查看预处理器输出的宏,用-dM -E参数:
echo | g++ -dM -E -x c++ - | grep __cplusplusUbuntu 20.04 默认 g++ 9.4 的输出大概是这样:
#define __cplusplus 201402L201402L对应 C++14。也就是说,你不指定任何-std选项时,编译器按 C++14 标准在编译。这正好解释了为什么很多 C++17/C++20 的代码拿到手上,直接g++ main.cpp -o main会编译失败。
如果你手动指定标准:
echo | g++ -std=c++17 -dM -E -x c++ - | grep __cplusplus输出会变成:
#define __cplusplus 201703L__cplusplus的值和 C++ 标准对应关系如下:
| 宏值 | 对应标准 |
|---|---|
| 199711L | C++98 |
| 201103L | C++11 |
| 201402L | C++14 |
| 201703L | C++17 |
| 202002L | C++20 |
注意有些实验阶段的编译器,比如 GCC 9 用-std=c++2a编译时,__cplusplus可能还是 201703L 或其它早期值,不能完全代表最终标准。遇到这种情况,还是要结合 g++ 版本来判断。
3.2 在代码里用宏做分支判断
如果你希望代码在不同标准下都能编译,或者想在程序运行时打印当前标准,可以直接用__cplusplus做条件判断:
#include <iostream> int main() { #if __cplusplus == 202002L std::cout << "C++20\n"; #elif __cplusplus == 201703L std::cout << "C++17\n"; #elif __cplusplus == 201402L std::cout << "C++14\n"; #elif __cplusplus == 201103L std::cout << "C++11\n"; #else std::cout << "pre-C++11\n"; #endif return 0; }编译时如果不带-std,输出结果就是默认标准。带上-std=c++20后,输出会变成C++20。这个方式比单纯跑g++ --version更准确,因为它反映的是“当前这次编译实际用的标准”,而不是“编译器理论上支持的标准”。
3.3 GNU 扩展标准gnu++17和c++17的区别
GCC 还提供-std=gnu++17、-std=gnu++20这类选项,它们会开启 GNU 扩展,比如变长数组、typeof 之类。gnu++17和c++17在__cplusplus宏上是一样的,都是 201703L,但实际编译行为有差别。
如果你写的代码非常依赖跨平台一致性,我建议优先用严格的-std=c++20,而不要用gnu++20。开 GNU 扩展长期来看会让代码越来越“绑定编译器”,以后换 MSVC 或 Clang 时会很痛苦。
4. GCC 版本、Ubuntu 版本和 C++ 标准支持对照表
把版本对应关系放进一张表里,会比记零散经验更清楚。下面这张表是我在实际项目中常用的判断依据,但它只能作为“起步参考”,最终仍要以你本机g++ --version的结果为准。
4.1 常见 GCC 版本和 C++ 标准支持范围
| g++ 版本 | 常见来源 | 默认标准 | 能稳定使用的标准 | 实验/部分支持 |
|---|---|---|---|---|
| g++ 7.5 | Ubuntu 18.04 | gnu++14 | C++11、C++14 | C++17 部分,C++20 基本没有 |
| g++ 9.4 | Ubuntu 20.04 | gnu++14 | C++11、C++14、C++17 | C++20 用 c++2a 部分支持 |
| g++ 11.4 | Ubuntu 22.04 | gnu++17 | C++11、C++14、C++17、C++20 | C++23 实验支持 |
| g++ 13.3 | Ubuntu 24.04 | gnu++17 | C++11、C++14、C++17、C++20 | C++23 部分支持 |
注意“稳定使用”和“部分支持”之间不是非黑即白的。比如 g++ 9 用-std=c++2a能编一些 C++20 的简单代码,但遇到 ranges、协程、模块这类重功能就会掉链子。生产项目我一般不会在 g++ 9 上开 C++20。
4.2 怎么看 Ubuntu 仓库里有没有更高版本的 g++
如果你当前 Ubuntu 版本自带 g++ 9,但你需要编译 C++20 项目,最简单的办法是装更高版本的 g++。先看仓库里有哪些版本:
apt-cache policy g++-12 g++-13Ubuntu 20.04 官方源里不一定有很新的版本,通常需要加ubuntu-toolchain-r/test这个 PPA 才能装到 g++-12、g++-13。当然,装之前要先确认你的系统和软件源访问正常,然后再执行:
sudo add-apt-repository ppa:ubuntu-toolchain-r/test sudo apt update sudo apt install g++-12装完后,系统里会多出一个/usr/bin/g++-12,但默认的g++不会自动切换过去。这正是很多人踩坑的地方。
4.3 “安装新版编译器”和“默认使用新版编译器”是两回事
这句话值得单独拿出来讲:在 Ubuntu 上,apt install g++-12只是把 g++-12 这个程序放进系统,并不会自动让g++变成 g++-12。默认的g++是哪个,还要看/usr/bin/g++这个符号链接指向谁。
所以很多人查g++ --version看到还是老版本,就误以为装失败了。其实新版就在/usr/bin/g++-12等着你用,只是你没把它设成默认。后面的章节我会专门讲怎么切换和固定编译器版本。
5. 不指定 g++ 版本时,系统到底是怎么选到老版本的
标题里那句“编译时不指定g++版本,默认使用老版本编译”是很多人的痛点。要解决它,得先理解 Linux 下g++这个命令的本质。
5.1 g++ 是一个符号链接,不是本体
在 Ubuntu 上执行:
which g++ readlink -f /usr/bin/g++你会看到/usr/bin/g++最终指向一个带版本号的真实编译器,比如/usr/bin/g++-9。也就是说,你敲g++的时候,真正执行的是/usr/bin/g++-9。系统里同时存在 g++-10、g++-12 时,它们互不影响,关键只是/usr/bin/g++这个入口指向谁。
5.2 update-alternatives 控制默认版本
Debian/Ubuntu 系的编译器版本切换,通常由update-alternatives管理。查看当前有哪些可选的 g++:
update-alternatives --list g++如果系统已经注册了多个版本,可以直接交互式切换:
sudo update-alternatives --config g++如果列表里没有你想要的版本,需要手动注册。比如把 g++-12 注册进 alternatives:
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 100其中100是优先级,数字越大越优先。注册完再执行sudo update-alternatives --config g++选择默认版本。切换后一定要再跑一次g++ --version确认,避免改了个寂寞。
5.3 PATH 顺序也会插一脚
比 alternatives 更隐蔽的是 PATH 顺序。Linux 寻找命令时,按 PATH 环境变量里目录的顺序逐个查找,谁先找到就用谁。
type -a g++这条命令会列出所有能匹配到g++的路径。如果输出里有/usr/local/bin/g++,而且它排在/usr/bin/g++前面,那么你敲g++时执行的可能是/usr/local/bin/g++,它不一定受 update-alternatives 控制,版本也完全是另一套。
我曾经遇到一种情况:有人从源码编译安装了新版 GCC 到/usr/local,但 PATH 里/usr/local/bin在前面,于是系统里明明有/usr/bin/g++-10,可默认g++还是源码安装的老版本。所以排查时一定用type -a g++看全所有位置,别只盯着/usr/bin/g++。
5.4 最稳妥的编译方式:显式指定完整路径或环境变量
如果你不想动系统默认的 alternatives,也不想被 PATH 干扰,最直接的办法是在编译命令里写完整路径:
/usr/bin/g++-12 -std=c++20 main.cpp -o mainMakefile 里可以固定编译器:
CXX = /usr/bin/g++-12 CXXFLAGS = -std=c++20 -Wall -O2CMake 项目则在配置阶段指定:
cmake -S . -B build -DCMAKE_CXX_COMPILER=/usr/bin/g++-12 -DCMAKE_CXX_STANDARD=20 -DCMAKE_CXX_STANDARD_REQUIRED=ON cmake --build build这里有个小警告:如果之前已经用旧编译器配置过 CMake 的 build 目录,直接改CMAKE_CXX_COMPILER不一定生效,缓存里可能还留着旧编译器路径。遇到这种情况,干净利落一点,把 build 目录删掉重新配置:
rm -rf build cmake -S . -B build -DCMAKE_CXX_COMPILER=/usr/bin/g++-12 ...6. 实际验证:让编译器自己说支持什么
说了这么多,最终还是要落到“本机怎么验证”。我自己最喜欢的方式是写一个小脚本,把所有标准和临时名都跑一遍,让 g++ 自己交底。下面这个脚本可以直接复制到你的 Linux 环境里执行:
g++ --version for std in c++11 c++14 c++1y c++17 c++1z c++20 c++2a; do if echo 'int main(){}' | g++ -std=$std -x c++ -fsyntax-only - 2>/dev/null; then echo "$std: supported" else echo "$std: not supported" fi done在 Ubuntu 20.04 默认 g++ 9.4 上,输出大概是这样:
g++ (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 c++11: supported c++14: supported c++1y: supported c++17: supported c++1z: supported c++20: not supported c++2a: supported同一个 Ubuntu 系统,切换默认编译器到 g++-10 或 g++-11 后再跑一遍,输出里c++20就会变成 supported。注意这个脚本只是验证“编译器认不认这个标准选项”,仍然不能证明所有 C++20 库函数都可用。
要看标准库是不是真的够新,可以试试直接编译一个引用新标准库头文件的空程序,比如:
echo '#include <ranges>' | g++ -std=c++20 -x c++ -fsyntax-only -如果头文件缺失,g++ 会直接报fatal error: ranges: No such file or directory,说明编译器虽然认 C++20,但标准库配套没跟上。这种情况常见于只升级了 gcc/g++ 前端,却没有升级对应的 libstdc++ 头文件和库文件。
判断标准库头文件版本,另一个朴素但有效的办法是看/usr/include/c++/下有哪些子目录。比如/usr/include/c++/9、/usr/include/c++/11,数字就对应 GCC 版本。如果只有 9,但你用 g++-11 编译,头文件路径可能还需要额外指定,或者说明你的 libstdc++-11-dev 没装完整。
所以完整流程应该是:
- 用
g++ --version确定默认编译器。 - 用标准测试脚本确定编译器认哪些
-std选项。 - 用
__cplusplus宏确认当前实际使用的标准。 - 用真实代码验证新标准库头文件能不能用。
- 通过 update-alternatives 或显式路径切换到目标版本。
这套流程走下来,基本不会再出现“代码没错,却编译失败”的误判。我个人现在的习惯是:不管项目大小,只要涉及 C++17 以上特性,第一件事就是把编译命令里的编译器写死成绝对路径,比如/usr/bin/g++-12,然后用__cplusplus打印验证。多花这十几秒,能省掉后面一晚上的排查时间。