C语言期末速成12:预处理、位运算与杂项
宏替换的括号陷阱、位运算应用、static 三种作用与经典递归汉诺塔。
本文是《C语言期末速成》系列第 12 篇,也是收官篇。把前面没细讲的”边角料中的大考点”一次收网:
#define宏替换的括号陷阱、位运算的应用、static的三种作用、文件包含,以及必考的递归经典——汉诺塔。
考点清单
- 预处理在编译前完成,不占运行时间、不属 C 语言、不加分号
#define是纯文本替换,带参宏要注意参数和整体都要加括号- 位运算:
&(按位与)、|(按位或)、^(按位异或)、~(取反)、<<(左移)、>>(右移) - 左移一位 ≈ 乘以 2,右移一位 ≈ 除以 2(未丢位时)
- 异或性质:
x ^ x = 0、x ^ 0 = x;可用异或无临时变量交换 #include "xxx.c"也是合法的文件包含;文件不一定有 main,头文件不一定以.h结尾static三种作用:静态局部变量(只初始化一次)、静态全局变量/函数(限本文件)、修饰 static 变量默认值为 0- 递归必考:必须有结束条件;经典题型汉诺塔
全局变量(不初始化)默认 0;int x(局部)默认不定值;static int x默认 0
一、预处理:编译前的”文字替换”
预处理是编译器最前面的一个阶段,处理所有 # 开头的指令,做完纯文本替换,不检查语法。因此:
- 预处理指令不是 C 语句:不占运行时间、不属 C 语言语法、不要加分号;
#define PI 3.14;是错的——分号会被原样替换进代码里,制造诡异错误。
#define PI 3.14159 /* 对:不加分号 */
#define PI 3.14159; /* 错:分号会被替换进代码 */
带参数宏:替换的”括号陷阱”(必考)
#define F(x) x * x
#define G(x) (x) * (x)
两者替换 F(2+3) 的结果完全不同:
F(2+3) → 2+3*2+3 /* = 11,完全不是 25! */
G(2+3) → (2+3)*(2+3) /* = 25,正确 */
原因:宏是逐字替换,不是先算参数。F(2+3) 把 x 的位置直接替换成 2+3,得到 2+3*2+3,按优先级算出 11。
正确写带参宏的规范:参数加括号 + 整体加括号:
#define SQ(x) ((x) * (x))
#define ADD(a,b) ((a) + (b))
SQ(3) → 9
SQ(1+2) → ((1+2)*(1+2)) = 9
10 / ADD(2,3) → 10 / ((2)+(3)) = 2 /* 整体括号保住了除法优先级 */
如果不给整体加括号,10/ADD(2,3) 会变成 10/(2)+(3) = 8。加括号是避免优先级翻车的唯一办法。
副作用陷阱(加深):宏参数出现几次就替换几次,所以传带副作用的表达式(如自增)会被重复执行:
#define SQ(x) ((x) * (x))
int a = 2;
int r = SQ(a++);
/* 展开成 ((a++) * (a++)):a 被自增了两次!结果也是未定义行为 */
而函数调用 square(a++) 只执行一次。带参宏不要传 a++、i-- 这类会改值的实参——这也是宏和函数的本质区别:宏在编译前替换、无类型检查、参数可被多次求值,函数在运行时调用、有类型检查、参数求值一次。
宏与函数的区别(加深)
| 对比 | 宏 | 函数 |
|---|---|---|
| 何时替换 | 预处理(编译前) | 调用时(运行时) |
| 是否占运行时间 | 否 | 是 |
| 有类型检查 | 无(纯文本) | 有 |
| 参数计算次数 | 每次替换出现几次就算几次 | 一次 |
宏快但危险(无类型检查、易重复计算副作用),适合简单常量替换;逻辑复杂用函数。
二、位运算:在二进制层面干活
位运算直接操作二进制位。第 2 章说过,运行时所有数据都以二进制存在,位运算就是”在二进制层面做手脚”。
2.1 六个位运算符
| 运算符 | 含义 | 规则 |
|---|---|---|
& | 按位与 | 全 1 才 1:1&1=1,其余为 0 |
| | 按位或 | 有 1 则 1:0|1=1 |
^ | 按位异或 | 不同为 1:1^0=1、0^0=0、1^1=0 |
~ | 按位取反 | 1 变 0,0 变 1 |
<< | 左移 | 低位补 0 |
>> | 右移 | 高位补 0(无符号)或符号位 |
考试记忆法:异或 ^ 一男(1)一女(0)才能生小孩(1)——两个 0 或两个 1 都得 0。
~ 取反的负数规律(加深):取反是逐位翻转,正数取反得到负数,用补码表示(负数按补码存储):
~1 → -2 ~0 → -1
~5 → -6 ~7 → -8
规律很简单:~x 恒等于 -x - 1。所以 ~0 = -1,~5 = -6,遇到取反题可以快速心算。
2.2 移位:快速乘除
int x = 6; /* 二进制 110 */
printf("%d\n", x << 2); /* 11000 = 24,即 6×4 */
printf("%d\n", x >> 1); /* 011 = 3,即 6÷2 */
在没丢掉位的前提下,左移一位等于乘 2,右移一位等于除 2。移位比乘除快,编译器经常这样优化。
char a = 6;
char b = a << 2; /* 先把 6 转二进制 110,左移 2 位得 11000 = 24 */
做题流程:十进制 → 二进制 → 位运算 → 二进制 → 十进制,不要试图在十进制层面心算。
2.3 位运算的四个实用套路(加深)
① 按位与 &:判断奇偶(利用最低位)
if (n & 1) /* 奇数 */ else /* 偶数 */
奇数的最低位是 1,偶数最低位是 0,n & 1 直接看最低位。比 n % 2 更快。
② 按位或 |:置位(把某位设为 1)
int flags = 0;
flags |= 4; /* 0000 | 0100 = 0100,把第 2 位置 1 */
③ 异或 ^:翻转(把某位取反)
int x = 0b1010; /* 0b 只是表达,C 用十进制/八/十六进制书写 */
x ^= 0b0100; /* 第 2 位翻转 */
④ 异或交换:无临时变量交换两个数
int a = 3, b = 5;
a = a ^ b; /* a = 3^5 */
b = a ^ b; /* b = 3^5^5 = 3(b 变成原来的 a) */
a = a ^ b; /* a = 3^5^3 = 5(a 变成原来的 b) */
原理:x ^ x = 0、x ^ 0 = x,用 3 次异或完成交换,不占额外变量。面试和考试都爱考。
三、文件包含:把另一个文件”粘”进来
#include 把头文件或源文件的内容原样插入到当前文件这个位置:
/* no1.c */
#include "no2.c" /* 把 no2.c 的内容包含进来 */
int main(void) {
printf("%d\n", add(29, 33)); /* 调用 no2.c 里定义的函数 */
return 0;
}
/* no2.c */
int add(int a, int b) {
return a + b;
}
两个常考的”反常识”判断题:
- “一个 C 程序的文件必须有 main” → 错。no2.c 被包含进来就没有 main,它只是函数库。
- “头文件一定以
.h结尾” → 错。#include "no2.c"以.c结尾照样合法。
<stdio.h>(尖括号)从系统目录找,"my.h"(双引号)先从当前目录找。
四、static 的三种作用(必考)
static 在不同位置含义不同,一共三种:
1. 静态局部变量:只初始化一次
void counter(void) {
static int n = 0; /* 只初始化一次! */
n++;
printf("%d\n", n);
}
int main(void) {
counter(); counter(); counter(); /* 输出 1 2 3 */
return 0;
}
普通局部变量每次调用都重新初始化为 0;static 局部变量的初始化只执行一次,值在函数调用之间被保留——所以三次调用 n 依次是 1、2、3。
2. 静态全局变量 / 静态函数:限制在本文件
static 修饰文件内的全局变量或函数时,表示只在本文件可见,别的文件(即使 extern)也访问不到——用于模块封装。
3. 默认初始化
static int x; /* x = 0 */
static 变量(未显式初始化)默认值为 0。对比:全局变量(未初始化)默认 0;局部变量 int x; 默认是”不定值”(栈上的残留垃圾值)。
五、递归必考:汉诺塔(加深)
递归(第 6 章)必考,汉诺塔是”递归思想”的终极题型:把 n 个盘子从 A 移到 C,借助 B,每次只移一个且大盘不能压小盘。
思路拆解(先想 n-1 个盘子怎么办):
- 把上面 n-1 个盘子从 A 借助 C 移到 B;
- 把最下面那个大盘从 A 直接移到 C;
- 把 B 上的 n-1 个盘子借助 A 移到 C。
void hanoi(int n, char from, char aux, char to) {
if (n == 1) { /* 递归出口:只剩一个盘子 */
printf("%c -> %c\n", from, to);
return;
}
hanoi(n - 1, from, to, aux); /* 1. 上层 n-1 个:A → B(经 C) */
printf("%c -> %c\n", from, to); /* 2. 大盘:A → C */
hanoi(n - 1, aux, from, to); /* 3. 上层 n-1 个:B → C(经 A) */
}
n=3 的调用会打印 7 步(移动次数 = 2ⁿ−1)。画递归树是必杀技:每个”三句函数体”对应一次完整的”移上层、移大盘、移上层”。
递归总结:任何递归都要有递归出口,每层调用必须缩小问题规模;手算时画递归树,从出口开始往上回填。
补充:递归基准条件的细节(填空/改错常考):
/* 阶乘:出口用 n <= 1 能同时覆盖 0! 和 1! */
int fact(int n) {
if (n <= 1) return 1; /* 基准:0! = 1! = 1 */
return n * fact(n - 1);
}
/* 斐波那契:双基准,覆盖第 1、2 项 */
int fib(int n) {
if (n == 1 || n == 2) return 1; /* 前两项都是 1 */
return fib(n - 1) + fib(n - 2);
}
写递归最容易出错的就是基准条件差一点(比如只写 n==1 导致 fact(0) 无法终止、或斐波那契漏了 n==2)。每写一个递归,先问自己:最小的输入(0 或 1)能直接返回吗?
六、杂项考点汇总
sizeof 应用:
int *p;
p = (int *)malloc(4); /* 申请 4 字节 */
p = (int *)malloc(sizeof(int)); /* 与上式等价,更通用 */
malloc 返回 void*,所以常强转。
函数指针复习(第 8 章):int (*f)(int,int); f = add; 后 add(2,3)、f(2,3)、(*f)(2,3) 三种调用等价。
指针的迷惑题:
char ch[] = "iamhandsome";
char *p = ch;
*(p + 2) /* 第 3 个字符 'm' */
*p + 2 /* 先取 'i' 的 ASCII 105,再 +2 = 107,即 'k' */
区别:*(p+2) 是”往后挪 2 个再取值”,*p+2 是”取第 1 个字符再加 2”(* 优先于 +)。
易错点 / 陷阱
#define加分号:#define PI 3.14;错误,分号会被替换进代码。- 带参宏不加括号:
#define SQ(x) x*x,SQ(1+2)得 5,必须写((x)*(x))。 - 位运算在十进制层面心算:统一”十进制→二进制→运算→十进制”。
- 异或交换原理记错:靠
x^x=0和x^0=x,顺序 a^b→b^a→a^b。 - static 局部变量每次重新初始化:只初始化一次,跨调用保留。
int x;(局部)默认 0:局部变量默认是不定值,static int x才是 0。- 头文件必须
.h/ 每个文件必须 main:#include "xxx.c"合法,被包含文件可以没有 main。 *p+2与*(p+2)混淆:*优先于+,前者先取值再加。
自测小练习
第 1 题:#define SQ(x) (x)*(x),求 SQ(2+3)。
第 2 题:int x = 7; 求 x << 1 和 x >> 1(用二进制推导)。
第 3 题:用异或写出交换 a、b 的三行代码。
第 4 题:void f(){ static int c = 0; c++; printf("%d", c); } 连续调用 3 次,输出什么?
第 5 题:char *s = "abcd"; 求 *(s+1) 和 *s + 1 各是多少?
参考答案
(2+3)*(2+3) = 25(x整体被替换,但注意本宏整体没括,若10/SQ(2+3)会错)。7 = 0111;x<<1 = 1110 = 14(乘 2);x>>1 = 0011 = 3(除 2,丢了一位)。a ^= b; b ^= a; a ^= b;123。static 局部变量只初始化一次,跨调用保留。*(s+1) = 'b'(第二个字符);*s + 1 = 97+1 = 98 = 'b'的 ASCII,但值是 98。
系列总结
从第 1 篇的程序骨架,到数据、运算符、输入输出,再到三大结构、函数、数组、指针、字符串、结构体链表、文件读写,最后以预处理和位运算收尾——你手中的 C 语言知识已经连成一张网:
- 骨架(第 1 篇)→ 数据(第 2 篇)→ 操作(第 3 篇)→ 交互(第 4 篇)
- 流程(第 5 篇)→ 模块化(第 6 篇)→ 批处理数据(第 7 篇)→ 间接访问(第 8 篇)
- 文本处理(第 9 篇)→ 组织数据(第 10 篇)→ 文件读写(第 11 篇)→ 编译与底层(第 12 篇)
上考场前,把每章末尾的”易错点”再过一遍,那才是真正帮你捡分的地方。祝期末顺利!