☰
Windows下用CMake将.c文件按C++编译的完整指南
2026/10/15 23:10:12 网站建设 项目流程

最近被一个新同学问住了:CMakeLists 里明明写的是add_executable(demo demo.c),我也在 CMake 里选了 C++ 编译器,为什么构建日志里它还是把demo.c当成 C 代码在编?这个问题的答案说穿了就一句话——CMake 决定源文件用什么语言,靠的是文件扩展名,而不是你全局指定的编译器;而 Windows 上的 MSVC 到了编译这一步,还会根据扩展名再判断一次。两层判断叠加,就形成了“我明明设了 C++ 怎么还是不行”的经典困惑。

这篇文章我就把“Windows 环境下如何让 CMake 把 .c 文件按 C++ 代码编译”完整捋一遍,包含 CMake 属性怎么改、编译器参数怎么加、完整的实操命令,以及我实际踩过的链接和编译坑。适合两类读者:一是手上有一批 C 代码、想用 C++ 编译器做严格检查做渐进迁移的;二是必须把第三方 C 库纳入 C++ 工程、但又不方便改文件后缀的人。

1. 先搞清楚源头:CMake 是怎么定语言的

1.1 扩展名映射是整个机制的地基

CMake 收到一个源文件,第一步就是识别语言。内置规则大致是:.c归 C,.cpp/.cc/.cxx/.c++归 CXX,.f/.f90归 Fortran,.cu归 CUDA。这个识别发生在project()或enable_language()阶段,它决定两件具体的事:

  • 这个文件挂在哪类编译规则下,对应使用CMAKE_C_COMPILER还是CMAKE_CXX_COMPILER;
  • 编译命令里插入的配置参数是CMAKE_C_FLAGS/CMAKE_C_FLAGS_<CONFIG>,还是 CXX 对应的那套变量。

所以如果你的工程开头写的是project(foo C),那 CXX 这套编译器探测和运行时初始化根本没发生。这时候你再给某个文件盖上“CXX 语言”的章,CMake 要么报The CXX compiler identification is unknown,要么直接跟你说找不到CMAKE_CXX_COMPILER。这个现象特别容易误导人,很多人以为是语法写错了,其实根子是语言列表没配齐。

动手之前第一件事永远是自查:project()里 C 和 CXX 有没有一起列出来。这也是我在所有示例开头都写project(xxx C CXX)的原因。

1.2 第二层判断:编译器自己也认扩展名

CMake 层的语言定好之后,它只负责拼命令行并调用编译器。这时关键问题出现了:编译指令里源文件的后缀仍然是.c,没变。

MSVC 的cl.exe对后缀非常敏感——.c默认按 C 编译,.cpp默认按 C++ 编译;GCC 和 Clang 同理,也默认按扩展名选语言。这就是第二层判断。要让编译器无视扩展名、强制执行 C++ 模式,MSVC 需要加/TP(文档里也写作/Tp),GCC/Clang 需要加-x c++。

提示:-x c++的意思是“对下一个输入文件按 C++ 处理”,/TP的意思是“本次编译调用里所有输入都按 C++ 处理”。因为 CMake 默认一个源文件对应一次编译调用,这两个参数在单文件场景下效果完全一样。

“两层判断”是这篇文章最核心的认知模型。很多时候你改了 CMake 的LANGUAGE属性发现没效果,不是没生效,而是第二层没打通。下面说的解法,本质就是把这两层都安排明白。

2. 核心解法:一个属性加一个参数

2.1 第一步:把源文件的 LANGUAGE 属性改成 CXX

CMake 每个源文件都有一堆属性,其中LANGUAGE专门控制这个文件按哪门语言参与构建。改法有两种,完全等价:

set_source_files_properties(demo.c PROPERTIES LANGUAGE CXX)
set_property(SOURCE demo.c PROPERTY LANGUAGE CXX)

第一种是传统写法,出现的频率最高;第二种是更现代的写法,语义更清晰。两者都行,我习惯用第一种,因为这个宏还可以顺带设别的源文件属性。

