第5讲:函数——把代码拆成可复用的小工具


C语言主理人:老王爱玩车
C语言主理人:老王爱玩车
适合人群:已经学过if、switch、while、for,能写简单 C 程序
学习方式:先理解“为什么需要函数”,再写“怎么定义和调用”,最后看“内存里到底发生了什么”
目录
- 函数为什么存在
- 库函数:别急着造轮子
- 自定义函数:把一段逻辑封装起来
- 形参和实参:值传递到底传了什么
return:函数的出口- 数组做函数参数:为什么传数组还要传长度
- 嵌套调用与链式访问
- 声明和定义:先打招呼,后实现
static和extern:链接属性初体验- 课堂练习
- 本讲小结
学习目标
学完本讲,你应该能够:
- 说清楚函数在 C 程序中的角色:任务拆分、代码复用、逻辑清晰;
- 会使用标准库函数,并知道如何查文档、包含头文件;
- 能定义、声明、调用自定义函数;
- 区分形参和实参,理解 C 语言的值传递;
- 掌握
return的常见用法和注意事项; - 会写“数组做参数”的函数,并理解数组传参退化为指针;
- 理解嵌套调用和链式访问;
- 了解多文件编程,初步认识
static和extern; - 完成函数相关的常见练习:判断、统计、交换、递归、数组处理等。
1. 函数为什么存在
假设你要写一个程序,判断 10 个数是不是素数。如果不用函数,你可能会把判断素数的逻辑复制 10 遍:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
int x = 0;
scanf("%d", &x);
int flag = 1;
if (x < 2) flag = 0;
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) {
flag = 0;
break;
}
}
if (flag) printf("是素数\n");
else printf("不是素数\n");
// 如果还要判断第二个、第三个数,就要把上面代码再抄一遍……
return 0;
}
这样写有两个问题:
- 重复代码太多,一旦逻辑要改,每一处都要改;
- 主函数太长,阅读的人很难一眼看出程序结构。
函数就是解决这类问题的。它把一段完成特定任务的代码封装起来,起一个名字,需要时调用即可。C 程序本质上就是由许多函数组合而成:main 是入口,其他函数负责各自的小任务。
函数就是一套已经提前设计好的机器或重复性的步骤,需要用的时候直接把原料(参数)投放给他,他就会按照预设好的步骤进行输出。
2. 库函数:别急着造轮子
C 语言标准库提供了大量函数,比如输入输出、字符串处理、数学计算、内存管理。它们已经在标准库中实现好了,我们只要包含对应头文件,就能直接使用。
2.1 库函数怎么查
常用查询方式:
-
cppreference:cppreference库函数学习网站
-
-
cplusplus:cplusplus库函数查询学习网站

查函数时重点看:
- 函数原型;
- 功能说明;
- 参数类型和返回类型;
- 需要包含哪个头文件;
- 示例代码。


利用好这些资源可以起到事半功倍的效果。
2.2 示例:计算两点距离
sqrt 用于开平方,pow 用于求幂,它们都在 <math.h> 中声明。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <math.h>
int main(void) {
double x1 = 1.0, y1 = 2.0;
double x2 = 4.0, y2 = 6.0;
double dx = x2 - x1;
double dy = y2 - y1;
double dist = sqrt(pow(dx, 2) + pow(dy, 2));
printf("两点距离 = %.3f\n", dist);
return 0;
}
运行结果:
两点距离 = 5.000

