在 Linux 下做 C++ 开发,最让新手摸不着头脑的问题之一,就是“我的编译器到底支持 C++ 哪个版本”。尤其当你兴致勃勃地写了段 C++17 甚至 C++20 风格的代码,编译时却被一堆语法错误砸脸,或者更隐蔽的——编译器根本没报错,但新特性就是不生效。而究其原因,很多时候都卡在同一个点上:系统默认调用的 g++ 版本太老,或者没主动指定编译标准。
我知道你看到“支持 C++11、支持 C++14、支持 C++17、支持 C++20”这一串就头大,特别是刚把代码从 Windows 的 Visual Studio 迁到 Ubuntu 上,或者在一台配置了老旧开发环境的服务器上调试。这篇文章就是想帮你把“查看工具链版本、确认默认标准、手动指定 C++ 版本、解决版本冲突”这一整条线走通。不废话,直接上干货,所有命令我都按“开箱即用”的标准给你,跟着敲就行。
本文的核心逻辑是“三步走”:先确认你的 Ubuntu、gcc、g++ 版本号,再验证当前编译器默认用哪个 C++ 标准,最后通过显式指定 -std 参数来编译对应标准的代码。如果你发现自己需要升级编译器才能支持 C++20,或者因为默认版本太老导致项目编不过,这篇也能给你排查思路。
1. 先把家底摸清楚:Ubuntu、gcc、g++ 三者版本怎么看
很多人误以为“Ubuntu 版本”就等于“开发环境版本”,这是两码事。Ubuntu 的发行版本决定的是系统库的基线,而 gcc/g++ 是独立安装的软件包,它们有各自的发布节奏。比如 Ubuntu 20.04 默认带的是 gcc 9,Ubuntu 22.04 默认带的是 gcc 11,但你完全可以手动装一个 gcc 13 上去,这并不冲突。
1.1 查看 Ubuntu 系统版本
系统版本决定了你的 apt 源里默认有哪些软件包版本,也决定了系统库(如 libstdc++6)的版本基线。查看命令很简单:
lsb_release -a或者:
cat /etc/os-release第一条命令会输出类似这样的内容:
Distributor ID: Ubuntu Description: Ubuntu 20.04.6 LTS Release: 20.04 Codename: focal注意Codename,它决定了你后续如果需要添加 PPA 或换源,该用哪个代号。20.04 是focal,22.04 是jammy,这个后面排查某些安装问题时会用到。
1.2 查看 gcc 和 g++ 版本
这两个命令看起来差不多,但很多人会忽略一个坑:gcc 管的是 C 语言,g++ 管的是 C++,虽然是同一个工具链,但两者版本可能不一致。如果系统里装了多个版本的编译器,一定要分别确认:
gcc --version g++ --version输出里会明确写出版本号,比如:
g++ (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0这一行就已经把信息说全了:发行方是 Ubuntu,版本是 9.4.0。
提示:别只看第一行。在 Debian/Ubuntu 系里,
gcc和g++可能是被 update-alternatives 管理的,你敲gcc调用的未必是默认安装的那个版本。如果感觉版本不对,用which gcc和which g++看一下实际的二进制路径。
1.3 查看当前实际使用哪个编译器
这里要区分两个概念:gcc这个名字指向的默认版本,和你项目里 Makefile 或 CMake 实际调用的编译器,可能是两套东西。用which和ls -l能看出端倪:
which g++ ls -l /usr/bin/g++如果输出显示/usr/bin/g++ -> g++-9,说明g++这个命令实际指向的是 g++ 9。同理检查 gcc:
ls -l /usr/bin/gcc这个软链关系,就是“我明明安装了新版,但编译时用的却是老版本”的罪魁祸首。后面第五节我会专门讲怎么切换。
我个人的习惯是,每到一个新环境,先把这三条命令敲一遍,然后存到项目的 README 里。因为 C++ 项目的“可复现性”往往就卡在编译环境差异上,同一套代码在 g++ 9 和 g++ 13 下的表现可能完全不同。
2. 关键问题:编译器默认使用哪种 C++ 标准
你以为“安装了 g++ 9 就默认支持 C++17 标准”,实际上这是个误区。编译器支持某个标准,和编译器默认启用某个标准,完全是两个概念。GCC 从某个大版本开始就具备支持 C++17 的能力,但为了让老项目不被新特性冲击,默认标准通常会“落后一档”。
2.1 GCC 不同版本的默认 C++ 标准
根据 GCC 官方的发布说明,GCC 各版本的默认标准变化大概是这样的:
| GCC 版本 | 默认 C++ 标准 | 支持的最高标准(可手动开启) |
|---|---|---|
| GCC 5 及以前 | C++98 | C++14(部分) |
| GCC 6 | C++14 | C++17(实验性) |
| GCC 7 | C++14 | C++17 |
| GCC 8 | C++14 | C++17 |
| GCC 9 | C++14 | C++20(实验性) |
| GCC 10 | C++14 | C++20 |
| GCC 11 | C++17 | C++23(部分) |
| GCC 12 | C++17 | C++23(部分) |
| GCC 13 | C++17 | C++23(部分) |
从这个表能看出一个规律:GCC 的默认标准通常比它能支持的最高标准低一到两个大版本。所以,你安装了一个“支持 C++20 的 GCC 9”,但如果编译时不指定-std=c++20,它用的仍然是 C++14 标准。
这就是我开头说的“最隐蔽的坑”:代码没报错,但新特性的行为和你预期不一致,或者某些新语法直接报错。原因不是你编译器不支持,而是你根本没打开对应的开关。
2.2 通过编译宏确认当前默认标准
想要确认“当前编译器默认启用哪个标准”,最直接的办法是看__cplusplus这个预定义宏的值。写个一行代码就能验证:
#include <iostream> int main() { std::cout << __cplusplus << std::endl; return 0; }然后直接编译运行:
g++ check.cpp -o check ./check输出的数字和 C++ 版本的对应关系如下:
| 宏的值 | C++ 标准版本 |
|---|---|
| 199711L | C++98 |
| 201103L | C++11 |
| 201402L | C++14 |
| 201703L | C++17 |
| 202002L | C++20 |
| 202302L | C++23 |
比如输出201402L,就说明 g++ 默认真的是 C++14 标准。
2.3 不同标准下的编译差异示例
为了加深印象,咱们用一个代码来实测一下。下面这段代码用了 C++17 才有的结构化绑定(structured binding)特性:
#include <iostream> #include <map> int main() { std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; for (auto& [key, value] : m) { std::cout << key << ": " << value << std::endl; } return 0; }直接编译不指定标准,如果你的 g++ 版本默认是 C++14,就会报这样的错:
error: 'auto' can't be used as a structured binding declaration error: 'value' was not declared in this scope这个报错曾经让很多人一头雾水:明明我用的 g++ 是支持 C++17 的版本啊?对,它支持,但它默认不用。你必须在编译命令里告诉它,比如-std=c++17,编译就通过了。这也解释了为什么很多时候“用新版编译器编译老代码反而崩了”,大概率就是新标准下的语法解析变化导致的。
3. 手动指定 C++ 版本编译:-std 参数详解
当你明确知道自己的代码要用哪个 C++ 标准后,最稳妥的做法不是依赖编译器默认值,而是在编译命令中显式指定标准。
3.1 最常用的 -std 写法
对于 GCC(g++)来说,常用的参数有:
g++ -std=c++11 main.cpp -o main g++ -std=c++14 main.cpp -o main g++ -std=c++17 main.cpp -o main g++ -std=c++20 main.cpp -o main就这么简单。编译时把这个参数加上,编译器就会用对应的标准去解析你的代码。
3.2 关于“c++1x”“c++2a”这类旧代号
老手可能见过有人用-std=c++1z或者-std=c++2a这种写法。这其实是标准还没正式发布时的“临时编号”:c++1x 指代 C++11,c++1y 指代 C++14,c++1z 指代 C++17,c++2a 指代 C++20。在现代 GCC 版本里,这些别名仍然有效,但我不推荐你写。理由很简单:代码是给别人看的,也是给未来的自己维护的,写-std=c++17比写-std=c++1z清晰一百倍,而且新版 GCC 随时可能移除这些旧编号。
3.3 如果编译器不支持指定标准怎么办
如果你执行g++ -std=c++20 main.cpp -o main时收到了这样的错误:
error: unrecognized command line option ‘-std=c++20’这基本可以断定,你的 g++ 版本太老,压根不认识 C++20 这个编号。GCC 对 C++20 的实验性支持从 GCC 8 开始,正式支持从 GCC 10 开始。如果你的编译器连-std=c++20这个参数都不认,先查版本号(第二节的命令),然后要么升级编译器,要么改用低版本标准。
3.4 代码里怎么判断当前编译标准
有时候你需要在代码层面做条件编译。比如,一段代码在 C++17 下用std::clamp,在 C++14 下用自定义实现。这时可以借助__cplusplus宏做判断:
#if __cplusplus >= 201703L // C++17 及以上的写法 #include <algorithm> int clamped = std::clamp(value, low, high); #else // 兼容老标准的写法 int clamped = value < low ? low : (value > high ? high : value); #endif__cplusplus是编译器内置的宏,你只管用,不用额外定义。
我强烈建议所有 C++ 项目在编译脚本、Makefile 或 CMakeLists.txt 里明确写出-std参数。为什么?因为“默认标准”这个东西是跟着编译器版本走的,你在 g++ 9 上编译通过的代码,到 g++ 11 上再编译一次,可能因为默认标准从 C++14 变成了 C++17,而产生行为差异。只有显式指定,才能保证“一处配置,处处一致”。
4. 框架级编译配置:Makefile 与 CMake 指定 C++ 标准
上面的命令行方式适合单个文件或临时验证。实际项目都会用构建工具,这里得分开讲。
4.1 Makefile 里怎么指定
在 Makefile 中,关键变量是CXXFLAGS。注意,编译 C++ 程序时对应的隐式规则用的变量是CXXFLAGS和CXX,不是CFLAGS(那是给 C 语言用的)。一个最简示例:
CXX = g++ CXXFLAGS = -std=c++17 -Wall -O2 main: main.cpp $(CXX) $(CXXFLAGS) main.cpp -o main这里有个特别容易踩的坑:如果你在 Makefile 里用cc或gcc去编译.cpp文件,编译器根本不会按 C++ 语法来链接标准库,会报一堆undefined reference to std::cout之类的链接错误。必须用g++或设置CXX变量。很多老掉牙的 Makefile 模板没改这两行,导致新项目一上来就编译不过。
4.2 CMake 里怎么指定
如果你是 CMake 用户,指定 C++ 标准的方式更“现代化”,推荐使用target_compile_features或CMAKE_CXX_STANDARD:
cmake_minimum_required(VERSION 3.10) project(MyProject LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_executable(main main.cpp)逐行解释一下:
CMAKE_CXX_STANDARD 17:告诉 CMake 打开 C++17 特性。CMAKE_CXX_STANDARD_REQUIRED ON:如果编译器不支持 C++17,直接报错,而不是静默降级到低版本。这个非常重要,否则 CMake 会在编译器不支持时自动退回低标准,你根本发现不了。CMAKE_CXX_EXTENSIONS OFF:关闭-std=gnu++17的默认行为。GNU 扩展在某些时候很有用,但也可能带来兼容性问题。建议一律 OFF,除非你明确需要 GNU 扩展。
CMake 这种方式比 Makefile 更优雅的地方在于:它会根据你填的标准版本,自动选择-std=c++17还是-std=gnu++17,不需要你手写编译参数。
4.3 一个典型的编译参数组合
实际项目里,我一般会这样组合参数:
g++ -std=c++17 -O2 -Wall -Wextra -pedantic main.cpp -o main这里每项参数的作用:
| 参数 | 作用 |
|---|---|
-std=c++17 | 启用 C++17 标准 |
-O2 | 开启二级优化,兼顾速度与编译时间 |
-Wall -Wextra | 开启常见警告,提前发现潜在 bug |
-pedantic | 严格遵循标准,对 GNU 扩展给出警告 |
-pedantic这个参数对新人有迷惑性——它是把双刃剑。如果你在代码里用了typeof这类 GNU 扩展,加上-pedantic会报一堆警告;但如果你想确保代码具有良好的跨编译器移植性,它又是必备工具。
5. 场景实测:从零验证一个环境的 C++ 支持能力
纸上谈兵没意思,咱们走一遍完整流程。假设我在一台全新的 Ubuntu 20.04 上,要把一段 C++20 代码编译成可执行文件。
5.1 第一步:确认工具链版本
先执行查看命令:
$ g++ --version g++ (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0看到 g++ 9.3.0。根据前文表格,这个版本的默认标准是 C++14,最高能开 C++20。注意是“实验性支持”,也就是说有些 C++20 特性可能没实现全。
5.2 第二步:确认默认标准
创建测试文件:
cat > check.cpp << 'EOF' #include <iostream> int main() { std::cout << __cplusplus << std::endl; return 0; } EOF编译运行:
$ g++ check.cpp -o check $ ./check 201402L输出 201402L,验证了默认是 C++14。
5.3 第三步:试用 C++20 标准
写一个小的 C++20 测试,比如用concepts或者ranges库。我用std::span做演示,这是 C++20 引入的连续对象序列视图:
#include <iostream> #include <span> #include <vector> void print_sum(std::span<const int> s) { int sum = 0; for (int x : s) sum += x; std::cout << sum << std::endl; } int main() { std::vector<int> v = {1, 2, 3, 4, 5}; print_sum(v); return 0; }用 C++20 编译:
g++ -std=c++20 test_span.cpp -o test_span在 g++ 9.3 上,这段代码大概率会编译失败,原因可能是<span>头文件的实现还不完整。这印证了前面说的:GCC 9 对 C++20 是“实验性支持”,别指望所有功能都能用。而 GCC 10 及以后对 C++20 的支持才算比较完整。
5.4 第四步:遇到“默认版本太旧”怎么办
如果说第三、四步排查下来,你发现当前 g++ 压根不支持你需要的标准,那就必须升级编译器。前面步骤的排查顺序,本质上是让你定位问题到底出在“没指定标准”还是“编译器太老”。
在 Ubuntu 20.04 上,手动装新版 g++ 的常见做法是加 Ubuntu Toolchain 的 PPA:
sudo add-apt-repository ppa:ubuntu-toolchain-r/test sudo apt update sudo apt install g++-13装完之后,系统里会多出一个g++-13命令,和老的g++命令共存。此时你可以用g++-13 -std=c++20 test_span.cpp编译,这样不会影响系统默认的g++,避免破坏其他依赖老版本编译器的项目。
注意:
add-apt-repository在某些精简版系统上可能不存在,需要先安装software-properties-common。另外,添加 PPA 后升级系统组件有一定风险,生产环境建议先在虚拟机或容器里验证。
5.5 用 update-alternatives 管理默认版本
如果你希望把g++命令默认指向新版,可以用update-alternatives配置优先级。先执行:
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 100然后查看可选项:
sudo update-alternatives --config g++这会进入交互式界面,让你选择默认的 g++。选完之后,再执行g++ --version确认。
我建议普通项目不要随便动这个默认切换,因为你把系统默认 g++ 换掉后,某些依赖旧编译器构建的第三方库可能重新编译时出现诡异问题。按需用g++-13指定编译,比全局切换安全得多。
6. 新手最容易踩的 5 个坑:排查与避坑指南
到了我这部分,就是纯粹拿钱买不来的经验了。以下 5 个问题是 C++ 开发者在 Linux 下被问得最多的,挨个说清楚。
6.1 坑一:Ubuntu 版本、gcc 版本、g++ 版本,三者傻傻分不清
很多人报问题的时候说“我的 Ubuntu 版本是 20.04,gcc 版本是 9,g++ 版本是 9”,这没问题。但有些人会说“我的 gcc 版本是 20.04”,这就混淆了系统版本和工具版本。记住:Ubuntu 版本决定系统库的原始版本,gcc/g++ 版本决定编译器能力,它们可以不一致。你完全可以在老 Ubuntu 上用新 g++,只要你能手动装上去。
6.2 坑二:默认用老版本编译导致的“伪报错”
如果一个项目在 README 里写“需要 C++17”,但你编译时总报error: 'clamp' is not a member of 'std'这类错误,先别怀疑代码,先看看你的编译命令是否带了-std=c++17。我见过太多人在 g++ 8 上编译 C++17 代码不指定参数,然后花一下午找语法错误,最后发现只是少了一个-std。
6.3 坑三:apt 装不上、版本太旧
apt install g++装到的版本永远取决于你的 Ubuntu 的软件源,不会是最新。比如 Ubuntu 20.04 的默认源里就是 g++ 9,这是系统设计如此——保守稳定。如果你需要新版 g++,要么加 PPA,要么下载官方二进制包,要么用 Docker 镜像(比如gcc:13镜像)隔离环境。对新手来说,我推荐用 Docker,完全绕开系统包管理器的版本限制,还不怕把系统搞坏。
6.4 坑四:gcc 升级了,但 g++ 还是旧的
有些人执行了sudo apt install gcc-13,以为 g++ 也一起升级了。实际上gcc和g++是两个独立的包。你需要同时安装gcc-13和g++-13:
sudo apt install gcc-13 g++-13检查是否都装好了,分别执行gcc-13 --version和g++-13 --version。
6.5 坑五:链接时找不到对应版本的 libstdc++
当你同时装了多个版本的 g++,编译链接时用的 C++ 标准库可能不是你想要的那份。比如你用g++-13编译,但链接时找的是旧版libstdc++.so.6,会报undefined reference或运行时报 GLIBCXX 版本错误。解决方法很简单:
g++-13 -std=c++20 -Wl,--no-as-needed main.cpp -o main或者干脆用g++-13同时负责编译和链接,不要手动指定-lstdc++。如果运行时报version 'GLIBCXX_3.4.30' not found,说明你的系统 libstdc++6 太老,需要升级基础库:
sudo apt install libstdc++6这个坑特别隐蔽,尤其是当你把编译好的二进制拷到另一台老机器上运行时。时刻记住:C++ 运行时库是一个独立的系统组件,和编译器版本没有必然绑定关系。
6.6 快速排查速查表
| 现象 | 可能原因 | 验证方法 | 解决办法 |
|---|---|---|---|
编译报error: ‘clamp’ is not a member of ‘std’ | 未指定 C++17 | g++ -std=c++17 main.cpp | 显式加-std=c++17 |
-std=c++20参数不识别 | g++ 版本太老 | g++ --version | 升级 g++ 到 10+ |
| 代码包含 C++20 特性但默认编译没报错 | 编译器默认标准不支持但代码用了兼容写法 | __cplusplus宏 | 显式指定正确的标准 |
| 换编译器后行为不一致 | 默认标准变化 | 检查__cplusplus | 构建脚本固定-std参数 |
运行时报GLIBCXX_3.4.xx not found | libstdc++6 库太旧 | strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX | 升级 libstdc++6 |
7. 一劳永逸的建议:如何正确管理 C++ 开发工具链
如果你还在靠“系统默认的 g++”打天下,我建议你花十分钟把工具链理清楚。开发 C++ 项目,最新工具链和稳定工具链都该有,用不同项目区分开。
第一,日常学习、验证新特性,用最新版 g++。在 Ubuntu 上,最简单的分离环境方式就是用 Docker。比如用gcc:13镜像,进去后用g++ -std=c++23随便造。这样不会影响宿主机。
第二,公司项目、生产环境,用项目规定的版本,并在 CMakeLists 或 Makefile 里固定好-std参数。别靠“这个编译器默认刚好支持 C++17”这种运气编程。
第三,新手阶段,建议把g++ --version和echo '#include <iostream>\nint main(){std::cout<<__cplusplus;}' | g++ -x c++ - -o /tmp/check && /tmp/check这两行命令记住,前者看版本,后者看默认标准。两秒钟,你就不会被编译器版本问题蒙在鼓里。
我个人在实际操作中的体会是:在 Linux 上判断 C++ 标准支持,最核心的一条原则就是“永远不要依赖默认值,永远显式指定 -std”。这句话我吃了很多亏才悟出来。哪怕是只编译一个 50 行的测试文件,我也会敲g++ -std=c++17 test.cpp -o test,而不是偷懒不写参数。因为偷懒省下的三秒钟,会在某个不可预料的下午,花三个小时来找回。