执行完这条之后,CMake 内部会把这个文件从 C 的构建规则挪到 CXX 的构建规则:编译器换成了 C++ 编译器,编译参数也换成 CXX 那套。但如果到此为止,你大概率会遇到“看起来没生效”的状态——因为编译器那边还认着.c后缀。

2.2 第二步:给编译器加“强制 C++”的参数

这一步必须在 CMake 里区分编译器类型,Windows 上主要两派:

if(MSVC) set_source_files_properties(demo.c PROPERTIES COMPILE_FLAGS "/TP") else() set_source_files_properties(demo.c PROPERTIES COMPILE_FLAGS "-x c++") endif()

这里特意用COMPILE_FLAGS这个源文件属性,而不是target_compile_options,原因很实在:它只往demo.c这一条编译命令后面追加参数,不会波及目标里其他源文件。如果目标里还有正常的.c文件要保持 C 编译,这个隔离就非常重要。

COMPILE_FLAGS在官方文档里被标记为 deprecated,但目前所有主流 CMake 版本都还在支持。偏好新语法的同学可以用生成表达式来实现同样效果:

target_compile_options(demo PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:/TP>")

意思很直白:只要文件是 CXX 语言,就给它加/TP。因为已经强行归到 CXX 的文件本身就满足这个条件,所以效果等价,而且这行代码还能在只有 MSVC 目标里安全复用。缺点是要理解生成表达式的写法,新手看着容易发怵。

2.3 别忘了 project() 的语言列表

整套配置里最容易漏的就是这个。很多人直接在原来的project(foo C)后面补代码,结果怎么改都不出正确结果。正确做法是把语言列表补齐:

project(foo C CXX)

如果因为某些原因不方便改project(),也可以单独加一句enable_language(CXX)。这一步的本质是把 C++ 编译器探测提前到 configure 阶段,后头所有跟 CXX 有关的宏和变量才能用。

3. 完整实操:一个 Windows 下可以直接复现的示例

3.1 准备一个“故意用 C++ 语法”的 demo.c

为了验证“真的按 C++ 编了”,我会刻意在一个后缀为.c的文件里写 C++ 的类。如果它能通过编译,说明两层判断都被正确绕过;如果哪一层没设置,编译必然爆错。这个验证思路比看日志更过硬。

// demo.c —— 故意写成 .c 后缀,但里面使用了 C++ 语法 #include <iostream> class Counter { public: Counter() : count_(0) {} void add(int n) { count_ += n; } int get() const { return count_; } private: int count_; }; int main() { Counter c; c.add(5); c.add(7); std::cout << "counter = " << c.get() << std::endl; return 0; }

别担心,实际项目里你要编译的 .c 文件大概率没有这么“C++”,这个例子只是为了制造一个强验证条件。

3.2 编写 CMakeLists.txt

cmake_minimum_required(VERSION 3.16) project(cpp_from_c_demo C CXX) add_executable(demo demo.c) # 第 1 步:告诉 CMake 这个 .c 文件按 CXX 语言处理 set_source_files_properties(demo.c PROPERTIES LANGUAGE CXX) # 第 2 步:告诉编译器无视扩展名,强制按 C++ 编译 if(MSVC) set_source_files_properties(demo.c PROPERTIES COMPILE_FLAGS "/TP") else() set_source_files_properties(demo.c PROPERTIES COMPILE_FLAGS "-x c++") endif()

这里有个细节值得说:add_executable(demo demo.c)放在改属性之前,顺序没问题,因为 CMake 对源文件语言的最终判定发生在生成构建规则的阶段,而不是add_executable那一行。你完全可以先写add_executable,再统一改属性。

3.3 编译:VS 生成器与 Ninja 两条线路

Windows 上主流的构建方式有两种。先看 Visual Studio 生成器:

cmake -S . -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release

如果用 Ninja,需要在“适用于 VS 的开发者命令行”里先把环境变量导进来,再执行:

cmake -S . -B build-ninja -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build-ninja

如果你用的是 MinGW-w64 那套工具链,把生成器换掉即可:

cmake -S . -B build-mingw -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release cmake --build build-mingw

我实测下来,这套 CMakeLists 在这三条线路上都能编过,运行demo.exe会输出counter = 12。

3.4 怎么确认真的按 C++ 编译了

构建时加--verbose直接看命令行,是最快的确认方式:

cmake --build build --config Release --verbose

VS 生成器下你会看到类似:

cl.exe /nologo /TP /utf-8 ... /I... /c demo.c /Fodemo.c.obj

MinGW/Ninja 下会看到:

g++ ... -x c++ ... demo.c

只要命令行里同时出现 C++ 编译器和后缀为.c的源文件,再加上/TP或-x c++,就说明两层都打通了。

顺便说一个反向验证小技巧:把 CMakeLists 里第 2 步的/TP或-x c++注释掉,重新构建。这时demo.c里的class会触发 C 语法错误,比如 MSVC 报error C2059: syntax error: 'class',GCC 报expected '=', ',', ';', 'asm' or '__attribute__' before 'Counter'。这一报错反而是好消息——它帮你确认了 CMake 把文件交给了 C++ 编译器,只是编译器还在按 C 解释。把参数加回去,问题立刻消失。

4. 进阶:把整个 C 库用 C++ 编译器过一遍

4.1 一个很实用的模式:C/C++ 双编译体检

理解了单文件玩法,就能玩一个更高级的套路:同一个 C 库源码,同时生成 C 版本和 C++ 版本的可执行文件。这在团队里有个讲法叫“C++ 兼容性体检”——用 C++ 编译器的严格规则把 C 代码里的隐患全部揪出来,但正式交付仍然用 C 版本。

cmake_minimum_required(VERSION 3.16) project(c_check C CXX) add_library(core STATIC core.c util.c) # 把同一份 C 源码编成一个“C++ 体检目标” add_executable(core_cpp_check core.c util.c) set_source_files_properties(core.c util.c PROPERTIES LANGUAGE CXX) if(MSVC) target_compile_options(core_cpp_check PRIVATE /TP) else() target_compile_options(core_cpp_check PRIVATE "-x c++") endif()

注意这里我改用了target_compile_options,而不是逐文件设COMPILE_FLAGS。理由是这样更省事,而且core_cpp_check里的源文件全部被盖过 CXX 章,-x c++对它们一视同仁,不会误伤谁。假如目标里混着没盖章的.c文件,那就要谨慎了——-x c++会让 C 编译器也去按 C++ 理解源码,反而乱了套。

4.2 extern "C" 与名字修饰不一致的坑

把.c文件编成 C++,副作用是里面的全局函数默认会被做 C++ 名字修饰(name mangling)。C 语言没有修饰,函数在符号表里就叫foo;C++ 会把它变成类似?foo@@YAHH@Z的怪名字。

麻烦的是,如果你的头文件里写了extern "C"的声明,但源文件里没有配合:

  • 头文件声明extern "C" int foo(int);,其他.cpp文件按 C 链接去找foo;
  • 而foo.c编成 C++ 时,定义却按 C++ 修饰成了带装饰符的名字。

两边对不上,MSVC 链接器报LNK2019 unresolved external symbol,GCC/Clang 报undefined reference to 'foo'。排查的方法很直接:看头文件里extern "C"包裹的是声明还是定义,确保所有翻译单元对“这个符号到底是什么链接属性”的认识一致。

有一种常见情况反而没问题:很多 C 库的头文件自带兼容保护:

#ifdef __cplusplus extern "C" { #endif int foo(int); #ifdef __cplusplus } #endif

当foo.c被 C++ 编译时,宏__cplusplus被定义,头文件里声明恰好被extern "C"包住,于是定义也变成 C 链接。这时候foo在符号表里就叫foo,跟其他 C 文件调用方完全一致,链接不会出错。所以我的经验是:优先保证头文件的兼容保护是完备的,再谈强制 C++ 编译。

4.3 C 代码在 C++ 编译器下的典型报错清单

有些 C 代码搬到 C++ 下不是链接问题,而是直接编译不过。我把最常见的几类整理成了一张表,照着改就行:

C 代码常见写法C++ 编译器的反应修改建议
void *p = malloc(100);报错:C++ 不允许隐式void*转其他指针改成int *p = (int*)malloc(100);或用 C++ 的new
C99 指定初始化{.x = 1, .y = 2}C++20 之前直接报错,之后也只是部分支持改用构造函数或逐字段赋值
旧式函数定义int f(a) int a;直接报语法错误改成标准原型int f(int a)
隐式函数声明,调用前没声明C++ 里是硬错误补头文件或前向声明
使用restrict/_Bool关键字不支持或行为不同用宏定义一层兼容层

这不是说所有 C 库都能无痛切到 C++ 编译。我的建议是,如果库的代码量大、C 特性用得多,先跑一次体检看看报错清单再决定要不要推广这种做法,别一上来就把整个库都切过去。

5. 踩坑实录:这些问题我基本都撞过

5.1 忘了加 /TP,编译命令看着已经是 C++ 但还在按 C 编

这是新手最容易卡住的地方。--verbose里明明看到了cl.exe,就以为大功告成,结果demo.c里一写 C++ 语法就报 C 错误。原因前面已经说透:cl.exe认后缀,.c就默认按 C 编。对策就是必加/TP或-x c++,这一行跟LANGUAGE CXX是配套的,缺一不可。

5.2 只改属性没启用 CXX,configure 阶段直接失败

我在 2.3 节强调过project(foo C CXX),但实际项目里总有人忘了同步修改。症状是cmake配置到一半报错,提示找不到 CXX 编译器或语言未启用。这类问题最容易在旧 CMake 版本上出现,因为有些版本不会自动补齐语言,直接给你难看的错误。改法就一行:语言列表加 CXX。

5.3 静态库被纯 C 目标链接时缺 C++ 运行时

还有一个隐蔽场景:你把一个静态库里的.c全部改成按 C++ 编译,库本身编译通过,但下游有个纯 C 可执行程序链它,链接器突然报一堆找不到operator new、std::cout之类的符号。原因很简单:库的目标文件是用 C++ 编译器生成的,里面的代码引用了 C++ 运行时库,而下游 C 目标只按 C 来组织链接。

解决思路有两个,选一个即可:

  • 给下游目标也创建为 C++ 语言,哪怕它的源文件全是.c,也照前面方法设置LANGUAGE CXX;
  • 或者显式指定库的LINKER_LANGUAGE:
set_target_properties(core PROPERTIES LINKER_LANGUAGE CXX)

这个属性能强制 CMake 在链接环节使用 C++ 链接器和运行库,算是处理混合语言工程的常用后手。

5.4 COMPILE_FLAGS 被弃用后的迁移思路

COMPILE_FLAGS在文档里被打上了 deprecation 标记。我个人的建议是:新项目直接用生成表达式版本,一行搞定,不再依赖这个旧属性:

if(MSVC) target_compile_options(demo PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:/TP>") else() target_compile_options(demo PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:-x c++>") endif()

需要留意的是-x c++作为两个独立参数传给编译器是符合 GCC/Clang 预期的,CMake 会把 options 按空格拆开,因此-x和c++会以两个参数的形式送进命令行,行为正确。而旧项目里只要COMPILE_FLAGS还在工作,也不必急着改,知道有这么一回事就行。

5.5 顺手记一个调试思路:先分离变量再排查

遇到编译或者链接异常,我的排查顺序固定是:先确认 configure 阶段语言有没有启用,再确认生成的命令行里编译器类型对不对,再确认有没有强制语言参数,最后才去查源码里的extern "C"和 C 语法。这个顺序是从“两层判断”模型推出来的,按这个走,九成问题都能在两轮以内定位。

从我个人实际操作中得来的体会是:把.c文件当 C++ 编译这件事,真正的难点不在 CMake 语法,而在你心里有没有“两层判断”这张图。CMake 管语言归属,编译器管语法模式,两层都打通,剩下的就是源码里那些 C/C++ 语法差异了。如果你只是临时需要验证某个库的兼容性,我建议直接照 4.1 的“双编译体检”模式来,成本最低,收益最直观,还不影响正式交付版本的构建链路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询