跳转至

知识点 07 · 函数、递归与变量存储类别

本页最后更新于 ,内容可能过时

1 · 值传递与地址传递

在 C 语言中,以下关于函数参数传递的说法哪一个是正确的?

  • A. 值传递不会改变实参的值
  • B. 参数是结构体指针时,需要传递整个结构体的数据
  • C. 指针传递不能用于传递数组
  • D. 数组作为函数参数时,传递的是数组的副本
答案

A

解析

逐项排雷——

  • A 对:值传递是把实参的副本给形参,函数内怎么改都不影响实参。注意这句话成立的前提:改的是形参本身。传指针时"改指针指向的内容"照样能影响实参,那不叫"改实参的值"(见下表第 2 行)。
  • B 错:传结构体指针恰恰是为了避免拷贝整个结构体。
  • C 错:数组名退化为指针,指针传递正是传数组的标准方式。
  • D 错:数组作参数时传递的是首元素地址,形参和实参是同一块内存,函数内修改会直接影响原数组。这是 C 最反直觉的一点。

两张对照表:

传递方式 形参 能否改实参的值 能否改实参指向的数据
void f(int x) 副本
void f(int *p) 地址副本 ❌(改 p 无效) ✅(改 *p 有效)
void f(int a[]) 首地址

2 · 值传递与地址传递(改错)

/*  1  */void swap(short *x, short *y) {
/*  2  */    short *z;
/*  3  */    z = *x;
/*  4  */    *x = *y;
/*  5  */    *y = z;
/*  6  */}

/*  7  */int main() {
/*  8  */    short a=3, b=5;
/*  9  */    swap(a, b);
/* 10  */    printf("a=%hd, b=%hd\n", a, b);
/* 11  */}

要求运行结果为 a=5, b=3。请指出错误行并写出正确语句(不增删语句、不改程序结构)。

答案

共 2 处错误:

错误 改正 错因
2 short *z; short z; 暂存变量只需存放一个 short 值,不该声明成指针
9 swap(a, b); swap(&a, &b); 形参是 short *,必须传地址,否则类型不匹配
解析
  • 只要把第 2 行改成 short z;,第 3 行 z = *x;(取出 *x 的值存进 z)、第 5 行 *y = z;(把值写回 *y)就都对了——这是一次完整的"通过指针交换两个变量"。
  • 第 9 行是本课程最经典的错误:swap(a, b) 传的是值的副本,函数里怎么改都影响不到 main 里的 ab(形参 xy 是另外两个变量,函数一返回就消失)。要改实参,必须传地址,所以只能写 swap(&a, &b)
  • 另一条改法的坑(别走):如果把第 3 行改成 z = x;、第 5 行改成 *y = *z;(即把 z 当指针用),实测结果是错的:zx 指向同一处,*x = *y 一执行,*z 读到的就已经是新值了,*y = *z 退化成自赋值,真机跑出来是 a=5, b=5,不是题目要的 a=5, b=3。所以本题只有"改第 2 行一行"这一条路——改错题优先选改动最小的方案。
  • 只要第 2 行改对了,第 9 行就一定还得加上 &;两处错误的本质是同一个:把"值"和"地址"搞混了。

材料未附答案

材料里这一题没有附答案,下面的结果是推导(并真机核对)得出的,建议自己编译验证一遍。 材料上留存的是一份个人作答,其中也纠结于 z = x / x = y 之类,但没有写出完整正确的改法。


3 · 值传递与地址传递(改错)

/* 第 1 行 */ void func(int *a, int *b)
/* 第 2 行 */ {
/* 第 3 行 */     int *x;
/* 第 4 行 */     x = *b;
/* 第 5 行 */     *b = *a;
/* 第 6 行 */     *a = x;
/* 第 7 行 */ }

/* 第 8 行 */ int main()
/* 第 9 行 */ {
/* 第 10 行 */     int u=8, v=4;
/* 第 11 行 */     func(u, v);
/* 第 12 行 */     printf("u=%d, v=%d\n", &u, &v);
/* 第 13 行 */     return 0;
/* 第 14 行 */ }

要求运行结果为 u=4, v=8。请指出错误行并写出正确语句(不增删语句、不改程序结构)。

答案

共 3 处错误:

