一句话: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。
| 层次 | 发生什么 | 典型症状 |
|---|---|---|
| ① 语法 / 语义 | 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。
| 写法 | C | C++ | 为什么 |
|---|---|---|---|
int *p = malloc(sizeof(int)); | OK | 错误 | C++ 不许 void* 隐式转成别的指针 |
struct Point {…}; Point p; | 错误 | OK | C 里标签名不是类型名,必须写 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 里声明变量、后面还有别的 case | OK | 错误 | 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++ 里是内建关键字 |
| 写法 | C | C++ | 为什么 |
|---|---|---|---|
int new = 5; / int class = 2; | OK | 错误 | new、class、template、this、try 在 C 里是普通标识符,在 C++ 里是关键字 |
bool ok = true; | C11 错误 / C23 OK | OK | C23 才把 bool / true / false 收进语言,之前要 <stdbool.h> |
x and y | 错误 | OK | 见上表最后一行(C 需要 iso646.h 宏) |
| 写法 | C 结果 | C++ 结果 | 说明 |
|---|---|---|---|
sizeof('a') | 4 | 1 | C 里字符常量类型是 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++ 里永远不合法 |
/* 这份代码叫 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;。
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;,这个习惯两边通用。
show(&(int){42}); /* C99 复合字面量:造一个临时 int 并取地址 */
/* C++: t03.c:5:16: error: lvalue required as unary '&' operand */
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。
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 支持了指定初始化器,但要求按声明顺序)
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 用函数重载或模板代替。
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()。
#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) 的习惯,代码就能两边通用。
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 的生命周期限制在分支内。
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 代码时,见到这些名字要改名(这是「移植」里最常见的机械性修改)。
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 打印是未定义行为。
/* 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 版本。
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。
这是跨语言调用唯一必学的一节。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 守卫。
malloc/calloc/realloc 的返回值补上强制转换,或者整体改用 new/delete。Point p; 改成 struct Point p;(C++ 里两者都行,改了哪边都不亏)。&(int){42} 改成先定义变量再取地址。int a[n]; 改成 std::vector<int> a(n); 或 new int[n]。restrict;_Generic / _Static_assert / _Alignas 换成 C++ 的模板/重载、static_assert、alignas。new、class、this、namespace、bool、and…)。goto 不要跳过带初始化的变量;case 里声明变量就给该分支加花括号。void f(); 写成 void f(void);(C 里表达「无参」的正确写法,两边通用)。extern(C++ 的 const 默认内部链接)。const char *(C++ 里赋给 char* 是警告,写它是错代码)。main 也别省)。-Wall -Wextra,想严格再加 -pedantic-errors。基本不可行,也不用尝试——两门语言已经分叉了。C 里没有:类、模板、引用、函数重载、
命名空间、异常、auto 推导、bool(C23 前)、STL。
唯一可行的边界是「用 extern "C" 导出一个 C 兼容的接口」,也就是第 4 节那套做法:
C++ 实现 + C 头文件 + extern "C"。
sizeof('a')、const 链接性、void f() 的语义。struct Point、
malloc 不转换、void f()、data[] 这几样,常见 C 代码就都能读了。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);,两边编译器的反应是?
extern "C"、const 链接性都用它。_wcppc/(matrix.py 可重跑,report.txt 是原始输出)。C 与 C++ 的兼容边界 · 实测于 gcc / g++ 16.2.0(MSYS2 UCRT64,Windows)· 持续更新 · 返回资源列表