跳转至

第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;
}
答案
1,2,3,4,13,14,15,16,
解析

第一步:确定数据。 形参 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 确实是最大公约数。

易错:

  1. 把两个分支的条件记反(必须在 m > n 时才做 gcd(m-n, n),否则会减出负数或 0)
  2. 漏掉 m == n 这个出口,以为会无限递归
  3. 数错层数——本题只递归两层就到达出口,不需要减很多次(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;
}
答案
3 3 3
5 5

(每行最后一个数字后面还有一个空格。)

解析

两个关键点:

  1. static 局部变量只初始化一次,且生命周期贯穿整个程序。 所以第二次调用 func(2) 时 count 从 3 接着数,而不是从 0 重来。
  2. 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。

易错:

  1. 把 count 当成普通局部变量(每层递归一份)——static 变量全程只有一份
  2. 以为打印是"递"的时候发生(那样会输出 1 2 3)
  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 局部变量的两个特性:

  1. "保留上次退出时的值" → 需要生命周期贯穿整个程序 → 排除 auto、register;
  2. "不希望其他函数访问" → 需要作用域限于本函数 → 排除 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 具有什么作用域?

int a = 10;
void func() {
    int b = 20;
    printf("%d", 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++));
}
答案
□□□□7□□□□9

即 7 9:每个数前面 4 个空格,两数之间没有别的字符,程序没有输出换行(整行不换行)。

解析

%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。

易错:

  1. 把 z 当普通局部变量,算成 7 7
  2. 以为 z++ 每次都从 3 开始
  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。

易错:

  1. 以为 sub1 是交换函数,答成 100,400,200,100——注意它没有一条语句是通过指针写回实参的,x、y 全是形参
  2. 以为第二次调用传的是 a1 的初值 300,答成 300,400,100,200
  3. 没看出 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;
}
答案
10,2,1
20,2,2
30,2,3
解析

本题考的就是同一个函数里 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(程序结束,不再使用)

三个要点:

  1. a 每次 +10:打印的是加 10 之前的值(a += 10 写在 printf 后面),所以是 10、20、30,不是 20、30、40。
  2. b 恒为 2:int b = 1; 每进一次函数都重新执行,b++ 后永远是 2——自动变量不跨调用保留。
  3. j 就是 1、2、3:main 里 for (i = 1; i <= 3; i++) f(i); 依次调用 f(1)、f(2)、f(3)。

易错:

  1. 把 a 也当成每次重置,答成三行都是 10,2,?
  2. 心算时把 a += 10 提到 printf 前面,答成 20,2,1 / 30,2,2 / 40,2,3
  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 就是"头文件卫士"的写法)

宏是文本替换,不是函数调用。宏的参数在展开时直接原样代入,不会自动加括号。

宏展开的三条注意事项

  1. 每个参数都加括号:((x) * (x)) 而不是 x * x;
  2. 整个表达式再加一层括号:(x + 1) 而不是 x + 1;
  3. 能不用宏就不用,优先 static inline 函数或 const 常量。

原因:宏没有类型检查;参数会被求值多次(SQUARE(i++) 会让 i 自增两次);

而 static inline 函数有类型检查、参数只求值一次,且同样没有函数调用开销。


16 · 宏展开:函数式宏必须加括号

以下程序的运行结果是?

#define SQUARE(x) x * x

int result = SQUARE(2 + 3);
printf("%d\n", result);
  • A. 25
  • B. 13
  • C. 9
  • D. 11
答案

D(11)

解析

宏展开是纯文本替换:

SQUARE(2 + 3)  →  2 + 3 * 2 + 3

按优先级算:2 + (3*2) + 3 = 2 + 6 + 3 = 11。✅(如果它是函数,答案才会是 25。)

正确写法必须给参数和整体都加括号:

#define SQUARE(x) ((x) * (x))     // → (2+3) * (2+3) = 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 · 宏展开(第二道):展开后完全按运算符优先级走

以下宏的输出结果是?

#define ADD(x, y) x + y

printf("%d\n", ADD(3, 4) * 2);
  • A. 14
  • B. 11
  • C. 18
  • D. 7
答案

B(11)

解析

这是同一个考点的第二道,区别是出错的层次:第一道是参数代入后与 * 结合(SQUARE(2+3) → 2+3*2+3),第二道是宏的整体与外面的运算符结合:

ADD(3, 4) * 2  →  3 + 4 * 2

宏是纯文本替换,展开后的表达式按 C 语言自己的运算符优先级走:* 比 + 高,所以先算 4 * 2 = 8,再算 3 + 8 = 11。✅

选项对照:

  • A)14 —— 把宏当成函数算的结果:(3+4)*2 = 14。这是最容易选错的选项。
  • B)11 —— 正确答案:3 + 4*2 = 11。
  • C. 18、D)7 —— 其余干扰项(材料上只给出了 B 与展开式,未逐项解释)。

正确写法:整体加括号即可(本题参数本身就是单个数字,参数括号可省,但仍建议都写全):

#define ADD(x, y) ((x) + (y))     // → ((3) + (4)) * 2 = 14

写错时反而看不出问题

改对之后答案就从 11 变成 14 了——宏写错时结果可能看起来正常,不容易发现。

宏展开后没有额外保护,一切按优先级走,所以参数要加括号,整体更要加括号。

两题对比:

  • #define SQUARE(x) x*x —— 参数 2+3 直接代入(2+3*2+3);
  • #define ADD(x,y) x+y —— 宏整体与外面的 *2 结合(3+4*2)。

一个是参数不加括号,一个是整体不加括号,分别对应上面三条注意事项的前两条。