错误 改正 错因
3 int *x; int x; 暂存一个 int 值,不需要指针
11 func(u, v); func(&u, &v); 形参是 int *,必须传地址
12 printf("u=%d, v=%d\n", &u, &v); printf("u=%d, v=%d\n", u, v); %d 要的是 int 值,&u 是地址
解析
  • 改完第 3 行之后,第 4 行 x = *b;(把 *b 的值存进 x)、第 6 行 *a = x;(把 x 写回 *a)就都对了——这是一次完整的"通过指针交换两个变量"的实现。
  • 第 11 行和第 12 行的错因是同一个:把"值"和"地址"写反了。取地址运算符 & 的正确用法是"调用时给形参指针(func(&u, &v))",而不是"打印时给 %d(那是 u, v)"。两个方向的误用都是本课程的高频陷阱。
  • 本题的形参是 short *x, short *y,错误是"暂存变量写成了指针" + "调用时没传地址",另外多送了一个 printf 里误用 &。同一个点反复考,说明它是必考项。

材料未附答案

材料里这一题没有附答案,下面的结果是推导(并真机核对)得出的,建议自己编译验证一遍。


4 · 递归的执行顺序(回溯)

请写出以下程序的运行结果。

#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 += 4x = 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

材料未附答案

材料里这一题没有附答案,下面的结果是推导(并真机核对)得出的,建议自己编译验证一遍。


5 · 递归的执行顺序(回溯)

请写出以下程序的运行结果。

#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")——两种写法都要会,才能真正吃透"递归中语句位置决定执行顺序"这个考点。

材料未附答案

材料里这一题没有附答案,下面的结果是推导(并真机核对)得出的,建议自己编译验证一遍。 另外,材料上这一题记的是 *4,与按程序实际运行得到的结果不一致(gcc 15.2.0 实测输出 9),以实测结果为准。


6 · 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

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

解析

两个关键点——

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) 打印 3func(2) 打印 3func(3) 打印 33 3 3

func(2)

  • count 从 3 继续 → 4、5;
  • 回溯打印 5 5

合计输出 3 3 3 换行 5 5

易错:① 把 count 当成普通局部变量(每层递归一份)——static 变量全程只有一份;

② 以为打印是"递"的时候发生(那样会输出 1 2 3);

③ 以为第二次调用会重新从 1 开始(那样会输出 1 2)。

语句在递归调用前 = 递推时执行(正序);在递归调用后 = 回归时执行(逆序)。


7 · 递归出口与无限递归

/* 第 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; 声明了却没用——不算错误(编译只是警告),改错题不要改这种"无用但合法"的语句,会超出题目的错误数量限制。

材料未附答案

材料里这一题没有附答案,下面的结果是推导(并真机核对)得出的,建议自己编译验证一遍。


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. "保留上次退出时的值" → 需要生命周期贯穿整个程序 → 排除 autoregister
  2. "不希望其他函数访问" → 需要作用域限于本函数 → 排除 externextern 恰恰是声明"别处定义的全局变量")。

四个存储类别的速查:

存储类别 存放位置 生命周期 作用域 初值
auto(默认) 函数执行期间 函数内 不确定(垃圾值)
register 寄存器(建议) 函数执行期间 函数内 不确定
static 局部 静态区 整个程序 函数内 自动为 0
extern / 全局 静态区 整个程序 全程序 自动为 0

易错:把"不被其他函数访问"理解成 externextern 是专门用来"跨文件访问"的,方向正好相反。


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. afunc 中无效
答案

C

解析

a 写在所有函数之外,是全局变量,从定义处起在本文件内处处可见,各函数内部都能直接引用——funcprintf("%d", a) 完全合法,所以选 C。

B 和 D 都把"作用域在函数外"理解成了"函数内用不了",方向正好说反;对照 func 里的 b,那才是只在函数内有效的局部变量。

严格说跨文件访问还需 extern 声明,考试按"整个程序有效"记即可。


12 · 存储类别

未指定存储类别的变量,其隐含的存储类别为( )。

  • A. auto
  • B. static
  • C. extern
  • D. register
答案

A(auto

解析

C 规定:函数内部的局部变量不写存储类别时,隐含就是 auto——存放在栈上,函数返回即销毁,初值不确定。

staticexternregister 三者的语义都必须显式写出才会生效,标准不会自动给你。

注意这条"默认 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 zz++ 前 → 后) 返回 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 次进 funz 已经是 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
  • a3a4 自始至终没变(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——注意它没有一条语句是通过指针写回实参的,xy 全是形参;

② 以为第二次调用传的是 a1 的初值 300,答成 300,400,100,200

③ 没看出 a1 是全局变量,以为函数里的 a1 = x 是在改形参。

顺带一提:题干里的 main() 没写返回类型,现在应写 int main(void)


15 · 递归辗转相减求最大公约数

写出以下程序的运行结果。

#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 一步就把大数砍到位)。

注意排版:题干里 else 后面的 return gcd(m, n - m); 被挤到了函数的 } 之外(页边排版噪声),已按 C 语法还原到 else 分支下。

顺带一提:题干里的 main() 没写返回类型,现在应写 int main(void)


16 · 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(打印时) b1++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:mainfor (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 才跑通)。