四、C语言函数

思维导图


一、 函数的基本结构

1.1 从重复劳动中解脱

如果你发现自己在复制粘贴代码,停下来,把它封装成函数。

语法结构:

c 复制代码
返回值类型 函数名(参数列表) {
    // 函数体
    return 结果;
}

代码对比:

写法 A(混乱):

c 复制代码
int main() {
    // 计算 3 + 5
    int a = 3, b = 5;
    int sum1 = a + b;
    printf("%d\n", sum1);

    // 计算 10 + 20
    int c = 10, d = 20;
    int sum2 = c + d; // 如果逻辑复杂,这里要复制好多行
    printf("%d\n", sum2);
    return 0;
}

写法 B(封装函数,清爽):

c 复制代码
// 定义一个加法工具
int add(int x, int y) {
    return x + y;
}

int main() {
    printf("%d\n", add(3, 5));
    printf("%d\n", add(10, 20)); // 想用几次用几次
    return 0;
}

1.2 为什么需要声明?

编译器是从上往下读代码的。如果函数 foo() 的定义写在 main() 后面,而 main() 中调用了 foo(),编译器会困惑:"这是个啥?"

错误示范:

c 复制代码
int main() {
    foo(); // 编译警告:implicit declaration of function 'foo'
}

void foo() {
    printf("Hello");
}

正确示范(使用原型):

c 复制代码
#include <stdio.h>

// 1. 函数声明(原型):告诉编译器 foo 是个啥
void foo(void); 

int main() {
    foo(); // 2. 函数调用:编译器现在认识 foo 了
    return 0;
}

// 3. 函数定义:具体的实现
void foo(void) {
    printf("Hello\n");
}

二、 形参、实参与值传递

2.1 核心机制:C 语言只有值传递

形参: 函数定义时的占位符变量(如 x, y)。
实参: 调用函数时传入的具体数值(如 10, 20)。

真理:

调用函数时,计算机会把实参的值拷贝 一份给形参。形参只是实参的副本在函数内部修改形参,永远不会影响外面的实参。

2.2 经典案例:Swap (交换) 为什么失败?

这是理解指针前最重要的一课。

错误示范(值传递):

c 复制代码
void swap_fail(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
    printf("[函数内] x=%d, y=%d (换成功了)\n", x, y);
}

int main() {
    int a = 10, b = 20;
    swap_fail(a, b);
    printf("[函数外] a=%d, b=%d (根本没变!)\n", a, b); 
}

解析: swap_fail 里的 xy 只是 ab 的复印件。你在复印件上涂改,原件怎么会变呢?

正确示范(模拟指针传递):
注:这里先展示代码,具体原理将在指针章节详解。关键在于传递的是变量的地址 &a

c 复制代码
void swap_success(int *x, int *y) {
    int temp = *x; // 去地址里取值
    *x = *y;       // 修改地址里的值
    *y = temp;
}
// 调用:swap_success(&a, &b);

三、 返回值设计

3.1 return 的语义

return 有两个作用:

1.带回结果 :给调用者一个交代。
2.立即终止 :函数执行到 return 会瞬间结束,后面的代码全都不看。

代码示例(利用 return 简化逻辑):

c 复制代码
// 判断是否为偶数
int is_even(int n) {
    if (n % 2 == 0) {
        return 1; // 是偶数,直接返回,函数结束
    }
    // 根本不需要 else,因为如果上面执行了,这里根本不会走到
    return 0; 
}

3.2 如何返回多个值?

C 语言的 return 只能返回一个值。如果任务需要返回状态码和计算结果怎么办?

方案 A:使用指针参数(推荐)

这是一种非常工程化的写法:函数返回值只代表成功/失败,真正的数据通过指针参数带出来。

c 复制代码
// 返回值 int 代表状态:1 成功,0 失败
// 真正的计算结果通过指针 result 传出
int safe_divide(int a, int b, double *result) {
    if (b == 0) {
        return 0; // 失败:除数为0
    }
    *result = (double)a / b; // 修改外部变量
    return 1; // 成功
}

int main() {
    double res;
    // 这种写法在 C 语言源码中随处可见
    if (safe_divide(10, 2, &res)) {
        printf("计算成功,结果: %.2f\n", res);
    } else {
        printf("错误: 除数不能为0\n");
    }
}

方案 B:返回结构体

c 复制代码
typedef struct {
    int status; // 0 失败, 1 成功
    double value;
} Result;

Result divide(int a, int b) {
    Result r = {0, 0.0};
    if (b != 0) {
        r.status = 1;
        r.value = (double)a / b;
    }
    return r;
}

四、 作用域与静态变量

4.1 局部 vs 全局

代码示例:

c 复制代码
int g_num = 100; // 全局变量:所有人都能改,危险!

void func() {
    int x = 10; // 局部变量:只有 func 能看见
    printf("%d", x);
}

int main() {
    // printf("%d", x); // 报错!main 看不见 func 里的 x
    printf("%d", g_num); // 可以访问全局变量
}

4.2 static 局部变量

这是一个神奇的关键字。它可以让局部变量拥有记忆力

普通局部变量(健忘):

c 复制代码
void normal_func() {
    int i = 0; // 每次调用都会重新初始化为 0
    i++;
    printf("%d ", i);
}
// main 中调用 3 次,输出:1 1 1

Static 局部变量(过目不忘):

c 复制代码
void static_func() {
    static int i = 0; // 只在程序启动时初始化一次!
    i++;
    printf("%d ", i);
}
// main 中调用 3 次,输出:1 2 3

应用场景: 函数调用计数器、生成唯一 ID。

