C 与 C++ 的兼容边界

一句话:C 是 C++ 的「近似子集」——大部分 C 代码 C++ 编译器能编,但下面这些写法会在 C++ 里直接报错,另有几处两边都能编、行为却不一样。

这份文档里的每一条结论、每一句报错原文,都是用本机 gcc 16.2.0 / g++ 16.2.0(MSYS2 UCRT64)真跑出来的, 源码在 _wcppc/ 下,报告在 _wcppc/report.txt。 命令固定为 gcc -std=c11 与 g++ -std=c++17,需要严格模式时加 -pedantic-errors。

1. 先建立三层心智模型

层次发生什么典型症状
① 语法 / 语义 C 的很多写法在 C++ 里不合法(不是警告,是错误) error: invalid conversion from 'void*' to 'int*'
② 编译(选语言) 用哪个编译器、文件叫什么后缀,共同决定这份代码被当 C 还是 C++ 同一个 .c 文件,gcc 编过、g++ 报错
③ 链接(名字修饰) C++ 会给函数名「改名」,C 不会 → 混编时要 extern "C" undefined reference to 'cadd(int, int)'

②这一层最容易踩:g++ 把所有输入都当 C++ 编(哪怕文件叫 .c);而 gcc 看后缀——编 .cpp 文件时它会去调用 C++ 前端(实测会有警告: command-line option '-std=c11' is valid for C/ObjC but not for C++)。 所以「这份代码是 C 还是 C++」不由文件内容决定。想保证按 C 编,就用 gcc + .c。


2. 速查总表

2.1 只有 C 能编(C++ 直接报错)

写法CC++为什么
int *p = malloc(sizeof(int));OK错误C++ 不许 void* 隐式转成别的指针
struct Point {…}; Point p;错误OKC 里标签名不是类型名,必须写 struct Point;C++ 里标签即类型名
show(&(int){42});OK错误复合字面量是 C99 特性(C++ 里临时量取不到地址)
int a[n];(n 是变量)OK严格模式错误变长数组 VLA:C 标准特性,C++ 没有;g++ 默认当扩展放行,-pedantic-errors 才报错
struct S s = {.b=2, .a=1};OK错误C99 指定初始化器顺序随意;C++20 也有但必须按声明顺序
void f(int *restrict a)OK错误restrict 是 C 的指针修饰符,C++ 里没有这个关键字
_Generic((x), int: …)OK错误C11 的类型分派宏,C++ 用模板 / 重载代替
auto int x = 1;OK错误C 里 auto 是存储类;C++11 起是类型推导,两个词撞车
int add(a, b) int a; int b; {…}OK错误K&R 风格定义只在 C89 还能编,C++ 从来没有
main() {…}(省略返回类型)C89 OK / C11 起错误g++ 放行隐式 int:C89 合法;本机 gcc 在 -std=c11 / -std=c23 下直接报 error;g++ 实测反而放行(GCC 扩展,别依赖)
enum Color c = 2;OK错误整数不能隐式转成 C++ 的枚举(要显式 static_cast)
void greet(); 然后 greet(1,2,3);OK错误C 的空参数表 = 「参数未指定」(无原型);C++ 的空参数表 = 「没有参数」
goto 跳过带初始化的变量OK错误C++ 禁止跳进变量的作用域:error: jump to label 'skip'
case 里声明变量、后面还有别的 caseOK错误error: jump to case label——解法是给该 case 加一对花括号
struct Buf { int n; int data[]; };OK严格模式错误柔性数组成员是 C99 特性,C++ 不认(-pedantic-errors 才拦)
a and !b(不包含 iso646.h)错误OK方向反过来:and / or / not 在 C 里要 <iso646.h>,在 C++ 里是内建关键字

2.2 只有 C++ 能编(C 报错)

写法CC++为什么
int new = 5; / int class = 2;OK错误new、class、template、this、try 在 C 里是普通标识符,在 C++ 里是关键字
bool ok = true;C11 错误 / C23 OKOKC23 才把 bool / true / false 收进语言,之前要 <stdbool.h>
x and y错误OK见上表最后一行(C 需要 iso646.h 宏)

2.3 两边都能编,但结果/待遇不同

