本文是《C语言期末速成》系列第 12 篇,也是收官篇。把前面没细讲的”边角料中的大考点”一次收网:#define 宏替换的括号陷阱、位运算的应用、static 的三种作用、文件包含,以及必考的递归经典——汉诺塔。


上一篇C语言期末速成 11:文件基本操作

考点清单

  • 预处理在编译前完成,不占运行时间、不属 C 语言、不加分号
  • #define纯文本替换,带参宏要注意参数和整体都要加括号
  • 位运算:&(按位与)、|(按位或)、^(按位异或)、~(取反)、<<(左移)、>>(右移)
  • 左移一位 ≈ 乘以 2,右移一位 ≈ 除以 2(未丢位时)
  • 异或性质:x ^ x = 0x ^ 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=10^0=01^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 = 0x ^ 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;
}

两个常考的”反常识”判断题:

  1. “一个 C 程序的文件必须有 main” → 错。no2.c 被包含进来就没有 main,它只是函数库。
  2. “头文件一定以 .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 个盘子怎么办):

  1. 把上面 n-1 个盘子从 A 借助 C 移到 B;
  2. 把最下面那个大盘从 A 直接移到 C;
  3. 把 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”(* 优先于 +)。


易错点 / 陷阱

  1. #define 加分号#define PI 3.14; 错误,分号会被替换进代码。
  2. 带参宏不加括号#define SQ(x) x*xSQ(1+2) 得 5,必须写 ((x)*(x))
  3. 位运算在十进制层面心算:统一”十进制→二进制→运算→十进制”。
  4. 异或交换原理记错:靠 x^x=0x^0=x,顺序 a^b→b^a→a^b。
  5. static 局部变量每次重新初始化:只初始化一次,跨调用保留。
  6. int x;(局部)默认 0:局部变量默认是不定值,static int x 才是 0。
  7. 头文件必须 .h / 每个文件必须 main#include "xxx.c" 合法,被包含文件可以没有 main。
  8. *p+2*(p+2) 混淆* 优先于 +,前者先取值再加。

自测小练习

第 1 题#define SQ(x) (x)*(x),求 SQ(2+3)

第 2 题int x = 7;x << 1x >> 1(用二进制推导)。

第 3 题:用异或写出交换 a、b 的三行代码。

第 4 题void f(){ static int c = 0; c++; printf("%d", c); } 连续调用 3 次,输出什么?

第 5 题char *s = "abcd";*(s+1)*s + 1 各是多少?

参考答案
  1. (2+3)*(2+3) = 25x 整体被替换,但注意本宏整体没括,若 10/SQ(2+3) 会错)。
  2. 7 = 0111x<<1 = 1110 = 14(乘 2);x>>1 = 0011 = 3(除 2,丢了一位)。
  3. a ^= b; b ^= a; a ^= b;
  4. 123。static 局部变量只初始化一次,跨调用保留。
  5. *(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 篇)

上考场前,把每章末尾的”易错点”再过一遍,那才是真正帮你捡分的地方。祝期末顺利!


上一篇C语言期末速成 11:文件基本操作

评论区