五、 递归 (Recursion)

5.1 递归的本质

函数自己调用自己。
核心口诀: 一定要有一个退出的条件(基准情况),否则就是死循环(栈溢出)。

代码示例:阶乘 (Factorial)

计算 n! = n * (n-1) * ... * 1

c 复制代码
int factorial(int n) {
    // 1. 基准情况 (Base Case):什么时候停?
    if (n <= 1) return 1;
    
    // 2. 递归步骤 (Recursive Step):把问题变小
    return n * factorial(n - 1);
}

5.2 性能陷阱:斐波那契数列

递归版(反面教材):

c 复制代码
int fib(int n) {
    if (n <= 2) return 1;
    return fib(n-1) + fib(n-2); // 极其低效!重复计算了无数次
}

解释: 计算 fib(50) 可能需要几分钟甚至几小时。

循环版(正面教材):

c 复制代码
int fib_loop(int n) {
    if (n <= 2) return 1;
    int a = 1, b = 1, temp;
    for (int i = 3; i <= n; i++) {
        temp = a + b;
        a = b;
        b = temp;
    }
    return b; // 瞬间完成
}

六、 练习题

题目 1: 如果一个函数不需要参数,也不返回任何值,它的原型应该怎么写?

题目 2: 下面代码中 change 函数执行后,main 中的 val 是多少?

c 复制代码
void change(int val) { 
    val = 100; 
}
int main() { 
    int val = 5; 
    change(val); 
    printf("%d", val); 
}

题目 3: 想要在函数中修改 main 函数里的 int 变量,参数类型应该是什么?

题目 4: 下列关于 return 的说法错误的是?

A. 函数可以有多个 return 语句。

B. void 函数不能有 return 语句。

C. return 会立即终止函数。

题目 5: 下面这个递归函数有什么问题?

c 复制代码
void dead_loop(int n) {
    printf("%d ", n);
    dead_loop(n - 1);
}

题目 6: 局部变量如果不初始化,它的默认值是多少?

题目 7: 全局变量如果不初始化,它的默认值是多少?

题目 8: static 局部变量如果不初始化,它的默认值是多少?

题目 9: 下面代码会发生什么严重问题?

c 复制代码
int* get_addr() {
    int x = 10;
    return &x; // 返回局部变量的地址
}

题目 10: 编写一个函数 max3,接收三个整数,返回其中的最大值。

题目 11: 计算 1+2+...+n。写出递归版的关键逻辑 sum(n)

题目 12: C 语言支持函数重载(同名不同参,如 add(int)add(float))吗?

题目 13: 将数组传递给函数时,实际上传递的是什么?

题目 14: 函数 int func(void)int func() 在 C 语言(C99前)中有区别吗?

题目 15: 编写一个函数 is_prime(int n),判断 n 是否为素数,返回 1 或 0。

七、 解析

题 1 解析
答案: void func_name(void);
详解:

void 在返回值位置表示"无返回值",在参数位置表示"不接受任何参数"。

题 2 解析
答案: 5。
详解:

值传递change 函数修改的只是 val 的副本(复印件),不影响外面的 val(原件)。

题 3 解析
答案: int* (整型指针)。
详解:

只有拿到地址,才能通过解引用修改原来的内存。

题 4 解析
答案: B。
详解:

void 函数可以使用 return;(不带值)来提前结束函数执行,这是合法的。

题 5 解析
答案: 缺少终止条件(Base Case)。
详解:

这会导致无限递归,最终耗尽栈内存,引发 Stack Overflow(栈溢出)崩溃。

题 6 解析
答案: 垃圾值 (Undefined)。
详解:

栈上的局部变量内存如果不手动清理,里面存的就是上次遗留的数据,可能是任何值。

题 7 解析
答案: 0。
详解:

全局变量存储在静态数据区(BSS段),编译器保证它们默认初始化为 0。

题 8 解析
答案: 0。
详解:

同上,static 变量具备静态存储期,也会自动初始化为 0。

题 9 解析
答案: 悬空指针 (Dangling Pointer)。
详解:

致命错误! 函数结束后 x 所在的栈内存会被回收。返回的指针指向了一个"已经销毁"的区域,访问它会导致未定义行为。

题 10 解析
答案:

c 复制代码
int max3(int a, int b, int c) {
    int max = a;
    if (b > max) max = b;
    if (c > max) max = c;
    return max;
}

题 11 解析
答案: return n + sum(n-1);
详解:

还要加上基准条件:if (n==1) return 1;

题 12 解析
答案: 不支持。
详解:

C 语言中函数名是唯一的标识符,不能重名。这是 C++ 的特性。

题 13 解析
答案: 数组首元素的地址。
详解:

数组作为参数时会退化为指针。传递数组并没有拷贝整个数组的数据,而是传递了地址(值传递的一种特殊情况,传递的是地址的值)。

题 14 解析
答案: 有区别。
详解:

int func(void) 明确表示不接受任何参数
int func() 在旧标准中表示参数个数不确定(为了兼容老代码,但不推荐使用)。

题 15 解析
答案:

c 复制代码
int is_prime(int n) {
    if (n <= 1) return 0;
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) return 0; // 发现因子,不是素数
    }
    return 1; // 是素数
}

日期:2025年2月10日

专栏:C语言

相关推荐
Dovis(誓平步青云)30 分钟前
《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》
c语言·开发语言·汇编·qt
isyangli_blog9 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008119 小时前
FastAPI APIRouter
开发语言·python
Benszen9 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆9 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木9 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充10 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~10 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball61610 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
春生野草10 小时前
反射、Tomcat执行
java·开发语言