四、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语言

相关推荐
ZHOUPUYU6 小时前
PHP 8.3网关优化:我用JIT将QPS提升300%的真实踩坑录
开发语言·php
寻寻觅觅☆10 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio11 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t11 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
赶路人儿11 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12312 小时前
C++使用format
开发语言·c++·算法
码说AI12 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS12 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子13 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言
老约家的可汗13 小时前
初识C++
开发语言·c++