写法C 结果C++ 结果说明
sizeof('a')41C 里字符常量类型是 int,C++ 里是 char——实测输出 4 和 1
char *s = "abc";OK警告C++ 里字符串字面量是 const char[];g++ 默认只给警告,加 -Werror=write-strings 就变错误
register int x = 3;OK,无提示警告C++17 起 register 被移除,g++ 只警告:ISO C++17 does not allow 'register' storage class specifier
文件级 const int LIMIT = 7; 被另一个文件 extern 引用链接成功,输出 7链接失败C:const 变量是外部链接;C++:命名空间作用域的 const 默认内部链接 → undefined reference to 'LIMIT'
struct Empty { };GCC 放行 / 严格模式错误OK(sizeof = 1)C 语言要求 struct 至少一个成员(GCC 当扩展),C++ 允许空类
int a[n];(VLA)OK(含 -pedantic-errors)扩展放行 / 严格模式错误C11 里 VLA 是标准(可选支持),C++ 里永远不合法

3. 逐条拆解(配真实报错)

3.1 malloc 要强制转换

/* 这份代码叫 t01.c;用 gcc 编过,用 g++ 报错 */
#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int));      /* C: void* 隐式转成 int* */
    if (!p) return 1;
    *p = 7;
    free(p);
    return 0;
}
t01.c:4:20: error: invalid conversion from 'void*' to 'int*' [-fpermissive]

改法:int *p = (int *)malloc(sizeof(int));。更现代的写法是用 new: int *p = new int;,配套 delete p;。

3.2 struct 标签不是类型名

struct Point { int x, y; };
int main(void)
{
    Point p;                            /* C: 必须写 struct Point */
    p.x = 1;
    return p.x - 1;
}
t02.c:4:5: error: unknown type name 'Point'; use 'struct' keyword to refer to the type

反过来也成立:struct Point p; 这种写法在 C++ 里照样合法, 所以 C 风格的 struct 前缀写到 C++ 里不会出错——只是啰嗦。 C 里想省掉前缀,习惯是 typedef struct Point {…} Point;,这个习惯两边通用。

3.3 复合字面量

show(&(int){42});                   /* C99 复合字面量:造一个临时 int 并取地址 */
/* C++: t03.c:5:16: error: lvalue required as unary '&' operand */

3.4 变长数组 VLA

int n = 5;
int a[n];                           /* C99 变长数组 */
gcc  -std=c11                     → OK,输出 4
gcc  -std=c11 -pedantic-errors    → OK(VLA 本来就是 C 标准)
g++  -std=c++17                   → OK(GCC 扩展放行)
g++  -std=c++17 -pedantic-errors  → t04.c:5:9: error: ISO C++ forbids variable length array 'a' [-Wvla]

这就是「g++ 默认放行很多 C 写法」的例子:不加 -pedantic-errors 时你以为代码是标准 C++, 换到 clang 或 MSVC 上就可能编不过。体检 C++ 代码时加 -pedantic-errors。

3.5 指定初始化器

struct S { int a; int b; };
struct S s = { .b = 2, .a = 1 };     /* C: 顺序随意 */
gcc -std=c11   → OK
g++ -std=c++17 → t05.c:4:35: error: designator order for field 'S::a' does not match declaration order in 'S'
g++ -std=c++20 → 同上(C++20 支持了指定初始化器,但要求按声明顺序)

3.6 C 专属修饰符:restrict / _Generic

void add(int *restrict a, int *restrict b, int *restrict c) { *c = *a + *b; }
/* C++: t06.c:1:24: error: expected ',' or '...' before 'a'  (然后是一串连带错误) */

#define type_name(x) _Generic((x), int: "int", double: "double", default: "other")
/* C++: t07.c:2:22: error: '_Generic' was not declared in this scope */

C++ 的对应物:restrict 之外没有直接等价物(靠 const/引用/别名分析优化); _Generic 用函数重载或模板代替。

3.7 老派写法:auto / register / 隐式 int / K&R

auto int x = 1;                      /* C: 存储类 auto(局部变量),C++11 起是类型推导 */
/* C++: t08.c:3:5: error: two or more data types in declaration of 'x' */

register int x = 3;                  /* C: OK;C++17 移除,仅警告 */
/* C++: t17.c:3:18: warning: ISO C++17 does not allow 'register' storage class specifier */

int add(a, b) int a; int b; { return a + b; }   /* C89 K&R 定义,C++ 从来没有 */
/* C++: t09.c:1:29: error: expected unqualified-id before '{' token */

/* 隐式 int:main() 省略返回类型
   gcc -std=c89   → OK
   gcc -std=c11   → t10.c:2:1: error: return type defaults to 'int' [-Wimplicit-int]
   gcc -std=c23   → 同上(报错)
   g++ -std=c++17 → 放行(见下面的警告框) */

实测到的怪事(别依赖):省略返回类型的 main() { … } 在 g++ -std=c++17 下没有报错、也没有警告,即使加 -pedantic-errors 也放行(普通函数 foo() { return 1; } 同样放行, nm 里能看到符号 _Z3barv 说明确实是 C++ 编的)。 C++ 标准要求写返回类型,这纯粹是 GCC 的宽松扩展。永远显式写 int main()。

