ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

gcc与g++的区别:用gcc编译C++的方法与常见坑

gcc与g++的区别:用gcc编译C++的方法与常见坑 那段时间我频繁在 Linux 命令行下处理 C/C 小项目最常用的一条命令就是gcc -o demo demo.c。直到有一次我改了后缀名把源码保存成demo.cpp依然习惯性敲下gcc -o demo demo.cpp结果终端刷出一堆undefined reference to std::cout。当时我第一反应是“是不是头文件没引对”反复检查了好几遍才意识到问题不在源码而在编译器命令本身。这个经历让我认真把 gcc 和 g 的“身份关系”捋了一遍也搞明白了为什么网上几乎所有教材都在强调“编译 C 请用 g”但同时又总有人问“gcc 到底能不能编 C”。这篇文章就是基于那段排查经历整理出来的适合刚接触 Linux 下 C/C 开发、在 VSCode 或命令行里配置环境时被 gcc 和 g 搞晕的读者。我会从两者真正的分工区别讲起再给出一套用 gcc 编译 C 的可行操作最后把后缀名、链接顺序、混编、工具链版本这几个高频坑一起讲清楚。1. 为什么写这篇一次 gcc 编译 C 的“符号闷棍”1.1 实测现场小代码大问号先还原一下当时的情况。我写了一个最简单的 C#include iostream int main() { std::cout hello from cpp std::endl; return 0; }然后执行gcc -o demo demo.cpp报错内容核心就是下面这几行/tmp/ccXXXXXX.o: in function main: demo.cpp:(.text0x10): undefined reference to std::cout demo.cpp:(.text0x15): undefined reference to std::ostream::operator(...) collect2: error: ld returned 1 exit status这段报错特别有迷惑性。因为前面的编译阶段是成功的生成了目标文件问题出在最后的链接阶段。你不能说“gcc 完全不能编译 C”因为它确实把 C 源码翻译成了汇编和机器码只是最后一步不知道怎么把 C 标准库的符号链接进来。类似的坑在论坛里很多有人贴的是找不到glibc有人贴的是找不到libstdc甚至会混着collect2: error: ld returned 1 exit status一起出现。如果你只盯着“undefined reference”这个词去搜很容易绕进“头文件路径不对”“命名空间写错”这类错误方向真正原因却一直被忽略。1.2 名字背后的真实分工要理解这个坑先得把名字这件事说清楚。GCC 的全称是 GNU Compiler Collection它是一整个编译器家族的统称下面包含 C、C、Fortran、Objective-C 等语言的前端。但是你在命令行里输入gcc的时候实际上启动的是这套集合里的“C 语言编译器驱动”。g 则是这套集合里的“C 语言编译器驱动”。注意它叫“驱动”而不是一个孤立的编译器。gcc 和 g 背后共用的是同一套汇编器、链接器、代码生成后端甚至连 C 编译器本体cc1plus都是同一个。真正决定它俩行为差异的一个是默认语言识别规则另一个是链接时默认链接的库。打个比方同一个厨师团队今天穿白衣服出来接宴席明天穿蓝衣服出来接散客。白衣服和蓝衣服只是分工不同的“入口”后厨那口锅、那套刀法其实是同一批人。gcc 和 g 的关系大体就是这种感觉。网上有些初学者把它们理解为“两个完全不同的编译器”这个印象是错的但如果继续把它们当成“完全等价的两个名字”又会踩上面那个链接库的坑。2. 拆穿外壳两者在驱动、前端和链接阶段到底差在哪2.1 同一个“编译器集合”但命令行前端不一样如果我们剥开gcc命令的调用过程会看到它在编译阶段实际上先判断输入文件的后缀名然后决定调用哪个“编译器 proper”。输入是.c就调cc1输入是.cpp、.cc、.cxx、.C就调cc1plus。g则不太一样它默认就把所有输入文件当成 C 代码来处理并且当你传入的是目标文件.o或汇编文件.s时它也会默认按照 C 程序来做最终链接。这里有个很重要的公式需要记住编译阶段gcc 看后缀决定语言 链接阶段gcc 不主动链接 libstdc g 则固定用 C 规则并在链接时自动带 libstdc所以同样是demo.cpp这个文件gcc前半段做得和 g 没有本质区别都是把 C 源码变成目标文件。后半段才是分水岭gcc 拿着目标文件去找main函数、找系统库却忘了把libstdc这种 C 标准库加上。结果一大堆模板、iostream、new、delete 相关的符号就成了“悬空引用”。你可以用gcc -v看到整个编译和链接的详细过程输出里会出现cc1plus这个程序。如果链接阶段缺库还能看到类似这么一行-lstdc 没有被添加所以从驱动层面看gcc 和 g 不是两套编译器而是两套“启动脚本”。2.2 后缀名先解析链接器再计较库我在前面那个坑里之所以“中招”还有个重要原因我下意识觉得“没用 g 编译所以 C 语法一定没被接受”。实际上这完全是两份工语法解析归前端链接符号归ld。源码被gcc正常翻译成汇编只是链接时缺少了标准库符号。这里顺便补充一个冷知识如果源码后缀是.c你用g去编译g 也会把它当成 C 代码来编译。老式写法有个坑就是 C 语言里某些写法在 C 语法规则下会报错比如void*到其他指针的隐式转换。反过来如果源码后缀是.cpp你用gcc编译GCC 集合里的驱动也能识别出来这是 C 代码所以不会出现“语法看不懂”的问题只会出现“链接缺库”的问题。把“后缀名识别”和“链接库”分开考虑你以后再遇到错误就不会一股脑归因于“编译器不认识 C”了。2.3 宏定义和默认行为也有细微差别除了链接库还有两个容易被忽略的差异点。第一g在编译任何输入文件时都会额外定义__cplusplus这个宏gcc只有在把文件识别为 C 时才定义这个宏。如果你的编译命令是gcc -x c demo.c因为明确了 C 语言__cplusplus也会存在。可如果只是普通地gcc demo.c就算系统头文件里有些代码想判断目前是不是 C 环境它也会发现这里不是从而走 C 语言分支。第二g链接阶段不仅会补-lstdc还会自动把一些运行时启动文件和 C 特有的启动逻辑加进去。日常写小程序可能体会不深但当你用到涉及全局对象构造、析构的代码时启动和收尾逻辑必须由 C 运行时配合完成。用gcc手动链接时只加-lstdc能解决大部分符号问题但特殊场景下你还需要额外处理crtbegin.o、crtend.o这类文件。这也解释了为什么很多人用 gcc 链接 C 项目时明明加了-lstdc某些复杂的全局对象还是运行得“不够利索”。3. 用 gcc 编译 C 的三条可行路线如果你只是因为“不习惯 g”或者“脚本里环境变量写得比较死”而想继续用 gcc 编译 C也不是完全没辙。下面三条路线我自己都试过从手动到自动可以按场景选。3.1 路线一分开编译最后用 gcc 手动链接既然问题在链接阶段那最直观的解决办法就是把“编译成目标文件”和“链接成可执行文件”分成两步在链接时手动把 C 标准库加进去。gcc -c demo.cpp -o demo.o gcc demo.o -o demo -lstdc第一条命令生成目标文件第二条命令完成链接。注意-lstdc写在了要链接的目标文件后面。这个顺序不是玄学而是链接器的工作原理链接器会从左到右扫描源文件里出现过的未定义符号再在右侧的库中找到对应定义如果库写在前面扫描时还没有人需要那些符号库就有可能在后面被直接丢弃。这条路线适合看着像“手工作坊”但特别稳定的场景。你甚至可以把一部分 C 源码和一部分 C 源码分开编译成不同的.o最后统一用一个 gcc 命令链接其中只有需要 C 标准库的目标文件会让你必须带上-lstdc。3.2 路线二一步到位但记得补上 -lstdc如果你想偷懒直接一条命令把编译和链接都做了那就写成这样gcc -o demo demo.cpp -lstdc这条命令会被驱动自动识别为 C 源码先用cc1plus编译再在链接阶段因为手动加了-lstdc而成功。这里最需要注意的是-lstdc仍然要放在最后或至少放在demo.cpp后面。如果写成gcc -lstdc -o demo demo.cpp某些老版本工具链下有可能正常但并不能保证因为链接时.cpp产生的目标文件还在后面先扫描的库可能不会重新检查。稳妥做法永远是把库写在来源之后。3.3 路线三把链接交给 g编译仍让 gcc 出马工程上还有一种常见玩法用gcc分别编译各个.cpp文件得到目标文件最后统一用g来做链接。例如gcc -c a.cpp -o a.o gcc -c b.cpp -o b.o g a.o b.o -o app这样做的好处是你不需要关心-lstdc这类细节链接阶段 g 会自己处理好。再加上你已经用gcc -c完成了“C 代码文本解析”这一步并不会把“编译器前端”割裂出去。说白了gcc 和 g 本来用的就是同一套前端这样分工完全不存在兼容性问题。如果你的工程里既有.c又有.cpp用这条路线更合适。C 源文件继续用 gcc 编译C 源文件也用 gcc 编译最后总链接丢给g基本不会出幺蛾子。4. 四个高频“坑”后缀陷阱、链接顺序、混编和旧版本幻觉4.1 后缀陷阱与 -x 强制指定命令行的语言识别依赖后缀名这看起来是常识但工程里出错频率极高。有人把 C 文件命名为.cpp结果里面全是malloc返回void*后直接赋给整型指针的写法被 C 严格的类型检查拦下来也有人把 C 文件命名为.c指望 gcc 自动识别模板结果编译器一脸茫然。如果文件名后缀不能改或者你想让某些无后缀文件按指定语言编译可以强制指定gcc -x c my_source.txt -o app -lstdc-x c后面的所有输入文件都会按 C 来编译直到你再遇到一个-x none才会恢复成按后缀判断。这招在脚本里处理一批特殊命名的文件时很有用。4.2 链接顺序问题库放错位置照样 undefined reference链接顺序最经典的表现是你明明加了-lstdc或者-lm但错误还在。比如gcc -o app -lstdc main.cpp如果链接器先扫描了-lstdc发现当时还没有任何未定义符号需要它提供后面扫描main.cpp生成的目标文件时才产生std::cout的引用此时库已经被处理完了链接器依然会说“符号未定义”。这就是为什么我一直强调-lstdc要放在所有输入文件或目标文件的后面。这个规律不仅限于 libstdc。你编译任何静态库都遵循同样的从左到右扫描规则。多个静态库之间有依赖关系时也应该把被依赖的库放右侧比如gcc a.o b.o -lB -lA -o app如果库 A 被库 B 依赖就应该写成-lB -lA自己体会一下这个顺序。4.3 混编代码中 extern C 的规则项目一旦进入 C/C 混编阶段你还会遇到“明明 gcc 能编 Cg 能编 C为什么放在一起就链接失败”的问题。最常见原因是符号修饰不同。C 编译器默认会对函数名做修饰比如foo(int)会变成_Z3fooi这样的名字而 C 语言没有这套机制函数名在符号表里就叫foo。所以当 C 代码想调用 C 模块里的foo如果 C 模块是用 gcc 编译的符号是fooC 模块里声明extern int foo(int);时如果这句话被 C 编译器按默认规则处理它会去找_Z3fooi自然找不到。解决方案就是用extern C把头文件里的声明包起来#ifdef __cplusplus extern C { #endif int foo(int); #ifdef __cplusplus } #endif这样 C 代码在引用foo时就会使用 C 语言符号规则。这也回到前面说的__cplusplus宏了为什么 gcc 在编译.c文件时不定义它、编译.cpp时定义它正是为了方便头文件在这种混编场景下自动切换声明方式。4.4 旧版本 gcc 幻觉以及安装不上的连锁反应最后一个坑严格说不完全是 gcc 和 g 的用法问题但搜索热词里反复出现而且确实会让人怀疑“是不是我命令写错了”。很多人在 CentOS 或 Ubuntu 上更新了 gcc然后执行gcc --version发现版本号还是老样子。原因多半不是“没装上”而是命令解析到的路径不是刚安装的目标。Unix 环境下命令查找依赖PATH环境变量的顺序。如果你手动把新版本 gcc 装到了/usr/local/bin但PATH里/usr/bin排在前面那么敲gcc时仍然会命中老版本。用which gcc或type -a gcc可以看出来。另外有些系统里存在update-alternatives机制需要你先注册并切换优先级否则即使你手动装了新版本系统也不会自动认领。这里再补一个真实场景有人在一台内网离线服务器上安装 gcc用 rpm 包一个个装装了一半发现依赖不满足然后又看到gcc --version出了个诡异结果最后写代码时发现-lstdc也找不到。这类问题十有八九跟“编译器版本不匹配”有关gcc 和 g 本身版本不匹配或者 gcc 版本和 libstdc 版本不匹配。你可以用gcc -print-file-namelibstdc.so来看当前 gcc 驱动实际会去找哪个路径下的标准库。如果这个路径和ldd显示的可执行文件实际加载路径不一样后面大概率会运行时报version GLIBCXX not found。排查时别急着怀疑命令格式先确认你到底在用哪一套工具链。5. 从命令到工程把 gcc/g 放到真实项目里5.1 一条值得背下来的典型命令如果你的项目规模不大建议直接形成自己的固定习惯。我个人的默认命令是g -stdc17 -Wall -Wextra -g -O2 main.cpp -o app其中-stdc17指定语言标准-Wall -Wextra打开额外警告-g生成调试信息-O2开优化。等调试阶段结束我可能把-g去掉但-Wall -Wextra一直保留。如果你是出于某种原因必须用gcc命令来做比如项目脚本里统一用 $CC 变量那么请至少把链接那步写成gcc -stdc17 -Wall -Wextra -g -O2 main.cpp -o app -lstdc记住两个原则一是-stdc17这类参数要放在编译阶段二是-lstdc要放在输入文件后面。这两个原则能救回大部分明显的坑。5.2 让 VSCode 的 C/C 环境不再打架很多人是在 VSCode 里配 C/C 环境时被 gcc 和 g 搞晕的。“vscode配置c/c环境”这个搜索词特别能说明问题。因为 VSCode 的 C/C 扩展里智能提示、代码补全、跳转需要的配置和实际编译需要的配置并不是一回事。你可以在c_cpp_properties.json里指定compilerPath比如写成/usr/bin/g这样语法检查和智能提示会按 C 规则来识别。如果你写成/usr/bin/gcc对于纯 C 项目没问题对于 C 项目就可能出现iostream都找不到的情况因为扩展在调用 gcc 去探测头文件目录时默认按照 C 语言规则把头文件列表给定死了。实际构建任务则写在tasks.json里你完全可以在里面用g{ type: cppbuild, command: /usr/bin/g, args: [ -stdc17, -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension} ] }这个配置的价值在于调试、运行和编辑器智能提示都用同一套语言规则避免出现“代码提示正常一编译全是坑”的割裂感。5.3 Makefile 和 CMake 中的 CC/CXX 选择到了工程化阶段你可能会写 Makefile 或 CMake。这里有个非常经典的误用把所有编译命令都写成CC gcc然后拿着一堆.cpp文件去编。Makefile 的默认变量分得很清楚CC ? gcc CXX ? gCC是 C 编译器的变量CXX是 C 编译器的变量。如果你用.cpp文件构建规则里应该用$(CXX)并且通常自动带上-lstdc的链接逻辑。混淆这两个变量就会出现和命令行里直接用 gcc 编译 C 一模一样的症状。CMake 会更严格。它会按项目语言读取CMAKE_C_COMPILER和CMAKE_CXX_COMPILER。如果你的项目是用.cpp文件写的但只设置了CMAKE_C_COMPILER/usr/bin/gcc没有设置 CXX 编译器CMake 大概率会报找不到可用的 C 编译器。正确做法是在 CMake 项目里指定set(CMAKE_C_COMPILER /usr/bin/gcc) set(CMAKE_CXX_COMPILER /usr/bin/g)或者更常见的是直接信任 CMake 在首次配置时自动探测到的系统默认编译器。总之分清 CC 和 CXX比记住几十条编译命令参数更有用。我个人在实际操作中最深的体会是不要跟工具链较劲。gcc 和 g 是同一个家族里的两个分工入口它们在编译阶段共享同样的 C 解析能力却在链接阶段表现出了完全不同的默认行为。日常开发时我默认还是会用 g 编译 C用 gcc 编译 C但当我需要在某个脚本里统一用 gcc 的时候我也知道只要补上-lstdc并放在正确的位置这条路照样走通。了解这层关系你以后看到的“undefined reference”就不再是天书了。
返回列表