第5章 · 函数¶
本页最后更新于 ,内容可能过时
本章题目对应教材第 5 章,按 5.1 函数定义与调用、5.2 递归函数基础、5.8 存储类别和作用域、5.9 编译预处理命令的顺序排列。
5.1 函数定义与调用¶
1 · 值传递与地址传递¶
在 C 语言中,以下关于函数参数传递的说法哪一个是正确的?
- A. 值传递不会改变实参的值
- B. 参数是结构体指针时,需要传递整个结构体的数据
- C. 指针传递不能用于传递数组
- D. 数组作为函数参数时,传递的是数组的副本
答案
A
解析
逐项分析——
- A 对:值传递是把实参的副本给形参,函数内怎么改都不影响实参。注意这句话成立的前提:改的是形参本身。传指针时"改指针指向的内容"照样能影响实参,那不叫"改实参的值"(见下表第 2 行)。
- B 错:传结构体指针恰恰是为了避免拷贝整个结构体。
- C 错:数组名退化为指针,指针传递正是传数组的标准方式。
- D 错:数组作参数时传递的是首元素地址,形参和实参是同一块内存,函数内修改会直接影响原数组。这一点与直觉相反。
两张对照表:
传递方式 形参 能否改实参的值 能否改实参指向的数据 void f(int x)副本 ❌ — void f(int *p)地址副本 ❌(改 p无效)✅(改 *p有效)void f(int a[])首地址 ❌ ✅
2 · 二维数组作函数参数 · 按列求最值¶
请写出以下程序运行所产生的输出。
#include <stdio.h>
#define N 4
void fun( int a[][N], int b[], int flag )
{ int i, j;
for( i=0; i<N; i++ )
{ b[i] = a[0][i];
for( j=1; j<N; j++ )
if ( flag ? (b[i] > a[j][i]) : (b[i] < a[j][i]) )
b[i] = a[j][i];
}
}
int main()
{ int x[N][N]={1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,16}, y[N], i;
fun(x, y, 1);
for (i=0;i<N; i++) printf("%d,", y[i]);
fun(x, y, 0);
for (i=0;i<N; i++) printf("%d,", y[i]);
return 0;
}
解析
第一步:确定数据。 形参 int a[][N] 省略第一维、必须写出第二维;x 按行优先填满,所以 x[i][j] = i*4 + j + 1,即
x[i][j] |
j=0 | j=1 | j=2 | j=3 |
|---|---|---|---|---|
| i=0 | 1 | 2 | 3 | 4 |
| i=1 | 5 | 6 | 7 | 8 |
| i=2 | 9 | 10 | 11 | 12 |
| i=3 | 13 | 14 | 15 | 16 |
第二步:fun 的功能。 外层 i 走的是列(注意内层比较用的都是 a[j][i],i 固定、j 变行)——所以 fun 是一列一列处理的。
先取 b[i] = a[0][i] 作为当前值,再让 j 从 1 到 3 依次比较:flag=1 时条件是 b[i] > a[j][i],比当前值小就更新,最后得到该列最小值;
flag=0 时条件反向,留下的是该列最大值。
第三步:逐列计算。 比较的是同一列的 4 个数(上下方向),不是同一行的数:
| 列 i | 该列的 4 个数(a[0][i] → a[3][i]) |
fun(x,y,1):取最小 |
fun(x,y,0):取最大 |
|---|---|---|---|
| 0 | 1, 5, 9, 13 | 1 | 13 |
| 1 | 2, 6, 10, 14 | 2 | 14 |
| 2 | 3, 7, 11, 15 | 3 | 15 |
| 3 | 4, 8, 12, 16 | 4 | 16 |
第四步:输出。 第一次 fun(x,y,1) 后 y = {1,2,3,4},打印 1,2,3,4,;
第二次 fun(x,y,0) 把 y 整个覆盖成 {13,14,15,16},接着打印 13,14,15,16,。
printf("%d,") 是每个数后面跟一个逗号、没有换行,两次输出的数字首尾相接,所以最终是 1,2,3,4,13,14,15,16,(末尾也留着逗号)。
易错:把
a[j][i]看成a[i][j],于是按"行"去算——那会得到1,1,1,1,16,16,16,16,之类的错答案。只要下标写成[j][i],就一定是按列处理。易错:
y是同一块内存,第二次调用是覆盖而不是追加,两次输出不是两个独立数组的结果拼接。
5.2 递归函数基础¶
3 · 递归的执行顺序(回溯)¶
请写出以下程序的运行结果。
#include <stdio.h>
void sub(int *a, int n, int k)
{
if (k <= n)
sub(a, n / 2, 2 * k);
*a += k;
}
void main()
{
int x = 0;
sub(&x, 4, 1);
printf("%d\n", x);
}
答案
7
解析
注意 *a += k 写在递归调用之后,所以累加发生在回溯阶段("归"的时候),由内向外:
| 调用 | k <= n? |
说明 |
|---|---|---|
sub(&x, 4, 1) |
1 ≤ 4 ✓ | 先递归 |
↳ sub(&x, 2, 2) |
2 ≤ 2 ✓ | 先递归 |
↳↳ sub(&x, 1, 4) |
4 ≤ 1 ✗ | 不递归,x += 4 → x = 4 |
↳ x += 2 |
x = 6 |
|
x += 1 |
x = 7 |
最终输出 7。(已用 gcc 15.2.0 实测确认。)
易错:把递归写成循环去推,得到
x += 1+2+4 = 7—— 答案碰巧一样,但过程理解是错的(本题恰好是累加,顺序无关;换成*a = *a * 2 + k之类就完全不同了)。
*a是"改实参指向的内容"的范例:a是形参指针(副本),但*a指向的就是main里的x。
4 · 递归的执行顺序(回溯)¶
请写出以下程序的运行结果。
#include <stdio.h>
void add(int *a, int n, int k) {
if (k > 0) {
add(a, n - 1, k - 1);
}
*a += (n + k);
}
int main() {
int x = 0;
add(&x, 3, 2);
printf("%d\n", x);
return 0;
}
答案
9
解析
*a += (n+k) 在递归调用之后,所以累加发生在回溯阶段:
| 调用 | k > 0? |
回溯时的 *a += (n+k) |
|---|---|---|
add(&x, 3, 2) |
2 > 0 ✓ 先递归 | x += 5 |
↳ add(&x, 2, 1) |
1 > 0 ✓ 先递归 | x += 3 |
↳↳ add(&x, 1, 0) |
0 > 0 ✗ 不递归 | x += 1 ← 最先执行 |
执行顺序是 1 → 3 → 5,累计 x = 1 + 3 + 5 = 9。(已用 gcc 15.2.0 实测确认。)
换个参数递减方式,
sub的函数结构完全一样(同样是"递归 +*a += k")——两种写法都要掌握,才能理解"递归中语句位置决定执行顺序"这一点。
5 · 递归出口与无限递归¶
/* 第 1 行 */ void digit(int n) {
/* 第 2 行 */ char ch;
/* 第 3 行 */ if(n < 10) {
/* 第 4 行 */ printf("%c", n + "a");
/* 第 5 行 */ }
/* 第 6 行 */ else {
/* 第 7 行 */ printf("%c", n % 10 + 'a');
/* 第 8 行 */ digit(n);
/* 第 9 行 */ }
/* 第 10 行 */ }
/* 第 11 行 */ int main() {
/* 第 12 行 */ digit(1234);
/* 第 13 行 */ return 0;
/* 第 14 行 */ }
要求 digit(1234) 输出 edcb。请指出错误行并写出正确语句(不增删语句、不改程序结构)。
答案
共 2 处错误:
| 行 | 错误 | 改正 | 错因 |
|---|---|---|---|
| 4 | printf("%c", n + "a"); |
printf("%c", n + 'a'); |
双引号 "a" 是字符串(地址),不能和整数相加;要用单引号的字符常量 'a' |
| 8 | digit(n); |
digit(n / 10); |
递归参数没有缩小 → 无限递归,永远不会到达 n < 10 的出口 |
解析
- 第 4 行的
"a"vs'a':这是本课程常见的一类错误。n + "a"是"整数 + 指针",类型完全错;即使能算,得到的也是地址偏移而不是字符。 - 第 8 行的递归边界:递归函数必须让参数朝终止条件收敛。这里每次要除以 10,才能从 1234 → 123 → 12 → 1,进入
n < 10分支。写成digit(n)就是自己调自己、参数不变 → 无限递归(栈溢出)。 - 注意
%c打印的是字符:1234 % 10 + 'a'=4 + 97=101→'e'。
执行过程(修正后):
digit(1234): 1234 ≥ 10 → 打印 '4'+'a' = 'e',再 digit(123)
digit(123) : 打印 '3'+'a' = 'd',再 digit(12)
digit(12) : 打印 '2'+'a' = 'c',再 digit(1)
digit(1) : 1 < 10 → 打印 '1'+'a' = 'b'
输出 edcb ✅(本题的打印在递归调用之前,所以是"递"的时候按高位到低位正序输出。)
顺带一提:第 2 行
char ch;声明了却没用——不算错误(编译只是警告),改错题不要改这种"无用但合法"的语句,会超出题目的错误数量限制。
6 · 递归辗转相减求最大公约数¶
写出以下程序的运行结果。
#include <stdio.h>
int gcd(int m, int n)
{
if (m == n)
return m;
else if (m > n)
return gcd(m - n, n);
else
return gcd(m, n - m);
}
void main()
{
printf("Gcd=%d\n", gcd(24, 36));
}
答案
Gcd=12
解析
gcd 用的是辗转相减法(更相减损术),三条分支的作用如下:
| 分支 | 含义 | 动作 |
|---|---|---|
m == n |
递归出口 | 返回 m(两数相等,它就是公约数) |
m > n |
大数减小数 | gcd(m - n, n) |
m < n |
大数减小数 | gcd(m, n - m) |
每递归一层,两数中较大的那个就被减掉较小的那个,两数之和严格变小,所以一定会收敛到 m == n,不会无限递归。
gcd(24, 36) 的完整调用过程:
| 层 | 调用 | 比较 | 走向 |
|---|---|---|---|
| ① | gcd(24, 36) |
24 < 36 | → gcd(24, 36-24) = gcd(24, 12) |
| ② | gcd(24, 12) |
24 > 12 | → gcd(24-12, 12) = gcd(12, 12) |
| ③ | gcd(12, 12) |
12 == 12 ✅ | return 12 ← 递归出口 |
| 回溯 | ①② 逐层把 12 原样传回 main |
所以 printf("Gcd=%d\n", gcd(24, 36)) 输出 Gcd=12。验算:24 = 12×2,36 = 12×3,12 确实是最大公约数。
易错:
- 把两个分支的条件记反(必须在
m > n时才做gcd(m-n, n),否则会减出负数或 0)- 漏掉
m == n这个出口,以为会无限递归- 数错层数——本题只递归两层就到达出口,不需要减很多次(36-24=12,24-12=12,两步就到达出口)
注意排版:题干里
else后面的return gcd(m, n - m);被挤到了函数的}之外(页边排版噪声),已按 C 语法还原到else分支下。顺带一提:题干里的
main()没写返回类型,现在应写int main(void)。
5.8 存储类别和作用域¶
7 · static 局部变量 + 递归¶
请写出以下程序运行所产生的输出。
#include <stdio.h>
void func(int n)
{
static int count = 0;
if (n > 0) {
count++;
func(n - 1);
printf("%d ", count);
}
}
int main(void)
{
func(3);
printf("\n");
func(2);
return 0;
}
解析
两个关键点:
static局部变量只初始化一次,且生命周期贯穿整个程序。 所以第二次调用func(2)时count从 3 接着数,而不是从 0 重来。printf写在递归调用之后,所以是"归"的时候打印,顺序是逆的。
func(3):
- 调用链
func(3) → func(2) → func(1) → func(0),count依次变成 1、2、3; func(0)时n > 0不成立,直接返回(不打印);- 回溯时打印:
func(1)打印3、func(2)打印3、func(3)打印3→3 3 3
func(2):
count从 3 继续 → 4、5;- 回溯打印
5 5
合计输出 3 3 3 换行 5 5。
易错:
- 把
count当成普通局部变量(每层递归一份)——static变量全程只有一份- 以为打印是"递"的时候发生(那样会输出
1 2 3)- 以为第二次调用会重新从 1 开始(那样会输出
1 2)语句在递归调用前 = 递推时执行(正序);在递归调用后 = 回归时执行(逆序)。
8 · static / 存储类别¶
关于静态变量,以下哪项说法是正确的?
- A. 静态变量在函数调用结束后会被销毁
- B. 静态变量在程序运行期间只初始化一次
- C. 函数中定义的静态变量可以在不同的函数之间共享
- D. 静态变量必须在函数外部声明
答案
B
解析
- A 错:
static局部变量的生命周期是整个程序,函数返回后它依然存在(只是作用域出了函数)。 - B 对:初始化只在第一次进入时执行一次,之后每次进入保留上次的值。
- C 错:
static局部变量的作用域仍限于本函数,别的函数看不见(想跨函数共享要用文件作用域的全局static)。 - D 错:
static既可以修饰全局变量/函数(限定文件内可见),也可以修饰局部变量。
区分"作用域"与"生命周期"——这是本考点的核心:
作用域 生命周期 普通局部变量 函数内 函数执行期间 static局部变量函数内 整个程序 全局变量 本文件(加 static)或全程序整个程序
9 · 按需求选择存储类别¶
如果希望调用某一函数时,使该函数内某一变量拥有最近一次退出该函数时所拥有的值,同时不希望其他函数访问该变量,则该变量应定义为什么存储类型?
- A.
auto - B.
register - C.
extern - D.
static
答案
D(static)
解析
题目给了两个条件,正好对上 static 局部变量的两个特性:
- "保留上次退出时的值" → 需要生命周期贯穿整个程序 → 排除
auto、register; - "不希望其他函数访问" → 需要作用域限于本函数 → 排除
extern(extern恰恰是声明"别处定义的全局变量")。
四个存储类别的速查:
| 存储类别 | 存放位置 | 生命周期 | 作用域 | 初值 |
|---|---|---|---|---|
auto(默认) |
栈 | 函数执行期间 | 函数内 | 不确定(垃圾值) |
register |
寄存器(建议) | 函数执行期间 | 函数内 | 不确定 |
static 局部 |
静态区 | 整个程序 | 函数内 | 自动为 0 |
extern / 全局 |
静态区 | 整个程序 | 全程序 | 自动为 0 |
易错:把"不被其他函数访问"理解成
extern。extern是专门用来"跨文件访问"的,方向正好相反。
10 · extern 与全局变量¶
执行下列程序,输出结果是__。
#include <stdio.h>
int x=5;
int func(int x1,int x2){
extern int x;
x1>x2?(x=3):(x=4);
return x+x1;
}
void main(){
printf("%d",func(6,7));
}
- A. 9
- B. 10
- C. 11
- D. 12
答案
B(10)
解析
函数里的 extern int x; 是声明而不是定义,它让这一行的 x 绑定到全局变量 x(值 5),不会新开一个局部变量。
调用 func(6,7) 时 6>7 为假 → 执行 (x=4),把全局 x 改成 4,于是 return x + x1 = 4 + 6 = 10。
这里要注意形参叫 x1、全局叫 x,漏看 extern 就会以为 x 是个没定义的局部变量;另外 x1 是形参(值 6),不是全局的 5。
11 · 全局变量的作用域¶
以下代码中的变量 a 具有什么作用域?
- A.
a仅在函数func内有效 - B.
a仅在函数外有效 - C.
a在整个程序中有效 - D.
a在func中无效
答案
C
解析
a 写在所有函数之外,是全局变量,从定义处起在本文件内处处可见,各函数内部都能直接引用——func 里 printf("%d", a) 完全合法,所以选 C。
B 和 D 都把"作用域在函数外"理解成了"函数内用不了",方向正好说反;对照 func 里的 b,那才是只在函数内有效的局部变量。
严格说跨文件访问还需 extern 声明,本题按"整个程序有效"理解即可。
12 · 默认存储类别¶
未指定存储类别的变量,其隐含的存储类别为( )。
- A.
auto - B.
static - C.
extern - D.
register
答案
A(auto)
解析
C 规定:函数内部的局部变量不写存储类别时,隐含就是 auto——存放在栈上,函数返回即销毁,初值不确定。
static、extern、register 三者的语义都必须显式写出才会生效,标准不会自动赋予。
注意这条"默认 auto"只针对局部变量,别和函数外定义的全局变量(具有外部链接、可被 extern 引用)混为一谈。
13 · static 局部变量跨调用保留¶
写出以下程序的运行结果(题面要求:空格用 □ 表示)。
#include <stdio.h>
int fun(int x)
{
int y = 0;
static int z = 3;
z++; ++y;
return(x + y + z);
}
void main()
{
int i, x = 2;
for (i = 0; i < 2; i++)
printf("%5d", fun(x++));
}
解析
%5d = "宽度 5、右对齐",所以每个数前补 4 个空格。真正要算的是两次 fun 的返回值,关键差别在这三个变量身上:
| 变量 | 声明 | 每次进入函数时 | 跨调用是否保留 |
|---|---|---|---|
x |
形参 | 由实参决定 | 不保留(每次都是新的形参) |
y |
int y = 0; |
重新执行 y = 0,之后 ++y → 1 |
❌ 每次重置 |
z |
static int z = 3; |
只在程序启动时初始化一次,z++ 在上次的值上继续 |
✅ 全程保留 |
调用过程表:
| 第几次调用 | main 里的 x |
传入的 x |
y(重置 0 → ++y) |
z(z++ 前 → 后) |
返回 x+y+z |
|---|---|---|---|---|---|
第 1 次 fun(x++) |
2 → 3 | 2 | 0 → 1 | 3 → 4 | 2+1+4 = 7 |
第 2 次 fun(x++) |
3 → 4 | 3 | 0 → 1 | 4 → 5 | 3+1+5 = 9 |
- 循环只跑 2 次(
i = 0, 1),所以只调用两次fun。 x++是后缀自增:先把旧值传给fun,回到main之后x才加 1,所以两次传入的是 2 和 3(不是 3 和 4)。static int z = 3;的初始化只在程序启动时做一次;第 2 次进fun时z已经是 4,z++得 5。
易错:
- 把
z当普通局部变量,算成7 7- 以为
z++每次都从 3 开始- 把
x++看成++x,算成两次传 3、4
static局部变量的作用域仍在函数内,生命周期却是整个程序:初始化只做一次,之后每次进来接着用上次的值。顺带一提:题干里的
main()没写返回类型(当年的教材风格),现在应写int main(void)并return 0;。
14 · 值传递与全局变量¶
写出以下程序的运行结果(题面要求:空格用 □ 表示)。
#include <stdio.h>
int a1 = 300, a2 = 400;
void sub1(int x, int y)
{
a1 = x;
x = y;
y = a1;
}
void main()
{
int a3 = 100, a4 = 200;
sub1(a3, a4);
sub1(a1, a2);
printf("%d,%d,%d,%d\n", a1, a2, a3, a4);
}
答案
100,400,100,200
解析
sub1 的函数体形式上像"交换"(a1 = x; x = y; y = a1;),实际没有完成交换——函数内只有实参的副本,真正被改掉的是全局变量 a1。
三条语句的实际效果:
| 语句 | 实际效果 |
|---|---|
a1 = x; |
把全局 a1 改成形参 x 的值 ← 唯一真正生效的修改 |
x = y; |
只改形参 x(副本),函数一返回就消失 |
y = a1; |
只改形参 y(副本),同样失效 |
逐步跟踪:
| 步骤 | a1(全局) |
a2(全局) |
a3(main 局部) |
a4(main 局部) |
说明 |
|---|---|---|---|---|---|
| 初值 | 300 | 400 | 100 | 200 | |
调用 sub1(a3, a4) |
300 | 400 | 100 | 200 | 形参 x=100, y=200(副本) |
↳ a1 = x; |
100 | 400 | 100 | 200 | 全局 a1 被改写(300 → 100) |
↳ x = y; |
100 | 400 | 100 | 200 | 只动副本 |
↳ y = a1; |
100 | 400 | 100 | 200 | 只动副本(此时 a1 已是 100) |
调用 sub1(a1, a2) |
100 | 400 | 100 | 200 | 实参取当前值:x=100, y=400 |
↳ a1 = x; |
100 | 400 | 100 | 200 | 把 100 再写回 a1,值不变 |
↳ x = y; y = a1; |
100 | 400 | 100 | 200 | 还是只动副本 |
printf |
100 | 400 | 100 | 200 | 输出 100,400,100,200 |
a3、a4自始至终没变(100、200):形参x/y只是副本,函数内对它们的修改影响不到实参。想改实参必须传地址(void sub1(int *x, int *y)+sub1(&a3, &a4))。a2也没变(400):它只是第二次调用的实参,函数体里没人碰全局a2。- 变了的只有
a1:300 → 100,而且是第一次调用改的;第二次调用只是把 100 又写了一遍。 - 第二次调用
sub1(a1, a2)的实参在调用前求值,此时a1已经是 100,所以传进去的是 100 和 400。
易错:
- 以为
sub1是交换函数,答成100,400,200,100——注意它没有一条语句是通过指针写回实参的,x、y全是形参- 以为第二次调用传的是
a1的初值 300,答成300,400,100,200- 没看出
a1是全局变量,以为函数里的a1 = x是在改形参顺带一提:题干里的
main()没写返回类型,现在应写int main(void)。
15 · static 变量与自动变量对照¶
写出以下程序的运行结果。
#include <stdio.h>
main()
{
int i; for (i = 1; i <= 3; i++)
f(i);
}
int f(int j)
{
static int a = 10;
int b = 1;
b++;
printf("%d,%d,%d\n", a, b, j);
a += 10;
}
解析
本题考的就是同一个函数里 static 变量与普通(自动)局部变量的对照:
| 变量 | 声明 | 初始化时机 | 每次调用的表现 |
|---|---|---|---|
a |
static int a = 10; |
只在第一次调用前做一次 | 在上次的结果上继续(a += 10) |
b |
int b = 1; |
每次调用都重新执行 | 永远 1 → 2,所以打印出来的 b 恒为 2 |
j |
形参 | 由实参决定 | 1、2、3 |
调用过程表:
| 第几次调用 | j |
a(打印时) |
b(1 → ++b) |
打印 | 打印后 a += 10 |
|---|---|---|---|---|---|
f(1) |
1 | 10 | 1 → 2 | 10,2,1 |
a:10 → 20 |
f(2) |
2 | 20 | 1 → 2 | 20,2,2 |
a:20 → 30 |
f(3) |
3 | 30 | 1 → 2 | 30,2,3 |
a:30 → 40(程序结束,不再使用) |
三个要点:
a每次 +10:打印的是加 10 之前的值(a += 10写在printf后面),所以是 10、20、30,不是 20、30、40。b恒为 2:int b = 1;每进一次函数都重新执行,b++后永远是 2——自动变量不跨调用保留。j就是 1、2、3:main里for (i = 1; i <= 3; i++) f(i);依次调用f(1)、f(2)、f(3)。
易错:
- 把
a也当成每次重置,答成三行都是10,2,?- 心算时把
a += 10提到printf前面,答成20,2,1/30,2,2/40,2,3- 把
b当成 static,答成 2、3、4注意:材料上逗号用的是全角"
,"(10,2,1),但printf的格式串"%d,%d,%d\n"用的是半角逗号,实际输出是10,2,1——真机验证确认。
static局部变量的初始化只做一次,值保留到程序结束;自动局部变量每次进入函数都重新初始化,函数返回后即销毁。两者的作用域相同(都只在本函数内可见),区别在生命周期。注意:题干用的是教材的写法(
main()没写返回类型,且f定义在main之后没有事先声明)。现在应写
int main(void),并在main之前加int f(int j);声明(或把f的定义挪到main前面);否则 gcc 15.2.0 会直接报
implicit declaration of function 'f'错误(验证时对这种老写法加了-fpermissive才跑通)。
5.9 编译预处理命令¶
预处理(# 开头)发生在编译之前,做的是纯文本层面的事:
| 指令 | 作用 |
|---|---|
#include |
把 stdio.h 等头文件内容原样插进来 |
#define / #undef |
宏定义 / 取消宏定义(宏展开 = 文本替换) |
#if #ifdef #ifndef #else #endif |
条件编译:按条件决定哪段代码参与编译(#ifndef X … #endif 就是"头文件卫士"的写法) |
宏是文本替换,不是函数调用。宏的参数在展开时直接原样代入,不会自动加括号。
宏展开的三条注意事项
- 每个参数都加括号:
((x) * (x))而不是x * x; - 整个表达式再加一层括号:
(x + 1)而不是x + 1; - 能不用宏就不用,优先
static inline函数或const常量。
原因:宏没有类型检查;参数会被求值多次(SQUARE(i++) 会让 i 自增两次);
而 static inline 函数有类型检查、参数只求值一次,且同样没有函数调用开销。
16 · 宏展开:函数式宏必须加括号¶
以下程序的运行结果是?
- A. 25
- B. 13
- C. 9
- D. 11
答案
D(11)
解析
宏展开是纯文本替换:
按优先级算:2 + (3*2) + 3 = 2 + 6 + 3 = 11。✅(如果它是函数,答案才会是 25。)
正确写法必须给参数和整体都加括号:
为什么两层括号缺一不可?
| 写法 | SQUARE(2+3) |
结果 | 够不够用 |
|---|---|---|---|
#define M(x) x*x |
2+3*2+3 |
11 ❌ | 错 |
#define M(x) (x)*(x) |
(2+3)*(2+3) |
25 ✅ | 本题够用,但换个场景仍出错:100/M(10) → 100/(10)*(10) = 100,而不是 1 |
#define M(x) ((x)*(x)) |
((2+3)*(2+3)) |
25 ✅ | 各种场景下都正确 |
- 参数加括号:防止
2+3被乘法的优先级拆开(本题就是这一层出错)。 - 整体加括号:防止宏的结果被外层运算符抢走优先级(如上面
100/M(10)的例子)。 2 + 3 * 2 + 3的读法:*比+优先级高,所以先算3*2,再从左到右加:2+6+3 = 11。
易错:选 A)25 —— 那是把它当函数算的结果(材料上只给出 D 与展开式,其余选项未逐项解释)。
宏没有类型、没有优先级保护,参数和整体都要加括号,两层一层都不能少。
17 · 宏展开(第二道):展开后完全按运算符优先级走¶
以下宏的输出结果是?
- A. 14
- B. 11
- C. 18
- D. 7
答案
B(11)
解析
这是同一个考点的第二道,区别是出错的层次:第一道是参数代入后与 * 结合(SQUARE(2+3) → 2+3*2+3),第二道是宏的整体与外面的运算符结合:
宏是纯文本替换,展开后的表达式按 C 语言自己的运算符优先级走:* 比 + 高,所以先算 4 * 2 = 8,再算 3 + 8 = 11。✅
选项对照:
- A)14 —— 把宏当成函数算的结果:
(3+4)*2 = 14。这是最容易选错的选项。 - B)11 —— 正确答案:
3 + 4*2 = 11。 - C. 18、D)7 —— 其余干扰项(材料上只给出了 B 与展开式,未逐项解释)。
正确写法:整体加括号即可(本题参数本身就是单个数字,参数括号可省,但仍建议都写全):
写错时反而看不出问题
改对之后答案就从 11 变成 14 了——宏写错时结果可能看起来正常,不容易发现。
宏展开后没有额外保护,一切按优先级走,所以参数要加括号,整体更要加括号。
两题对比:
#define SQUARE(x) x*x—— 参数2+3直接代入(2+3*2+3);#define ADD(x,y) x+y—— 宏整体与外面的*2结合(3+4*2)。
一个是参数不加括号,一个是整体不加括号,分别对应上面三条注意事项的前两条。