3.8 空参数表:C 和 C++ 的意思不一样

#include <stdio.h>
void greet();                            /* C: 空参数表 = 参数未指定 */
int main(void)
{
    greet(1, 2, 3);                      /* C: 只有声明(无原型)时,多传实参能编过 */
    return 0;
}
void greet(void) { printf("hi\n"); }
gcc -std=c11   → OK,运行输出 hi
g++ -std=c++17 → t14.c:5:10: error: too many arguments to function 'void greet()'

这条是真正的语义差异:C 里 void greet(); 是「我不知道参数是什么」, C++ 里是「没有参数」。所以 C 代码里大量存在的 void f(); 声明,挪到 C++ 后会变成严格的参数检查, 原本悄悄多传的实参全部变成编译错误。
想表达「无参数」,C 里要写 void greet(void);——这个写法两边都合法且含义一致, 所以写 C 时养成写 (void) 的习惯,代码就能两边通用。

3.9 goto / switch 跨过初始化

goto skip;
int x = 5;
skip:  …                              /* C: 能编过(x 存在但没初始化,用它才是 UB) */
/* C++: t15.c:6:1: error: jump to label 'skip'  (跳过了 x 的初始化) */

switch (i) {
    case 1: int y = 3; return y - 3;  /* C: 能编过 */
    case 2: return 0;
}
/* C++: t16.c:8:14: error: jump to case label */

解法(C++ 里也照样清晰):case 1: { int y = 3; … } —— 给这个分支加一对花括号, 让 y 的生命周期限制在分支内。

3.10 关键字冲突

int new = 5;                         /* C: 普通标识符;C++: 关键字 */
int class = 2;
/* C++: t18.c:3:9: error: expected unqualified-id before 'new'
        t18.c:5:18: error: expected primary-expression before 'class' */

C++ 关键字里最容易撞名字的:new delete class template this try catch operator public private protected friend namespace using typename virtual explicit bool true false and or not。 翻 C 代码时,见到这些名字要改名(这是「移植」里最常见的机械性修改)。

3.11 sizeof('a')

printf("%zu\n", sizeof('a'));
/* C:   sizeof('a') = 4   (字符常量类型是 int)
   C++: sizeof('a') = 1   (字符常量类型是 char) */

顺便:sizeof 的结果类型是 size_t,打印要用 %zu, 在 Windows 的 MinGW 上 size_t 是 64 位、unsigned long long, 用 %d 打印是未定义行为。

3.12 const 的链接性(分文件时最坑)

/* t24_def.c */
const int LIMIT = 7;                     /* C: 外部链接;C++: 内部链接 */

/* t24_main.c */
extern const int LIMIT;
int main(void) { printf("%d\n", LIMIT); return 0; }
gcc 编两个 .c 再链接   → OK,输出 7
g++ 编两个 .c 再链接   → ld: … undefined reference to `LIMIT'
                          collect2.exe: error: ld returned 1 exit status

原因:C++ 里命名空间作用域的 const 变量默认是内部链接(相当于自动带了 static), 所以别的编译单元根本看不见它。
想跨文件用:extern const int LIMIT = 7;(定义处)+ extern const int LIMIT;(声明处)。 头文件里的 const 常量在 C++ 里天生是内部链接,每个编译单元一份,通常不是问题—— 但如果代码要 C/C++ 双头文件通用,就必须写 extern 版本。

3.13 空结构体与柔性数组

struct Empty { };                    /* gcc 默认放行(扩展);-pedantic-errors 报错
                                        C++: 合法,sizeof(Empty) == 1 */

struct Buf { int n; int data[]; };   /* C99 柔性数组成员 */
/* C++: ISO C++ forbids flexible array member 'data' [-Wpedantic] */

空结构体在 C++ 里 sizeof 是 1(每个对象必须有唯一地址); 柔性数组在 C++ 里通常改用「指针 + 单独分配」或 std::vector。


4. 混编实战:extern "C" 与名字修饰

这是跨语言调用唯一必学的一节。C 写的库被 C++ 调用时,链接器找不到符号, 报错长这样:

/* cadd.c —— 用 gcc 编译 */
int cadd(int a, int b) { return a + b; }

/* use_bad.cpp —— 用 g++ 编译,声明时没有 extern "C" */
#include <cstdio>
int cadd(int a, int b);                    /* 在 C++ 里这样声明 → C++ 链接 */
int main() { std::printf("%d\n", cadd(2, 3)); return 0; }

/* 链接结果: */
ld.exe: … use_bad.cpp:(.text+0x18): undefined reference to `cadd(int, int)'
collect2.exe: error: ld returned 1 exit status

