基础语法陷阱
-
int a = 5; printf("%d", a++ + ++a);
输出什么?
(未定义行为,结果不可预测) -
if (sizeof(int) > -1) printf("True"); else printf("False");
结果?
(输出False,sizeof
返回size_t
,与-1比较时发生类型转换) -
char str[] = "Hello"; str[5] = '!';
是否合法?
(越界访问,字符串结尾隐含\0
,索引5已超出) -
int x = 0; if (x = 1) { printf("True"); }
输出什么?
(输出True,=
赋值操作符返回左值) -
int a = 1; printf("%d", a << 32);
结果?
(未定义行为,移位超过类型宽度) -
#define SQUARE(x) x * x
,SQUARE(1+2)
的值?
(展开为1+2*1+2=5
,而非预期的9) -
int a = 1; switch(a) { case 1: case 2: printf("A"); default: printf("B"); }
输出?
(输出AB,case穿透到default) -
int a = 3, b = 5; int c = a/*b;
是否合法?
(语法错误,/*
被解析为注释开始) -
float f = 0.1; if (f == 0.1) printf("Equal");
是否成立?
(不成立,浮点数精度问题,0.1
默认是double) -
int a[3] = {1,2,3}; printf("%d", 2[a]);
输出?
(输出3,数组下标可写作a[2]
或2[a]
)
指针与内存管理
-
int *p = NULL; printf("%d", *p);
会发生什么?
(未定义行为,可能段错误) -
int arr[5]; int *p = arr + 5; *p = 10;
是否合法?
(越界访问,指针指向数组末尾后一位,非法) -
char *str = "Hello"; str[0] = 'h';
是否合法?
(未定义行为,字符串字面量只读) -
void free(int *p) { /* ... */ }
能否自定义free
函数?
(合法,但会覆盖标准库函数,导致未定义行为) -
int *p = malloc(sizeof(int)); free(p); free(p);
是否合法?
(双重释放,未定义行为) -
int a = 10; int *p = &a; printf("%d", sizeof(p));
输出?
(输出指针大小,如4或8,与a
类型无关) -
int arr[][3] = {1,2,3,4};
实际维度是?
(等效于int arr[2][3]
,自动填充0) -
int (*p)[3]
和int *p[3]
的区别?
(前者是数组指针,后者是指针数组) -
void func(int arr[]) { sizeof(arr); }
返回什么?
(返回指针大小,数组退化为指针) -
char *p = malloc(0);
是否合法?
(合法,但返回的指针可能为NULL或不可用地址)
预处理器与宏
-
#include "file.c"
是否合法?
(合法但危险,可能导致重复定义) -
#define MIN(a,b) a < b ? a : b
,MIN(1, 2++)
展开后?
(导致2++
被求值两次) -
#if sizeof(int) == 4
是否合法?
(不合法,sizeof
不能在预处理期使用) -
#define STR(x) #x
,STR(1 + 2)
的结果?
(字符串化为"1 + 2"
,空格被保留) -
#define CAT(a,b) a##b
,CAT(1, CAT(2,3))
展开结果?
(展开为1CAT(2,3)
,宏不递归展开)
类型与转换
-
char c = 255; printf("%d", c);
输出?
(若char
有符号,输出-1;无符号则255) -
int a = -1; unsigned int b = a; printf("%u", b);
输出?
(输出UINT_MAX
,二进制补码转换) -
float f = 1E39;
是否合法?
(未定义行为,溢出) -
int a = 1; long b = 2; if (a - b > 0) ...
是否成立?
(a
转换为long
,结果-1,不成立) -
printf("%d\n", (short)32768);
输出?
(溢出,结果取决于实现,可能是-32768)
函数与作用域
-
void func() { static int count = 0; count++; }
多次调用后count
的值?
(每次调用累加,静态变量只初始化一次) -
int main() { return main(); }
能否运行?
(无限递归,栈溢出) -
extern int a;
和extern int a = 0;
的区别?
(后者是定义,可能导致链接错误) -
void func(int a, int a) { ... }
是否合法?
(非法,参数名重复) -
int a; int a = 10;
是否合法?
(合法,int a;
是暂定定义)
未定义行为(UB)
-
int i = 0; printf("%d %d", i++, i++);
输出?
(未定义,参数求值顺序不确定) -
int a = 1; int *p = &a; printf("%d", *(p + 1));
是否合法?
(越界访问,UB) -
int a = 1; a = a++;
是否合法?
(UB,同一变量多次修改无序列点) -
memcpy(&a, &a, sizeof(a));
是否合法?
(UB,源和目标内存区域重叠) -
int *p; printf("%d", *p);
是否合法?
(UB,使用未初始化的指针)
地狱级反直觉问题
1. 空指针的诡异访问
int *p = NULL;
int size = sizeof(*p); // 是否触发未定义行为?
解析:sizeof(*p)
不会解引用空指针!sizeof
的操作数在编译期仅进行类型推导,不会实际计算表达式值。此代码合法且返回sizeof(int)
。
2. 数组的负数索引
int arr[5] = {1,2,3,4,5};
int *p = &arr[2];
printf("%d", p[-2]); // 输出什么?
解析:输出1
。指针允许负数索引,只要最终地址在数组范围内。p[-2]
等价于*(p - 2)
,即arr[0]
。
3. 字符串拼接的魔法
printf("Hello " "World!\n"); // 输出?
解析:输出Hello World!
。C语言在编译期自动拼接相邻字符串字面量,即使中间无+
操作符。
4. 函数指针的变形术
void (*func)() = (void(*)())0xDEADBEEF;
func(); // 会发生什么?
解析:尝试跳转到绝对地址0xDEADBEEF
执行代码。在嵌入式系统中可能合法,但通常触发未定义行为(如段错误)。
5. 循环中的浮点数陷阱
for (float f = 0.0; f != 1.0; f += 0.1) {
printf("%f\n", f);
}
解析:无限循环!浮点数精度问题导致f
永远无法精确等于1.0
(例如最后一步可能是0.999999
)。
6. 结构体的“隐身”成员
struct { char a; int b; } s;
printf("%zu", sizeof(s)); // 输出?
解析:输出可能是8
(32位系统)或更高。结构体内存对齐导致a
和b
之间有填充字节(padding)。
7. 预处理器的“时间旅行”
#define A B
#define B A
int A = 10; // 最终变量名是什么?
解析:变量名为A
。宏展开在递归检测时停止,防止无限循环。
8. 位域的神秘截断
struct { unsigned int a:3; } s = {7};
s.a++; // 现在s.a的值是?
解析:s.a
变为0
!3位无符号位域最大值为7(111
),加1后溢出为000
。
9. 逗号操作符的优先级
int x = (1, 2, 3);
int y = 1, 2, 3; // 是否合法?
解析:x
被赋值为3
(逗号表达式返回最后一个值),但第二行语法错误(逗号被解析为分隔变量声明)。
10. 未初始化的静态变量
static int a;
printf("%d", a); // 输出什么?
解析:输出0
。静态变量默认初始化为零值,与自动变量不同。
编译器魔法与UB的狂欢
11. 无限循环的消失
while (1) { /* 空循环 */ } // 编译器会优化掉吗?
解析:C标准规定无限循环必须有可观察行为,否则编译器可删除此循环!添加printf
或volatile
变量可阻止优化。
12. 指针的整数转换
int *p = (int*)1000;
p++; // p的值现在是多少?
解析:p
指向1000 + sizeof(int)
。指针算术以指向类型的大小为单位(如sizeof(int)=4
则结果为1004
)。
13. 除零的“安全”操作
int a = 0, b = 1;
float c = b / a; // 是否触发异常?
解析:不触发异常,但结果为Infinity
(浮点数除零在IEEE 754中定义),而整数除零直接导致UB。
14. 联合体的类型双胞胎
union { int a; float b; } u;
u.a = 123;
printf("%f", u.b); // 输出有意义吗?
解析:输出取决于int
和float
的二进制表示,可能打印出无意义的浮点数(如1.72e-43
)。
15. 函数参数的求值顺序
int i = 0;
printf("%d %d", i++, i++); // 输出什么?
解析:未定义行为!C标准未规定函数参数的求值顺序(可能是0 0
或1 0
,取决于编译器)。
终极黑暗技巧
16. 修改字符串字面量
char *str = "hello";
str[0] = 'H'; // 是否合法?
解析:未定义行为!字符串字面量存储在只读内存段,修改会导致段错误(但某些嵌入式系统可能允许)。
17. 数组的越界初始化
int arr[3] = {1,2,3,4}; // 是否合法?
解析:编译错误!初始化列表长度超过数组大小。
18. 类型双关的别名冲突
float pi = 3.14;
int *ip = (int*)π
printf("%d", *ip); // 是否合法?
解析:严格别名规则下属于未定义行为(通过不同类型指针访问同一内存),但实践中可能输出pi
的二进制整数表示。
19. 预处理器的字符串化魔法
#define STR(s) #s
printf("%s", STR(\n\t\\")); // 输出什么?
解析:输出换行符\n
、制表符\t
、反斜杠\\
和双引号\"
(字符串化会自动转义特殊字符)。
20. 零长度数组的幽灵
struct S { int len; char data[0]; };
struct S *s = malloc(sizeof(*s) + 10);
s->data[5] = 'A'; // 是否合法?
解析:合法(GCC扩展)。零长度数组常用于动态结构体尾部,但C99后应使用灵活数组成员(char data[];
)。
极度反直觉案例(附解析)
1. 时间旅行的restrict
关键字
int foo(int *restrict a, int *restrict b) {
*a = 10;
*b = 20;
return *a; // 返回值是10还是20?
}
解析:编译器可能优化为直接返回10!restrict
承诺两个指针不重叠,因此假设*b
的修改不影响*a
。但若实际传入重叠指针,则是UB。
2. volatile
的失效时刻
volatile int flag = 0;
while (!flag); // 其他线程可能修改flag
解析:即使flag
是volatile
,循环仍可能被优化为无限空循环!某些编译器(如旧版GCC)在多线程场景下不保证volatile
的可见性,需用原子操作或内存屏障。
3. 结构体赋值的“量子态”
struct S { int a; char b; };
struct S s1 = {1, 'A'};
struct S s2 = s1; // 是否复制填充字节?
解析:可能不复制填充字节!结构体赋值按成员复制,但填充字节的值是未定义的(可能是原值或随机值)。用memcpy
更安全。
4. switch
的隐藏入口
int i = 2;
switch (i) {
case 1: printf("A");
case 2: printf("B");
case 3: printf("C");
}
解析:输出B C!case
语句默认“穿透”(fall-through),需手动加break
。但若i=5
且无default
,则整个switch
静默跳过。
5. 数组名的“死亡转换”
int arr[5];
printf("%p %p", arr, &arr); // 输出是否相同?
解析:地址值相同,但类型不同!arr
类型是int*
,&arr
类型是int(*)[5]
。arr+1
前进sizeof(int)
,&arr+1
前进sizeof(int)*5
。
6. 函数返回的“僵尸栈”
int *func() {
int a = 42;
return &a;
}
int *p = func();
printf("%d", *p); // 能否输出42?
解析:可能输出42(栈未覆盖),但属于UB!函数返回后栈帧失效,但编译器未擦除数据,导致“暂时可用”的幻觉。
7. const
的虚假安全感
const int a = 10;
int *p = (int*)&a;
*p = 20;
printf("%d %d", a, *p); // 输出什么?
解析:可能是10 20!编译器可能将a
优化为立即数,而*p
修改的是内存中的副本。此行为属于UB,可能导致程序崩溃。
8. 预处理器的“毒药宏”
#define malloc(n) evil_malloc(n)
void *evil_malloc(size_t n);
// 之后所有代码中的malloc都会被替换!
解析:包括标准库头文件中的malloc
!在包含<stdlib.h>
前定义此类宏,会导致标准库内部使用被篡改的malloc
,引发灾难。
9. 浮点数的“静默降级”
float f = 0.1;
double d = 0.1;
if (f == d) printf("Equal");
else printf("Not Equal"); // 输出?
解析:输出Not Equal!0.1
在float
和double
中的二进制表示不同,比较时float
被提升为double
,但精度损失导致不等。
10. _Generic
的“类型幻觉”
#define type(x) _Generic((x), \
int: "int", double: "double")
char s[] = "hello";
printf("%s", type(s)); // 输出什么?
解析:编译错误!_Generic
的控制表达式(x)
会推导数组类型char[6]
,但未在列表中处理,触发编译失败。
编译器优化的魔法与诅咒
11. 消失的变量
int func() {
int a = 5;
int b = a + 1;
return b;
}
// 编译优化后可能变为直接返回6,变量a和b被删除
12. 循环的“时空折叠”
for (int i = 0; i < 1000; i++) {
arr[i] = i * 2;
}
// 可能被向量化(SIMD指令)并行处理,而非顺序执行
13. 死代码的“复活”
if (0) {
printf("Hello"); // 死代码
}
// 某些调试模式下,死代码仍会被编译(保留符号表)
恶魔级代码(切勿在实际项目中使用!)
14. 自修改代码
void func() {
char *p = (char*)func;
p[0] = 0xC3; // 修改函数开头为RET指令(x86)
}
// 调用func()后将立即返回,后续调用也失效(UB)
15. 利用未定义行为崩溃
int a = 1;
int b = 0;
int c = a / b; // 触发UB
// 可能:程序崩溃、输出任意值、静默继续执行,甚至删除整个程序逻辑!
16. 内存撕裂的原子性
// 线程1:
long x = 0;
x = 0x123456789ABCDEF0; // 非原子写入(32位系统)
// 线程2:
printf("%lx", x); // 可能看到中间状态(如0x1234567800000000)
为什么这些更反直觉?
• 违反物理世界直觉:如自修改代码、死代码复活;
• 与高级抽象背道而驰:如restrict
的优化假设、volatile
的不可靠性;
• 编译器与硬件的共谋:如循环向量化、变量消除;
• 类型系统的“欺骗”:如_Generic
的严格类型检查、const
的假不可变性;
• 时间与空间的混沌:如栈帧失效后的数据残留、结构体填充的随机性。
黑洞级反直觉问题(附解析)
1. 数组下标的时间扭曲
int i = 1;
int arr[] = {10, 20, 30};
printf("%d", i[arr - 1]); // 输出什么?
解析:输出20!等价于*(i + (arr - 1))
→ *(1 + arr -1 )
→ arr[0]
的下一个元素。实际行为是UB,但可能输出20。
2. 预处理器的"薛定谔宏"
#define M(a) a + 1
#define MM(a) M(a) * M(a)
printf("%d", MM(1)); // 输出什么?
解析:展开为1 + 1 * 1 + 1
→ 3,而非预期的(1+1)*(1+1)=4
。宏展开不自动添加括号。
3. 浮点数的"维度折叠"
float a = 1.0;
float b = 0.0;
float c = a / b; // INFINITY
float d = c * 0;
printf("%f", d); // 输出什么?
解析:输出nan!INFINITY * 0
在IEEE 754中属于未定义操作,生成NaN(Not a Number)。
4. 结构体的"平行宇宙"
struct A { int x; };
struct B { int x; };
struct A a = {1};
struct B *b = (struct B*)&a;
printf("%d", b->x); // 是否合法?
解析:合法但危险!C标准允许通过兼容类型的指针访问,但严格别名规则下属于UB。可能输出1或引发崩溃。
5. 指针的"量子隧穿"
int arr[5];
int *p = &arr[5]; // 指向数组末尾后的地址
printf("%d", p[-5]); // 访问arr[0]吗?
解析:语法合法但属于UB!指针允许指向数组末尾后的地址,但解引用该地址非法。可能正常访问arr[0],也可能触发段错误。
编译器的时间魔法
6. 循环变量的"相对论"
for (int i = 0; i < 5; i++) {
printf("%d", i);
i = i % 3; // 循环几次?
}
解析:无限循环!每次迭代i被重置为0/1/2,永远无法达到5。但某些编译器可能优化掉冗余操作。
7. 函数的"时空折叠"
void func() {
static int count = 0;
if (count++ < 5)
func();
}
// 实际调用栈深度?
解析:仅2层栈帧!编译器可能进行尾递归优化(TCO),将递归转换为循环。但C标准不强制要求TCO。
8. 位域的"概率云"
struct {
unsigned a:3;
unsigned :0; // 零宽度位域
unsigned b:5;
} s;
printf("%zu", sizeof(s)); // 大小?
解析:可能是4字节!零宽度位域强制对齐到下一个存储单元(通常为int宽度),导致a和b分开存储。
内存的混沌领域
9. malloc的"虚空造物"
int *p = malloc(0);
if (p != NULL) {
*p = 42; // 是否合法?
}
解析:UB!malloc(0)可能返回NULL或独特指针,但解引用该指针必然越界。
10. 内存的"幽灵共振"
int *p = malloc(sizeof(int));
free(p);
int *q = malloc(sizeof(int));
if (p == q) { // 可能成立吗?
*p = 42; // 会发生什么?
}
解析:可能成立!内存分配器可能重用相同地址。此时*p=42
会修改*q指向的内存,属于UB但可能静默生效。
语法的时间悖论
11. 标签的"第四维跳跃"
goto label;
int x = 5; // 跳过后是否初始化?
label:
printf("%d", x);
解析:输出未定义值!跳转到跨越变量初始化的标签属于UB,x可能包含随机值。
12. switch的"多世界诠释"
switch (1) {
default: printf("A");
case 1: printf("B");
case 2: printf("C");
}
解析:输出BC!default不要求放在最后,case执行后继续穿透后续分支。
终极黑暗技巧
13. 标准库的"恶魔低语"
printf("%d\n", printf("Hello!")); // 输出什么?
解析:先输出"Hello!",再输出6(printf返回写入的字符数)。但输出流顺序可能因缓冲策略变化。
14. 信号的"因果倒置"
#include <signal.h>
void handler(int sig) { /* 修改全局变量 */ }
int main() {
signal(SIGINT, handler);
while(1) {
// 全局变量修改可能永远不会被看到!
}
}
解析:编译器可能将全局变量缓存在寄存器中,导致信号处理程序的修改不可见。需声明变量为volatile
。
15. 预处理器的"克苏鲁召唤"
#define DEFINE_STRUCT(name) \
struct name { \
int name; \
struct name* name; \
}
DEFINE_STRUCT(MyStruct); // 是否合法?
解析:合法!生成的结构体包含同名成员和指针:struct MyStruct { int MyStruct; struct MyStruct* MyStruct; };
完全合法但极度危险。
为何这些是终极反直觉?
- 违反物理世界直觉:零宽度位域对齐、指针虚空解引用
- 挑战逻辑因果律:goto跳过初始化、递归的栈折叠
- 暴露计算机本质:内存重用、尾递归优化
- 类型系统的崩塌:结构体自指、同名成员
- 编译器的"读心术":变量缓存优化、死代码复活