第5讲:函数

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

在这里插入图片描述

在这里插入图片描述
C语言主理人:老王爱玩车

C语言主理人:老王爱玩车
适合人群:已经学过 ifswitchwhilefor,能写简单 C 程序
学习方式:先理解“为什么需要函数”,再写“怎么定义和调用”,最后看“内存里到底发生了什么”


目录

  1. 函数为什么存在
  2. 库函数:别急着造轮子
  3. 自定义函数:把一段逻辑封装起来
  4. 形参和实参:值传递到底传了什么
  5. return:函数的出口
  6. 数组做函数参数:为什么传数组还要传长度
  7. 嵌套调用与链式访问
  8. 声明和定义:先打招呼,后实现
  9. staticextern:链接属性初体验
  10. 课堂练习
  11. 本讲小结

学习目标

学完本讲,你应该能够:

  • 说清楚函数在 C 程序中的角色:任务拆分、代码复用、逻辑清晰;
  • 会使用标准库函数,并知道如何查文档、包含头文件;
  • 能定义、声明、调用自定义函数;
  • 区分形参和实参,理解 C 语言的值传递;
  • 掌握 return 的常见用法和注意事项;
  • 会写“数组做参数”的函数,并理解数组传参退化为指针;
  • 理解嵌套调用和链式访问;
  • 了解多文件编程,初步认识 staticextern
  • 完成函数相关的常见练习:判断、统计、交换、递归、数组处理等。

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;
}

这样写有两个问题:

  1. 重复代码太多,一旦逻辑要改,每一处都要改;
  2. 主函数太长,阅读的人很难一眼看出程序结构。

函数就是解决这类问题的。它把一段完成特定任务的代码封装起来,起一个名字,需要时调用即可。C 程序本质上就是由许多函数组合而成:main 是入口,其他函数负责各自的小任务。


函数就是一套已经提前设计好的机器或重复性的步骤,需要用的时候直接把原料(参数)投放给他,他就会按照预设好的步骤进行输出。


2. 库函数:别急着造轮子

C 语言标准库提供了大量函数,比如输入输出、字符串处理、数学计算、内存管理。它们已经在标准库中实现好了,我们只要包含对应头文件,就能直接使用。

2.1 库函数怎么查

常用查询方式:

在这里插入图片描述

查函数时重点看:

  1. 函数原型;
  2. 功能说明;
  3. 参数类型和返回类型;
  4. 需要包含哪个头文件;
  5. 示例代码。

在这里插入图片描述

在这里插入图片描述
利用好这些资源可以起到事半功倍的效果。

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;
}

在这里插入图片描述

函数名、参数、返回类型都可以根据实际需要灵活设计。好的函数名应该能表达功能,比如 IsPrimeGetMaxPrintArray


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 里的 xy 只是 ab 的拷贝。改变 xy 不会影响 ab

如果想真正交换,需要传地址,使用指针。这部分会在指针章节深入讲解。现在只需要记住:形参是实参的临时拷贝,形参改变不影响实参


5. return:函数的出口

return 用于从函数返回,并可以带回一个值。常见注意事项:

  1. return 后面可以是一个值,也可以是一个表达式;
  2. return; 适合返回类型为 void 的函数;
  3. return 执行后,函数立即结束,后面代码不再执行;
  4. 返回值类型和函数返回类型不一致时,会隐式转换;
  5. 如果函数有分支,要保证每条路径都有返回值;
  6. 函数返回类型不写,旧标准默认是 int,但现代 C 不推荐;
  7. 写了返回类型却没有 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;
}

在这里插入图片描述

注意:ifelse 都有 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 调用 Max3Max3 又调用 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. staticextern:链接属性初识

在讲 staticextern 之前,先复习两个概念:

  • 作用域:名字能起作用的范围;
  • 生命周期:变量从创建到销毁的时间段。

局部变量作用域是所在局部范围,生命周期是进入作用域到出作用域。
全局变量作用域是整个工程,生命周期是整个程序。

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 语言模块化编程的起点。下一讲,我们会把函数和数组结合起来,处理更复杂的任务,比如排序、查找、矩阵运算。建议你把上面的代码逐个敲一遍,再尝试修改参数、改变返回值、增加功能,真正把函数用起来。

在这里插入图片描述

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值