为什么找不到?因为 C++ 会把函数名「修饰」成唯一的名字以支持重载。nm 实测:

cadd_c.o     (gcc 编的 C 文件)  →  0000000000000000 T cadd
mangle_cpp.o (g++ 编的 C++ 文件)→  0000000000000000 T _Z4caddii     int cadd(int,int)
                                     0000000000000014 T _Z4cadddd     double cadd(double,double)
                                     000000000000002e T _Z4takePi     void take(int*)

_Z4caddii 就是 cadd(int, int) 被修饰后的名字。而 C 文件里导出的符号就叫 cadd, 两边对不上,于是「未定义引用」。

正确做法:头文件用 __cplusplus 守卫

/* cadd.h —— 这份头文件 C 和 C++ 都能包含 */
#ifdef __cplusplus
extern "C" {
#endif

int cadd(int a, int b);

#ifdef __cplusplus
}
#endif
/* use_good.cpp */
#include <cstdio>
extern "C" int cadd(int a, int b);         /* C 链接声明 */
int main() { std::printf("%d\n", cadd(2, 3)); return 0; }
/* 实测:编译链接成功,运行输出 5 */

编译链接命令(记住「谁的文件用谁的编译器」):

gcc -c cadd.c -o cadd.o          # C 代码 → gcc 编
g++ use_good.cpp cadd.o -o app   # 链接交给 g++(它认识 C++ 运行时)

extern "C" 的限制:它只能包住 C 兼容的函数(不能有重载、不能有模板); 一个函数名只能有一个 C 链接版本。反过来,C 里没有 extern "C" 这个概念—— C 编译器遇到 extern "C" 会直接语法错误,所以才需要上面的 #ifdef __cplusplus 守卫。


5. 移植清单:把 C 代码交给 C++ 编译器之前

  1. malloc/calloc/realloc 的返回值补上强制转换,或者整体改用 new/delete。
  2. Point p; 改成 struct Point p;(C++ 里两者都行,改了哪边都不亏)。
  3. 复合字面量 &(int){42} 改成先定义变量再取地址。
  4. VLA int a[n]; 改成 std::vector<int> a(n); 或 new int[n]。
  5. 指定初始化器改成按声明顺序写,或者用赋值语句逐个赋。
  6. 删掉 restrict;_Generic / _Static_assert / _Alignas 换成 C++ 的模板/重载、static_assert、alignas。
  7. 变量名/函数名撞 C++ 关键字的,全部改名(new、class、this、namespace、bool、and…)。
  8. goto 不要跳过带初始化的变量;case 里声明变量就给该分支加花括号。
  9. 柔性数组成员改成指针成员或标准容器。
  10. 函数声明补原型:把 void f(); 写成 void f(void);(C 里表达「无参」的正确写法,两边通用)。
  11. 跨文件共享的常量补 extern(C++ 的 const 默认内部链接)。
  12. 字符串字面量一律用 const char *(C++ 里赋给 char* 是警告,写它是错代码)。
  13. 所有函数显式写返回类型(main 也别省)。
  14. 编译时加 -Wall -Wextra,想严格再加 -pedantic-errors。

6. 反向:把 C++ 代码给 C 编译器

基本不可行,也不用尝试——两门语言已经分叉了。C 里没有:类、模板、引用、函数重载、 命名空间、异常、auto 推导、bool(C23 前)、STL。 唯一可行的边界是「用 extern "C" 导出一个 C 兼容的接口」,也就是第 4 节那套做法: C++ 实现 + C 头文件 + extern "C"。

7. 怎么用这份知识


小测

Q1:文件叫 t01.c,里面的 int *p = malloc(sizeof(int)); 用 g++ -std=c++17 t01.c 编译,会怎样?

Q2:一个文件写了 const int LIMIT = 7;,另一个文件用 extern const int LIMIT; 引用,用 g++ 链接两个目标文件,结果?

Q3:C 文件里的 int cadd(int, int),C++ 文件里只写 int cadd(int, int);(没有 extern "C")然后调用,会怎样?

Q4:sizeof('a') 在 C 和 C++ 里分别是多少?

Q5:C 代码里 void greet(); 之后调用 greet(1, 2, 3);,两边编译器的反应是?


推荐资源

C 与 C++ 的兼容边界 · 实测于 gcc / g++ 16.2.0(MSYS2 UCRT64,Windows)· 持续更新 · 返回资源列表