注意:在 Linux/macOS 下用 gcc 编译时,可能需要加 -lm 链接数学库:
后期在学习Linux时会详细介绍,现在只作简单了解即可。
gcc test.c -o test -lm
在初学阶段可以试着复现一下库函数,增强理解,库函数没有必要死记硬背,平时可以浏览相关内容,用的多了记忆自然就深刻了。
2.3 库函数文档的一般格式
看文档时,通常会看到:
- 函数原型;
- 函数功能;
- 参数说明;
- 返回值说明;
- 代码示例;
- 相关函数链接。
养成查文档的习惯,比死记硬背更有效。
3. 自定义函数:把一段逻辑封装起来
库函数虽然好用,但真正体现程序设计能力的,是自定义函数。
顾名思义,自定义函数就是我们自己设计并封装的函数,能更加贴切我们的实际需求。
3.1 函数的基本语法
返回类型 函数名(形式参数)
{
函数体;
return 返回值;
}
例如:
int IsPrime(int n)
{
if (n < 2) return 0;
for (int i = 2; i <= n / i; i++) {
if (n % i == 0) return 0;
}
return 1;
}
int:返回类型,表示函数返回一个整数;IsPrime:函数名,见名知意;(int n):形式参数,表示调用时需要传一个整数;{ ... }:函数体,完成判断素数的逻辑;return 0/1:返回结果,0 表示不是素数,1 表示是素数。
3.2 完整示例:判断素数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int IsPrime(int n)
{
if (n < 2) return 0;
for (int i = 2; i <= n / i; i++) {
if (n % i == 0) return 0;
}
return 1;
}
int main(void)
{
int x = 0;
scanf("%d", &x);
if (IsPrime(x))
printf("%d 是素数\n", x);
else
printf("%d 不是素数\n", x);
return 0;
}

函数名、参数、返回类型都可以根据实际需要灵活设计。好的函数名应该能表达功能,比如 IsPrime、GetMax、PrintArray。
4. 形参和实参:值传递到底传了什么
函数定义时括号里的参数叫形参(形式参数),调用时传进去的参数叫实参(实际参数)。
int Add(int x, int y) // x、y 是形参
{
return x + y;
}
int main(void)
{
int a = 3, b = 5;
int c = Add(a, b); // a、b 是实参
return 0;
}
C 语言中,实参传递给形参是值传递:形参是实参的一份临时拷贝。形参和实参各有独立的内存空间。
4.1 经典例子:交换函数为什么失败
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void Swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
int main(void)
{
int a = 10, b = 20;
printf("调用前:a=%d, b=%d\n", a, b);
Swap(a, b);
printf("调用后:a=%d, b=%d\n", a, b);
return 0;
}
运行结果:
调用前:a=10, b=20
调用后:a=10, b=20

为什么没交换?因为 Swap 里的 x、y 只是 a、b 的拷贝。改变 x、y 不会影响 a、b。
如果想真正交换,需要传地址,使用指针。这部分会在指针章节深入讲解。现在只需要记住:形参是实参的临时拷贝,形参改变不影响实参。
5. return:函数的出口
return 用于从函数返回,并可以带回一个值。常见注意事项:
return后面可以是一个值,也可以是一个表达式;return;适合返回类型为void的函数;return执行后,函数立即结束,后面代码不再执行;- 返回值类型和函数返回类型不一致时,会隐式转换;
- 如果函数有分支,要保证每条路径都有返回值;
- 函数返回类型不写,旧标准默认是
int,但现代 C 不推荐; - 写了返回类型却没有
return,返回值是未知的。
8.多条分支语句需要返回值时,要保证每一条都有对应的’return’语句,否则可能会造成逻辑错误。
5.1 示例:求两个数的最大值
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Max(int a, int b)
{
if (a > b)
return a;
else
return b;
}
int main(void)
{
int x = 0, y = 0;
scanf("%d %d", &x, &y);
int m = Max(x, y);
printf("最大值 = %d\n", m);
return 0;
}

注意:if 和 else 都有 return,保证所有分支都有返回值。
在判断两个数的最大值,有时使用三木操作符可能会更简洁。
6. 数组做函数参数:为什么传数组还要传长度
数组经常需要交给函数处理。比如:把数组每个元素翻倍,再打印出来。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void DoubleArray(int arr[], int n)
{
for (int i = 0; i < n; i++) {
arr[i] *= 2;
}
}
void PrintArray(int arr[], int n)
{
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(void)
{
int a[] = {1, 2, 3, 4, 5};
int n = sizeof(a) / sizeof(a[0]);
DoubleArray(a, n);
PrintArray(a, n);
return 0;
}
运行结果:
2 4 6 8 10

重点知识:
- 数组传参时,形参可以写成
int arr[],也可以写成int *arr(数组名救赎数组首元素的地址); - 形参不会创建新数组,它和实参操作的是同一块内存;
- 数组传参后,函数内部无法用
sizeof(arr) / sizeof(arr[0])求元素个数,因为arr已经退化为指针; - 所以必须额外传一个参数
n,表示数组长度; - 二维数组传参时,行可以省略,列不能省略,例如
void Print(int arr[][4], int row)。
7. 嵌套调用与链式访问
7.1 嵌套调用
函数之间可以互相调用。比如先写一个 Max,再写一个 Max3 求三个数的最大值。
函数支持嵌套调用,但不能嵌套定义。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Max(int a, int b)
{
return a > b ? a : b;
}
int Max3(int a, int b, int c)
{
return Max(Max(a, b), c);
}
int main(void)
{
int x = 3, y = 9, z = 6;
printf("最大值 = %d\n", Max3(x, y, z));
return 0;
}

main 调用 Max3,Max3 又调用 Max,这就是嵌套调用。
注意:函数可以嵌套调用,但不能嵌套定义。也就是说,不能在一个函数里面再定义一个函数。
7.2 链式访问
把一个函数的返回值直接作为另一个函数的参数,像链条一样串起来,叫链式访问。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Square(int x)
{
return x * x;
}
int Max(int a, int b)
{
return a > b ? a : b;
}
int main(void)
{
printf("最大值的平方 = %d\n", Square(Max(3, 7)));
return 0;
}

Max(3, 7) 返回 7,Square(7) 返回 49,最后 printf 打印 49。
再比如 printf 本身也有返回值,它返回成功打印的字符个数。下面这段代码会打印 4321:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}

原因:
- 最内层
printf("%d", 43)打印43,返回 2; - 中间层打印
2,返回 1; - 最外层打印
1。
核心问题就是printf()返回值的问题。
所以屏幕输出:4321。
8. 声明和定义:先打招呼,后实现
8.1 单文件:先声明,后使用
如果函数定义在调用之后,编译器在调用处还没见过这个函数,就可能报警告。解决办法是:在调用前先声明。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int IsPrime(int n); // 函数声明
int main(void)
{
int x = 0;
scanf("%d", &x);
if (IsPrime(x))
printf("是素数\n");
else
printf("不是素数\n");
return 0;
}
int IsPrime(int n) // 函数定义
{
if (n < 2) return 0;
for (int i = 2; i <= n / i; i++) {
if (n % i == 0) return 0;
}
return 1;
}

函数声明只要交代:函数名、返回类型、参数类型。参数名可以省略,例如:
int IsPrime(int);
8.2 多文件:头文件放声明,源文件放定义
企业开发中,代码通常分文件管理。例如:
area.h
#ifndef AREA_H
#define AREA_H
double RectArea(double w, double h);
#endif
area.c
#include "area.h"
double RectArea(double w, double h)
{
return w * h;
}
main.c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "area.h"
int main(void)
{
double w = 3.0, h = 4.0;
printf("面积 = %.2f\n", RectArea(w, h));
return 0;
}
这样结构清晰,便于多人协作和代码复用。
9. static 和 extern:链接属性初识
在讲 static 和 extern 之前,先复习两个概念:
- 作用域:名字能起作用的范围;
- 生命周期:变量从创建到销毁的时间段。
局部变量作用域是所在局部范围,生命周期是进入作用域到出作用域。
全局变量作用域是整个工程,生命周期是整个程序。
9.1 static 修饰局部变量
普通局部变量每次进入函数都会重新创建,出函数就销毁。被 static 修饰后,它会存到静态区,生命周期变成整个程序。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void Counter(void)
{
static int cnt = 0;
cnt++;
printf("cnt = %d\n", cnt);
}
int main(void)
{
Counter();
Counter();
Counter();
return 0;
}
运行结果:
cnt = 1
cnt = 2
cnt = 3

