目录
1 函数的基本概念
1.1 为什么需要函数
在《街霸》游戏中,实现人物的出拳、出脚或跳跃等动作通常需要编写 50-80 行的代码。如果在每次需要这些动作时都重复编写相同的代码,不仅会使程序变得臃肿,还会影响代码的可读性和维护性。为了解决这一问题,可以将出拳、出脚或跳跃相关的代码提取出来,放在一个 {} 中,并为这段代码命名,这样就可以在需要执行这些动作的地方通过名称来调用这段代码。
提取出来的这部分代码可以视为程序中的一个函数。每当游戏需要执行出拳、出脚或跳跃的动作时,只需调用相应的函数即可。这种方法不仅减少了代码重复,还提高了程序的整洁度和可维护性。
1.2 什么是函数
函数是一种可重复使用的代码块,用于执行特定任务或操作。它允许我们将代码逻辑组织成独立的单元,从而提高代码的可读性、可维护性和重用性。
在 C 语言中,一个程序可以由一个或多个源文件组成(多文件编程),源文件的扩展名为 .c。每个源文件都是一个编译单位,并且可以包含多个函数,这些函数之间可以相互调用,因此函数是 C 程序的基本组成单位。
1.3 函数的作用
- 封装功能,将一个完整的功能封装成函数,提高代码的结构化和复用性。
- 代码模块化,将程序按照功能拆分成若干模块单元,有助于降低复杂度。
- 增强可维护性,如果需要修改某项功能,只需要调整对应的函数代码。
- 隔离细节,通过函数调用可以隐藏实现细节,只关心输入输出。
1.4 函数的分类
C 语言中,从使用的角度,函数可以分类两类。
1. 库函数,也称为标准函数,是由 C 系统提供的,用户不必自己定义,可直接使用它们,使用库函数,必须包含 #include 对应的头文件。
2. 自定义函数,解决具体需求而自己定义的函数,需先定义再使用。
2 函数的定义与调用
2.1 函数的定义
函数定义提供了函数的实际实现代码,即函数体。它包含了函数如何完成其任务的所有指令。函数定义包括函数的返回类型、函数名、参数列表(参数的类型和名称)以及函数体(包含执行函数任务的代码)。在定义函数时,编译器或解释器会生成相应的机器代码或字节码,以便在函数调用时执行。
返回类型 函数名(参数列表)
{
函数体语句1;
函数体语句2;
…………………………
函数体语句n;
return 返回值;
}
结构说明:
函数名:函数被调用时使用的名字,函数名要符合标识符规范。
函数体:函数中所包含的代码块,用于实现函数的具体功能和操作。
参数列表(形参列表):用于接收调用函数时传递进来的值(实参)。
返回值:函数执行完毕后,从函数传回到调用点的值,返回值的类型要与函数名前面的返回类型对应,否者会发生未定义行为,如果没有返回值,返回类型可以写 void。
2.2 案例演示
下面的代码演示了如何在 C 语言中定义函数:
#include <stdio.h>
// 定义了一个名为 func 的函数
// 它没有参数也没有返回值,只是做简单的打印输出
void func()
{
printf("hello func\n");
}
// 定义了一个名为 minus 的函数
// 接受两个整数参数 m 和 n,并返回它们的差
int minus(int m, int n)
{
return m - n;
}
// 定义了一个名为 adds 的函数
// 接受两个 double 类型的参数 i 和 j,并返回它们的和
double add(double i, double j)
{
double addRes = i + j;
return addRes;
// return i + j; 也可以直接返回数据
}
// 定义了一个名为 max 的函数
// 接受两个整数参数 a 和 b,并返回较大的一个
int max(int a, int b)
{
// 定义一个变量,存储结果
int maxNum;
maxNum = a > b ? a : b;
return maxNum;
}
// 主函数
int main()
{
return 0;
}
2.3 函数不能嵌套定义
C 语言中,所有的函数都是互相独立的,它们之间不能嵌套定义。这意味着一个函数不能定义在另一个函数的内部。
- C 语言不允许在一个函数内部定义另一个函数。
- 每个函数都是独立存在的,并且可以通过函数名来调用。
- 函数可以互相调用,但不能嵌套定义。
//错误演示
int func1(int a,int b) //第 1 个函数的定义
{
...
int func2(int c,int d) //第 2 个函数的定义
{
...
}
...
}
有些编译器的扩展允许函数嵌套定义,但这不是 C 标准的一部分,代码的可移植性可能会受到影响,强烈不建议。
2.4 函数的调用
在函数名后面加上圆括号即表示函数的调用,有参数的话,参数需要写在圆括号内。每当函数被调用一次,函数体内的语句都会被执行一遍。
#include <stdio.h>
// 定义了一个名为 func 的函数
// 它没有参数也没有返回值(void类型),只是简单地打印输出
void func()
{
printf("hello func\n");
}
// 定义了一个名为 minus 的函数
// 它接受两个整数参数 m 和 n,并返回它们的差
int minus(int m, int n)
{
return m - n;
}
// 定义了一个名为 add 的函数
// 它接受两个 double 类型的参数 i 和 j,并返回它们的和
double add(double i, double j)
{
double addRes = i + j; // 计算两个参数的和,并存储在局部变量 addRes 中
return addRes; // 返回和
// return i + j; 也可以直接返回结果
}
// 定义了一个名为 max 的函数
// 它接受两个整数参数 a 和 b,并返回较大的一个
int max(int a, int b)
{
int maxNum = a > b ? a : b;
return maxNum; // 返回较大的数
// return a > b ? a : b; 也可以直接返回结果
}
// 主函数,程序的入口点
int main()
{
// 1. 调用没有参数和返回值的函数
func(); // 直接调用,打印 "hello func"
func(); // 再次调用,再次打印 "hello func"
// 2. 调用有参数和返回值的函数
// 传递字面量作为参数
printf("10-20的结果:%d\n", minus(10, 20)); // 打印 10 减去 20 的结果
printf("20-10的结果:%d\n", minus(20, 10)); // 打印 20 减去 10 的结果
// 传递变量作为参数
double d1 = 10.0, d2 = 90.0; // 定义并初始化两个 double 类型的变量
printf("10.0+90.0的结果:%.2f\n", add(d1, d2)); // 打印 d1 和 d2 的和,保留两位小数
// 传递字面量作为参数
printf("20.0+80.0的结果:%.2f\n", add(20.0, 80.0));
// 调用 max 函数来比较整数
// 传递字面量作为参数
printf("66和88之间较大的是:%d\n", max(66, 88)); // 打印 66 和 88 中较大的数
printf("45和31之间较大的是:%d\n", max(45, 31)); // 注意更正了参数,以保持逻辑一致性
// 演示对返回值的操作
// 打印两个 max 函数返回值的和
printf("可以操作返回来的数据:max(66, 88) + max(12,6) = %d \n", max(66, 88) + max(12, 6));
return 0; // 程序正常结束
}
输出结果如下所示:
3 函数的返回值
函数调用后可以返回一个确定的值,这个值称为函数的返回值。返回值通常代表某种计算结果,或者用于指示函数执行的状态。
3.1 无返回值类型
当函数没有返回值或明确不需要返回值时,可以使用 void 作为其返回类型。
#include <stdio.h>
// 无返回值类型的函数,使用 void(即空类型)表示。
void fun01()
{
printf("调用了 fun01 函数\n");
}
int main()
{
// 调用无返回值的函数,仅执行其内部的打印操作
fun01();
return 0;
}
输出结果如下所示:
3.2 有返回值类型
3.2.1 正常情况
明确指定函数的返回值类型,例如 int、float、char 等,并在函数体内部使用 return 语句来返回具体的值。
#include <stdio.h>
// 有返回值类型的函数,返回 double 类型的值
double fun02()
{
return 3.1415926;
}
int main()
{
// 调用返回 double 类型值的函数,并打印其返回值
printf("fun02() 返回的数据:%.2f \n", fun02()); // 3.14
return 0;
}
输出结果如下所示:
3.2.2 无 return 语句
如果函数定义的返回类型不是 void,这意味着函数应该返回数据。但如果函数体内部没有 return 语句,函数将会返回一个不确定的值。
#include <stdio.h>
// 返回值类型为 int,但函数中没有 return 语句
// 这将导致函数返回一个不确定的值(通常是未定义行为)
int fun03()
{
10 + 20; // 这行代码仅计算了 30,但没有将其赋值给任何变量,也没有返回它
// 由于缺少 return 语句,函数将返回一个不确定的值
}
int main()
{
// 调用返回不确定值的函数,打印结果可能是任何整数
printf("fun03() 返回的数据(不确定值):%d \n", fun03());
return 0;
}
输出结果如下所示:
3.2.3 返回类型不一致
如果函数的返回类型与 return 语句中表达式的类型不一致,编译器会尝试进行隐式类型转换。
如果转换是安全的:编译器会进行类型转换,将 return 语句中的值转换为函数定义的返回类型。这种转换可能涉及数据截断(例如,从 int 转换为 char)、符号扩展(例如,从 unsigned char 转换为 int)或浮点数的精度损失(例如,从 double 转换为 float)。
#include <stdio.h>
// 返回值类型为 int,但 return 语句中的值是 double 类型
// 这种情况下,double 值会被隐式转换为 int 类型,导致精度损失
int fun04()
{
return 20.89;
}
int main()
{
// 调用返回值类型不匹配的函数,打印结果为 20,因为 20.89 被隐式转换为整数
printf("fun04() 返回的数据(精度损失):%d \n", fun04());
return 0;
}
输出结果如下所示:
如果转换不安全或不可能:编译器会警告或报错。例如,如果尝试从 void 类型的函数返回一个值(因为 void 类型表示无返回值),或者尝试返回一个指向局部变量的指针(因为局部变量在函数返回后会被销毁,导致悬垂指针),编译器可能会给出警告或报错。如下图所示:
3.3 案例演示
综合上述返回值的四种情况,我们通过下面的代码来进行综合的案例演示:
#include <stdio.h>
// 无返回值类型的函数,使用 void(即空类型)表示。
void fun01()
{
printf("调用了 fun01 函数\n");
}
// 有返回值类型的函数,返回 double 类型的值
double fun02()
{
return 3.14;
}
// 返回值类型为 int,但函数中没有 return 语句
// 这将导致函数返回一个不确定的值(通常是未定义行为)
int fun03()
{
10 + 20; // 这行代码仅计算了 30,但没有将其赋值给任何变量,也没有返回它
// 由于缺少 return 语句,函数将返回一个不确定的值
}
// 返回值类型为 int,但 return 语句中的值是 double 类型
// 这种情况下,double 值会被隐式转换为 int 类型,导致精度损失
int fun04()
{
return 20.89;
}
// 这个函数定义为 void 类型,意味着它不应该有返回值
// 如果尝试返回一个值(如已被注释的代码所示),编译器会发出警告或错误
void fun05()
{
return 666; // 如果不注释这行代码,这会导致编译错误或警告,因为 void 函数不能返回值
}
int main()
{
// 调用无返回值的函数,仅执行其内部的打印操作
fun01();
// 调用返回 double 类型值的函数,并打印其返回值
printf("fun02() 返回的数据:%.2f \n", fun02());
// 调用返回不确定值的函数,打印结果可能是任何整数
printf("fun03() 返回的数据(不确定值):%d \n", fun03());
// 调用返回值类型不匹配的函数,打印结果为 20,因为 20.89 被隐式转换为整数
printf("fun04() 返回的数据(精度损失):%d \n", fun04());
return 0;
}
输出结果如下所示:
提示:
为了避免潜在的问题并保持代码的可读性,建议确保函数的返回类型与 return 语句中表达式的类型一致,或者至少确保它们之间的转换是明确且安全的。如果需要进行类型转换,最好显式地进行(使用强制类型转换运算符,如 (int)),以使代码意图更加清晰。
现代 C 编译器通常会在检测到可能的类型转换问题时发出警告或错误。这些警告和错误有助于开发者识别并修复潜在的问题。因此,建议开启编译器的所有警告选项,并认真审查编译器输出的任何警告信息。
4 函数的参数
函数的参数可以分为形式参数(形参)和实际参数(实参)。
4.1 形参与实参
在定义函数时,函数名后面括号 () 中定义的变量称为形式参数,简称形参。
在调用函数时,函数名后面括号 () 中的使用的常量、变量、表达式称为实际参数,简称实参。
注意:
实参的数量要与形参的数量一致,否则会报错。
4.2 参数传递 -> 值传递
在 C 语言中,当我们调用一个函数时,实参(实际参数)回去初始化形参(形式参数)。这个过程通常被称为 “参数传递”。
当调用一个函数时,实参的值会被复制给形参,形参就像是一个临时变量,用于存储实参的值。因此,可以认为实参 “初始化” 了形参,但这并不是一个标准术语,而是描述这一过程的一种说法。
C 语言本身不直接支持引用传递,但可以通过传递实参的地址来间接实现(后续学习)。此时,形参通常是指针,指向实参的地址。
#include <stdio.h>
/**
* 函数功能:计算两个整数的和
* @param x 第一个整数(形参)
* @param y 第二个整数(形参)
* @return 返回 x 和 y 的和
*/
int func(int x, int y)
{
// 返回两个整数的和
return x + y;
}
int main()
{
// 调用 func 函数,实参为 3 和 5,用于计算它们的和
int sum = func(3, 5);
printf("%d \n", sum); // 输出:8
// 如果实参数量与形参不一致时,编译器会报错
func(100, 299, 300); // 错误:func 函数只接受两个参数
func(100); // 错误:func 函数需要两个参数
return 0;
}
如果实参数量与形参数量不一致,编译器会报错,如下所示:
5 文档注释
在 C 语言中,传统的注释有两种形式:
- 单行注释:使用 // 开头。
- 多行注释:使用 /* 开始并以 */ 结束。
然而,这些传统的注释方式并不能直接生成可读性高的文档。为了生成易于阅读和理解的文档,程序员们常常使用一种特殊的注释格式——文档注释。虽然 C 语言本身并不支持特定的文档注释语法,但一些工具可以解析特定格式的注释来生成文档。
VS Code 中文档注释的快捷键是 /** 。
/**
* @brief 计算两个整数的和
*
* @param x 第一个整数
* @param y 第二个整数
*
* @return 返回 x 和 y 的和
*/
int func(int x, int y)
{
// 返回两个整数的和
return x + y;
}
- @brief:简要描述函数的功能。
- @param:描述每个参数的作用。
- @return:描述函数返回值的意义。
编写文档注释后,当调用函数时,开发人员将鼠标悬停在函数名上即可查看函数的使用信息,这有助于更准确地调用该函数。
6 函数的原型声明
在 C 语言中,默认情况下,函数必须先定义后使用。因为在调用一个函数之前,编译器需要了解函数的返回类型和参数列表等基本信息。鉴于程序执行通常是从 main() 函数开始的,我们习惯于将所有函数的具体实现放置在 main() 函数之前。
如果想将函数的定义放在 main() 函数之后,可以通过在程序的开头部分给出函数的原型(即函数声明)。函数原型包含了必要的信息(如返回类型、函数的名称、参数列表[形参可以省略,但是形参的数据类型不可以省略]),但不包括具体的实现逻辑。这样一来,即使函数的实际定义位于程序的较后位置,编译器也能正确地进行处理。例如,如果有一个函数 int add(int, int); 这就是一个函数原型,它告诉编译器这个函数名为 add,返回值类型为 int 并且接受两个 int 类型的参数。
函数原型的主要用途:
- 告诉编译器函数的接口信息。
- 在函数调用前进行声明,以确保在实际调用时编译器能够正确识别该函数。
#include <stdio.h>
// 函数原型声明
int twice1(int num1, int num2); // 分号;是必需的
int twice2(int, int, int); // 形参的名称可以省略掉,数据类型不能省略
// 主函数
int main(void)
{
int result1, result2;
// 调用函数 twice1 并打印结果
result1 = twice1(10, 5); // 30
printf("twice1(10, 5) = %d\n", result1);
// 调用函数 twice2 并打印结果
result2 = twice2(10, 5, 2); // 34
printf("twice2(10, 5, 2) = %d\n", result2);
return 0;
}
// 函数 twice1 的定义
/**
* @brief 这个函数接收两个整数作为参数,并返回它们乘以 2 的结果
*
* @param num1
* @param num2
* @return int
*/
int twice1(int num1, int num2)
{
return (num1 + num2) * 2;
}
// 函数 twice2 的定义
/**
* @brief 这个函数接收三个整数作为参数,并返回它们乘以 2 的结果
*
* @param num1
* @param num2
* @param num3
* @return int
*/
int twice2(int num1, int num2, int num3)
{
return (num1 + num2 + num3) * 2;
}
7 多文件编程
学习完函数原型之后,我们可以利用这一功能特性来实现简易版的多文件编程。具体步骤如下:
7.1 搭建项目结构
首先创建一个名为 math 的文件夹,在 math 里面创建五个源文件,分别为 main.c、add.c、sub.c、mul.c 和 div.c。
- main.c:包含 main 函数和所有函数的原型声明。
- add.c:只包含加法函数的实现。
- sub.c:只包含减法函数的实现。
- mul.c:只包含乘法函数的实现。
- div.c:只包含除法函数的实现,并注意处理除数为 0 的情况。
7.2 编写源代码
main.c 中代码内容如下所示:
#include <stdio.h>
// 函数原型声明
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
int main()
{
printf("5 + 3 = %d\n", add(5, 3));
printf("5 - 3 = %d\n", sub(5, 3));
printf("5 * 3 = %d\n", mul(5, 3));
printf("5 / 3 = %d\n", div(5, 3));
return 0;
}
add.c 中代码内容如下所示:
// add.c
// 只包含加法函数的实现
int add(int a, int b)
{
return a + b;
}
sub.c 中代码内容如下所示:
// sub.c
// 只包含减法函数的实现
int sub(int a, int b) {
return a - b;
}
mul.c 中代码内容如下所示:
// mul.c
// 只包含乘法函数的实现
int mul(int a, int b) {
return a * b;
}
div.c 中代码内容如下所示:
// div.c
// 只包含除法函数的实现,注意处理除数为 0 的情况
int div(int a, int b)
{
if (b == 0)
{
// 这里只是简单地返回 0,实际中可能需要更复杂的错误处理
// 这通常不是一个好的做法,因为它隐藏了错误
return 0;
}
return a / b;
}
7.3 一次性编译多个源文件
在 VS Code 中,如果想编译多个 C 文件,通常会使用 tasks.json 文件来配置一个编译任务。这个任务会告诉 VS Code 如何调用编译器(如 gcc 或 clang )来编译代码。
在这里,我们的目标是简单地实现一个多文件编程的 C 项目,而不想通过 VS Code 的复杂配置(如修改 tasks.json)来达成。我们可以直接利用命令提示符(cmd)这一更为基础且灵活的工具来编译我们的多个 C 源文件,具体操作如下图所示: