Ubuntu下g++版本与C++标准支持检测及切换方法
1. 先确认在干活的是哪个编译器g 版本、gcc 版本、Ubuntu 版本三件事要分开看前阵子一个朋友给我发来一段 C20 代码说在 Ubuntu 上编译直接报错怎么改都不对。我第一反应不是帮他把代码改成 C17而是先问了一句你现在用的默认 g 到底是哪个版本他愣了一下回头查了一下发现系统里确实装了新版本但每次直接敲g编译时走的一直是老的 9.4.0。这就是典型的“你知道有新版但编译命令根本没用它”。要在 Linux 上搞清楚“编译器支持 C11、C14、C17 还是 C20”不能只看某篇文章里的支持表格必须结合三样东西一起判断当前默认的 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_ID20.04或VERSION_CODENAMEfocal就知道系统底子有多老、官方源里默认给的编译器大概是哪个版本。还有一个容易忽略的点g和gcc版本在正常情况下是配套的但如果你只装了gcc没装build-essential那g很可能直接不存在。反过来有人通过源码手动编了一个新 gcc但没有同步更新 g也会出现版本对不上的情况。所以你确认 C 编译器能力时必须看g --version不能只拿gcc --version替代。1.2 g 和 gcc 的关系为什么 C 必须看 ggcc 是 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.5Ubuntu 20.04 默认 g 9.4Ubuntu 22.04 默认 g 11.4Ubuntu 24.04 默认 g 13.3。这个表格的意义在于如果你装完系统不额外折腾直接apt install build-essential装到的就是这些默认版本。很多人项目报错不是代码写得有问题而是 Ubuntu 20.04 上的默认 g 9 对 C20 支持还不完整。你硬要拿-stdc20编译确实能过一部分简单代码但遇到std::ranges、std::span这类新标准库组件或者协程、模块等语法就会暴露各种版本问题。所以先判断系统版本再判断编译器版本然后才能判断该用哪个 C 标准这个顺序不能乱。2. 怎么直接测出编译器支持 C11 / C14 / C17 / C20查 g 支持哪些 C 标准最笨但也最可靠的办法就是“拿编译选项去试”。不同编译器对标准支持列表不一样网上表格再全也不如你本机跑一个命令来得准。2.1 用空文件试探标准选项Linux 下不需要真的写一个完整 C 源文件直接用管道给 g 喂一个空程序就行echo int main(){} | g -stdc20 -x c -fsyntax-only -命令的意思是把 stdin 当成 C 源代码用-stdc20选项做语法检查不生成可执行文件。如果 g 支持这个标准选项命令会安静地返回退出码为 0如果不支持会报错比如error: unrecognized command line option -stdc20这就是最直接的结论你的 g 连这个标准选项都不认那它对这个标准的支持就是“完全不在考虑范围内”。同样的方法可以依次测c11、c14、c17echo int main(){} | g -stdc11 -x c -fsyntax-only - echo int main(){} | g -stdc14 -x c -fsyntax-only - echo int main(){} | g -stdc17 -x c -fsyntax-only -一条条敲太麻烦我一般用循环跑一遍for std in c11 c14 c17 c20; 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 在标准还没有正式定名之前使用的是类似c1y、c1z、c2a这样的临时名字。C14 早期叫c1yC17 早期叫c1zC20 早期叫c2aC23 早期叫c2b比如 Ubuntu 20.04 自带的 g 9.4你直接试-stdc20可能报“unrecognized command line option”但尝试-stdc2a却能正常通过。这时候不能说“你的编译器完全不支持 C20”应该说“它只支持 C20 的实验阶段选项而且支持不完整”。所以完整一点的测试脚本应该把临时名也纳入for std in c11 c14 c1y c17 c1z c20 c2a; 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 -stdc20能通过语法检查只能说明编译器认这个标准选项不能说明标准库和语言特性的所有功能都完整实现了。C20 的功能跨度很大概念concepts、协程coroutines、模块modules、std::span、std::ranges、std::format这些都是 C20 的内容但 GCC 11 和 GCC 13 对它们的支持程度完全不在一个档次。所以“支持 C20”这句话要分“编译器认识这个选项”“语言核心特性基本能编译”“标准库完整实现”三个层级来看。3. 更硬的证据用__cplusplus宏看编译器实际启用的标准编译选项能测出来“编译器认不认”但有时候你写 Makefile 时没有指定任何-std这时 g 用的是默认标准。默认标准是什么老版本 GCC 可能是 C14GCC 11 之后是 C17。你光查“支持哪些标准”还不够还得知道“当前编译命令实际用了哪个标准”。__cplusplus宏就是干这个用的。它由编译器内置定义是判断当前编译模式最可靠的证据。3.1 用 -dM -E 直接导出宏Linux 下查看预处理器输出的宏用-dM -E参数echo | g -dM -E -x c - | grep __cplusplusUbuntu 20.04 默认 g 9.4 的输出大概是这样#define __cplusplus 201402L201402L对应 C14。也就是说你不指定任何-std选项时编译器按 C14 标准在编译。这正好解释了为什么很多 C17/C20 的代码拿到手上直接g main.cpp -o main会编译失败。如果你手动指定标准echo | g -stdc17 -dM -E -x c - | grep __cplusplus输出会变成#define __cplusplus 201703L__cplusplus的值和 C 标准对应关系如下宏值对应标准199711LC98201103LC11201402LC14201703LC17202002LC20注意有些实验阶段的编译器比如 GCC 9 用-stdc2a编译时__cplusplus可能还是 201703L 或其它早期值不能完全代表最终标准。遇到这种情况还是要结合 g 版本来判断。3.2 在代码里用宏做分支判断如果你希望代码在不同标准下都能编译或者想在程序运行时打印当前标准可以直接用__cplusplus做条件判断#include iostream int main() { #if __cplusplus 202002L std::cout C20\n; #elif __cplusplus 201703L std::cout C17\n; #elif __cplusplus 201402L std::cout C14\n; #elif __cplusplus 201103L std::cout C11\n; #else std::cout pre-C11\n; #endif return 0; }编译时如果不带-std输出结果就是默认标准。带上-stdc20后输出会变成C20。这个方式比单纯跑g --version更准确因为它反映的是“当前这次编译实际用的标准”而不是“编译器理论上支持的标准”。3.3 GNU 扩展标准gnu17和c17的区别GCC 还提供-stdgnu17、-stdgnu20这类选项它们会开启 GNU 扩展比如变长数组、typeof 之类。gnu17和c17在__cplusplus宏上是一样的都是 201703L但实际编译行为有差别。如果你写的代码非常依赖跨平台一致性我建议优先用严格的-stdc20而不要用gnu20。开 GNU 扩展长期来看会让代码越来越“绑定编译器”以后换 MSVC 或 Clang 时会很痛苦。4. GCC 版本、Ubuntu 版本和 C 标准支持对照表把版本对应关系放进一张表里会比记零散经验更清楚。下面这张表是我在实际项目中常用的判断依据但它只能作为“起步参考”最终仍要以你本机g --version的结果为准。4.1 常见 GCC 版本和 C 标准支持范围g 版本常见来源默认标准能稳定使用的标准实验/部分支持g 7.5Ubuntu 18.04gnu14C11、C14C17 部分C20 基本没有g 9.4Ubuntu 20.04gnu14C11、C14、C17C20 用 c2a 部分支持g 11.4Ubuntu 22.04gnu17C11、C14、C17、C20C23 实验支持g 13.3Ubuntu 24.04gnu17C11、C14、C17、C20C23 部分支持注意“稳定使用”和“部分支持”之间不是非黑即白的。比如 g 9 用-stdc2a能编一些 C20 的简单代码但遇到 ranges、协程、模块这类重功能就会掉链子。生产项目我一般不会在 g 9 上开 C20。4.2 怎么看 Ubuntu 仓库里有没有更高版本的 g如果你当前 Ubuntu 版本自带 g 9但你需要编译 C20 项目最简单的办法是装更高版本的 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管理。查看当前有哪些可选的 gupdate-alternatives --list g如果系统已经注册了多个版本可以直接交互式切换sudo update-alternatives --config g如果列表里没有你想要的版本需要手动注册。比如把 g-12 注册进 alternativessudo 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 -stdc20 main.cpp -o mainMakefile 里可以固定编译器CXX /usr/bin/g-12 CXXFLAGS -stdc20 -Wall -O2CMake 项目则在配置阶段指定cmake -S . -B build -DCMAKE_CXX_COMPILER/usr/bin/g-12 -DCMAKE_CXX_STANDARD20 -DCMAKE_CXX_STANDARD_REQUIREDON 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 c11 c14 c1y c17 c1z c20 c2a; 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 c11: supported c14: supported c1y: supported c17: supported c1z: supported c20: not supported c2a: supported同一个 Ubuntu 系统切换默认编译器到 g-10 或 g-11 后再跑一遍输出里c20就会变成 supported。注意这个脚本只是验证“编译器认不认这个标准选项”仍然不能证明所有 C20 库函数都可用。要看标准库是不是真的够新可以试试直接编译一个引用新标准库头文件的空程序比如echo #include ranges | g -stdc20 -x c -fsyntax-only -如果头文件缺失g 会直接报fatal error: ranges: No such file or directory说明编译器虽然认 C20但标准库配套没跟上。这种情况常见于只升级了 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 或显式路径切换到目标版本。这套流程走下来基本不会再出现“代码没错却编译失败”的误判。我个人现在的习惯是不管项目大小只要涉及 C17 以上特性第一件事就是把编译命令里的编译器写死成绝对路径比如/usr/bin/g-12然后用__cplusplus打印验证。多花这十几秒能省掉后面一晚上的排查时间。