cnt 只初始化一次,之后一直保留上次的值。但它的作用域仍然只在 Counter 函数内部。
9.2 static 修饰全局变量和函数
全局变量和函数默认具有外部链接属性,其他源文件可以通过 extern 声明后使用。
如果加上 static,它们就变成内部链接属性,只能在本源文件内使用。
例如:
file1.c
static int secret = 100;
file2.c
extern int secret; // 链接时会出错,因为 secret 是 file1.c 内部私有的
函数同理:
static int Helper(int x)
{
return x * 2;
}
这个 Helper 只能在本文件内使用。
使用建议:
- 只想在本文件内使用的全局变量或函数,加
static; - 需要跨文件使用的,在头文件中声明,在源文件中定义。
9.3 extern
extern 用于声明外部符号。比如一个全局变量在 A 文件定义,在 B 文件想用,就可以在 B 文件写:
extern int g_val;
然后就可以使用 g_val。
10. 课堂练习
练习 1:写函数判断一个数是不是素数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int IsPrime(int n)
{
if (n < 2) return 0;
for (int i = 2; i <= n / i; i++) {
if (n % i == 0) return 0;
}
return 1;
}
int main(void)
{
int x = 0;
scanf("%d", &x);
if (IsPrime(x))
printf("%d 是素数\n", x);
else
printf("%d 不是素数\n", x);
return 0;
}
练习 2:写函数求数组所有元素的和
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int SumArray(int arr[], int n)
{
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
int main(void)
{
int a[] = {10, 20, 30, 40, 50};
int n = sizeof(a) / sizeof(a[0]);
printf("数组和 = %d\n", SumArray(a, n));
return 0;
}
练习 3:写函数逆置数组
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void ReverseArray(int arr[], int n)
{
int left = 0;
int right = n - 1;
while (left < right) {
int t = arr[left];
arr[left] = arr[right];
arr[right] = t;
left++;
right--;
}
}
void PrintArray(int arr[], int n)
{
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(void)
{
int a[] = {1, 2, 3, 4, 5, 6};
int n = sizeof(a) / sizeof(a[0]);
ReverseArray(a, n);
PrintArray(a, n);
return 0;
}
练习 4:递归求阶乘
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Fact(int n)
{
if (n <= 1) return 1;
return n * Fact(n - 1);
}
int main(void)
{
int n = 0;
scanf("%d", &n);
if (n < 0)
printf("负数没有阶乘\n");
else
printf("%d! = %d\n", n, Fact(n));
return 0;
}
练习 5:模拟 strlen 求字符串长度
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int MyStrlen(const char* str)
{
int count = 0;
while (str[count] != '\0') {
count++;
}
return count;
}
int main(void)
{
char s[] = "hello world";
printf("长度 = %d\n", MyStrlen(s));
return 0;
}
11. 本讲小结
这一讲我们围绕函数展开了完整学习:
- 函数用于拆分任务、复用代码、让程序结构更清晰;
- 库函数要包含对应头文件,并学会查文档;
- 自定义函数由返回类型、函数名、形参列表和函数体组成;
- 形参是实参的临时拷贝,C 语言是值传递;
return用于返回结果并结束函数,注意每条分支都要有返回值;- 数组传参时,形参不会创建新数组,操作的是同一块内存,因此通常还要传数组长度;
- 函数可以嵌套调用,返回值可以作为另一个函数的参数,形成链式访问;
- 函数使用前要声明,定义可以放在后面;多文件编程时,声明放头文件,定义放源文件;
static可以改变局部变量生命周期,也可以限制全局变量和函数只能在本文件使用;extern用于声明外部符号。
函数是 C 语言模块化编程的起点。下一讲,我们会把函数和数组结合起来,处理更复杂的任务,比如排序、查找、矩阵运算。建议你把上面的代码逐个敲一遍,再尝试修改参数、改变返回值、增加功能,真正把函数用起来。

各位大佬留个关注支持一下吧

242

被折叠的 条评论
为什么被折叠?



