函数

函数
苏丙榅1. 函数概述
1.1 函数的分类
C语言是一种通用的高级编程语言,拥有丰富的函数库和强大的系统编程能力。在C语言中,函数是模块化和可重用的代码段,用于实现特定的功能。函数将一系列的语句组织在一起,可以接受输入参数并返回值。
函数的定义包括函数名、参数列表、返回类型和函数体:
- 函数名是唯一的标识符,用于调用函数。
- 参数列表指定了函数接受的参数,可以是任意数量和类型的参数。
- 返回类型指定了函数返回值的类型,可以是基本数据类型、指针类型或结构体类型。
- 函数体是实现具体功能的语句块。
C语言中的函数可以分为以下几类:
- 标准库函数(Standard Library Functions):C语言提供了很多常用的函数,如输入输出函数(如- scanf和- printf)、字符串处理函数(如- strcpy和- strlen)、数学库函数(如- sqrt和- sin)等。这些函数被包含在标准库中,可以直接调用。
- 用户自定义函数(User-defined Functions):在程序中,我们可以自定义函数,通过函数名调用并执行特定的功能。用户自定义函数可以接受参数并返回值,可以将程序分解成多个函数,提高代码的模块化和可读性。
- 递归函数(Recursive Functions):递归函数是一种特殊的函数,它在函数体内部调用自身。通过递归调用,函数可以解决涉及重复计算的问题,实现更简洁和优雅的算法。但需要注意递归函数应当包含条件语句以避免无限递归。
- 内联函数(Inline Functions):内联函数是一种将函数的代码嵌入到调用点的编译器特性。内联函数的主要目的是减少函数调用的开销,提高程序的执行速度。通过使用关键字- inline来声明内联函数。
- 回调函数(Callback Functions):回调函数是一种通过函数指针作为参数传递的函数。它可以在程序运行时动态地指定回调函数,以实现特定的行为。常见的例子是在图形界面编程中,通过回调函数响应用户的操作。
无论是标准库函数、用户自定义函数还是其他类型的函数,都可以根据需要进行组合和调用,实现复杂的程序逻辑和功能。
1.2 函数的作用
函数在编程中有多种作用和用途,以下是函数的一些常见作用:
- 代码模块化:函数可以将一段代码逻辑封装成一个独立的模块,使代码结构更清晰和可维护。通过将程序分解成多个函数,可以使代码更易于理解、修改和调试。
- 代码重用:函数的设计就是为了提高代码的重用性。通过将通用的功能封装成函数,可以在程序中多次调用该函数来实现相同或类似的功能,避免重复编写重复的代码。
- 提高可读性:函数使代码的逻辑更加清晰和易于理解。给函数起一个好的函数名,并使用适当的注释,可以使代码的意图更加明确,提高代码的可读性。
- 提高代码的维护性:使用函数可以将代码结构化,并使其更易于理解和修改。当需要修改某个功能时,只需修改对应的函数,而不需要修改整个程序。这种模块化的设计使得代码更易于维护和改进。
总的来说,函数是编程中的基本构建块,它们提供了一种有效的方式来组织、重用和管理代码。通过合理地设计和使用函数,可以提高代码的可读性、可维护性、可测试性和复用性,进而改善开发效率和代码质量。
关于函数的使用,下面通过两个例子来进行说明:
| 1 | // 示例1 | 
- 第 4 行的max是一个自定义函数,有两个int型参数a和b,返回值为int类型
- 在main函数中可以多次调用自定义函数max,并且可以动态给它指定不同的实参,从而得到不同的最大值,非常灵活
通过例子对函数有了大致的了解之后,接下来我们来详细讲解如何去定义和声明一个函数。
2. 函数的定义和声明
2.1 函数定义
在编程中,函数定义是指定义一个函数的结构、功能和操作的代码块。函数定义包含函数的名称、参数列表、返回类型、函数体等组成部分。
以下是一个函数定义的一般格式:
| 1 | 返回类型 函数名(参数列表) | 
具体来说,各个部分的含义如下:
- 返回类型:函数执行完成后返回的数据类型。 - 可以是基本数据类型,如整数、浮点数、字符等
- 可以是指针、结构体、自定义类型等。
- 如果函数没有返回值,可以使用 void关键字表示。
 - 1 
 2
 3
 4
 5
 6- // 函数返回值为整形 int 
 int max(int a, int b){}
 // 函数返回值为布尔类型 bool
 bool isRunning(int c){}
 // 没有返回值
 void show(){}
- 函数名:函数的名称(最好见名知义),用于调用和引用函数。 - 合法的函数名由字母、数字和下划线组成
- 一般约定以字母开头
 - 1 
 2
 3
 4
 5
 6- // 有意义的函数名 
 int max(int a, int b){} // 求最大值
 int sum(int a, int b, int c){} // 求和
 // 没有意义的函数名
 void zaaaaaaaa(){}
 void xyc123(char c){}
- 参数(形参)列表:参数列表中的参数可以有N(N>=0)个 - 定义函数的时候指定的参数叫做形参,形参不占用存储空间
- 调用函数的时候指定的参数叫做实参,实参是占用存储空间的
- 参数可以有多个,用逗号分隔,每个参数包括参数类型和参数名
- 如果函数没有参数,参数列表可以为空,或者指定为 void
- C 语言中,形参不允许指定默认值(赋值)
 - 1 
 2
 3
 4
 5
 6
 7
 8- // 没有参数 
 void show(){}
 void print(void){}
 // 有参数
 int max(int a, int b){} // 2个参数, a, b 都是形参
 int sum(int a, int b, int c){} // 3个参数, a, b, c 都是形参
 // 给形参初始化 -- error
 int show(int a = 9){} // error, 语法错误
- 函数体:函数的具体实现,包括一系列语句和操作。 - 函数体内可以有变量声明、条件语句、循环语句、函数调用等
- 如果函数有返回值,需要再函数体最后添加return语句- return语句是函数体内部执行的最后一个语句
- return语句一般出现在函数体末尾,但是也可能出现在函数体中间位置,表示函数从该位置退出了。
 
 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31- // 函数有返回值,需要返回一个整形 
 int max(int a, int b)
 {
 if(a > b)
 {
 return a; // 返回一个变量, 变量是整形
 }
 else
 {
 return b+10; // 返回一个表达式, 表达式结果是整形
 }
 }
 double sum(int a, int b)
 {
 // 返回值是一个表达式, 值是整形
 // 会先将表达式结果隐式转换为 double 类型, 然后再返回
 return a+b;
 }
 // 函数没有返回值
 void show()
 {
 printf("hello, world\n");
 }
 void print()
 {
 printf("hello, world\n");
 return; // 相当于没有返回任何数值, 可以省略不写
 }
2.2 函数声明
所谓函数声明,就是在函数尚在未定义的情况下,事先将函数的名称、参数列表和返回类型通知编译器,这样可以保证编译能正常进行,作为用户也可以通过声明去理解和使用这个函数。函数声明通常放置在头文件中,可以在多个源文件中共享。
函数声明的一般格式如下:
| 1 | 返回类型 函数名(参数列表); | 
关于函数声明我们应该掌握如下几点:
- 函数声明可以写到头文件也可以写到源文件
- 一个函数只能被定义一次,但可以声明多次
- 如果函数定义的位置在主调函数(调用它的函数)之后,则必须在调用此函数之前对被调用的函数作声明
以下是一个函数声明的示例:
| 1 | int add(int a, int b); | 
在上述示例中,函数声明了一个名为 add 的函数,它接受两个整数类型的参数 a 和 b,返回类型为 int。函数声明只说明了函数的名称、参数列表和返回类型,而没有提供函数的具体实现。可以在函数调用的地方使用这个声明,而编译器会在链接时找到该函数的定义。
- 函数定义在主调函数之前 - 不需要声明 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 int add(int a, int b)
 {
 return a + b;
 }
 int main()
 {
 int result = add(8, 9);
 printf("result = %d\n", result);
 return 0;
 }- 在上面的例子中,主调函数对应的就是 - main函数,在- main函数之前- add函数已经被定义出来了,所以此时不需要进行函数声明。
- 函数定义在主调函数之后 – 需要先声明 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 int add(int a, int b);
 int main()
 {
 int result = add(8, 9);
 printf("result = %d\n", result);
 return 0;
 }
 int add(int a, int b)
 {
 return a + b;
 }- main函数是程序的入口函数,在这个函数中调用了- add函数,此时编译器是懵逼的,全然不知道有这么一个函数的存在,所以在编译的时候我们就能看到相关的警告信息:- 1 - warning C4013: “add”未定义;假设外部返回 int - 解决方法也很简单,只需要在主调函数( - main函数)之前添加被调用函数的函数声明即可,此时编译器就知道- add函数是何方神圣(叫什么名字),有什么神通了(参数有几个分别是什么类型,返回值是什么类型)。
最后,给大家总结一下函数定义和声明的区别:
- 定义是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。 
- 声明的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)告知给编译器,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。 
3. 函数的调用
定义函数后,必须要该函数进行调用,这样它的函数体才能够被执行。自定义函数和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main() 函数里调用别的函数,一个 C 程序里有且只有一个main()函数。
3.1 函数的执行流程
下面通过一个简单的例子给大家讲一下C语言中程序的执行流程:
| 1 | 
 | 
- 找到程序中的入口函数main()从第一行开始执行
- 调用了自动以函数add(int a, int b)- 编译器会在main()函数的上面寻找这个叫做add的自定义函数
- 如果找到了,检查函数的参数类型是否匹配,没参数则忽略
 
- 编译器会在
- 执行add()函数,此时main()函数里面的执行会被add()这一行(第9行)代码阻塞,直到add()函数执行完毕,main()中的代码才会继续向下(第10行)执行。
- main()函数执行到- return 0, 程序退出,执行完毕。
3.2 函数的形参和实参
在函数调用中,有两个相关概念:形式参数(形参)和实际参数(实参):
- 形式参数(形参): - 在函数定义中声明的参数。- 形参用于描述函数接受的参数类型和顺序。 
- 形参在函数定义中充当占位符,表示函数在执行时将接受的值。 
- 形参不占用内存空间
- 形参通常在函数定义的括号内声明,并且可以有多个,用逗号分隔。
- 形参只在函数内部可见,其作用域局限在函数内部。
 - 下面是一个简单的函数定义,其中 - a和- b就是形式参数:- 1 
 2
 3
 4
 5- int add(int a, char b) 
 {
 int sum = a + b; // b 会进行隐式类型转换 char->int
 return sum;
 }- 在上述示例中, - add函数定义了两个整型形式参数- a和- b。
- 实际参数(实参): - 在函数调用时传递给函数的具体值或表达式。- 实参作为函数调用的一部分,提供了函数在执行时所需的数据。 
- 实参可以是常量、变量、表达式或函数返回值等。 
- 实参传递给函数,按照函数定义中形式参数的顺序进行匹配。 
 - 下面是一个函数调用的示例,其中 整数 - 2和 字符- a就是实际参数:- 1 - int result = add(2, 'a'); - 在上述示例中,函数 - add被调用时,实际参数- 2和- 3传递给了函数的形式参数- a和- b。
在进行函数调用的时候,形参和实参有如下区别:
- 实参变量对形参变量的数据传递是“值传递”,即单向传递,只能由实参传给形参,反之则不被允许。
- 在调用函数时、调用时、调用时,编译器临时给形参分配存储单元。调用结束后,形参单元被释放。
- 实参单元与形参单元是不同的单元,因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
| 1 | 
 | 
以上代码输出的结果如下:
| 1 | 形参 a = 109, b = 206 | 
- 在自定义函数modify内部操作了两个整形变量,就是这个函数的两个形参a和b,这两个形参只能在改函数体内部使用。
- 在主函数main中对自定义函数modify进行了调用,并且给函数指定了两个实参num1和num2,也就意味着这两个实参的值被赋值给了函数的两个形参,相当于做了这样的操作:- a = num1;
- b = num2;
 
- 实参num1和num2只是将值传递给了modify函数的形参a和b- 形参a和b值的改变不会对实参num1和num2有任何影响
- 实参num1和num2访问范围是主函数main的函数体
- 实参num1和num2的生命周期随着主函数main的函数体结束而结束
 
- 形参
3.3 函数的返回值
函数返回值是指在函数执行完毕后,通过 return 语句将结果返回给函数调用的地方。返回值允许函数将计算结果、状态信息或其他需要传递的数据返回给调用者。
函数的返回值在函数定义时通过返回类型声明,在函数调用时使用接收变量来接收返回值。以下是一般的函数返回值的语法格式:
| 1 | 返回类型 函数名(参数列表) | 
具体来说,函数返回值的相关要素如下:
- 返回类型:函数执行完成后返回的数据类型。 - 可以是基本数据类型,如整数、浮点数、字符等
- 可以是指针、结构体、自定义类型等。
- 如果函数没有返回值,可以使用 void关键字表示。
 
- return语句:用于返回函数执行的结果。- 它可以后接一个表达式,表示返回的值。
- 如果函数的返回类型是 void,则不需要return语句。
 
以下是一个函数返回值的示例:
| 1 | int add(int a, int b) | 
在上述示例中,函数 add 接受两个整数类型的参数 a 和 b,在函数体中计算它们的和,并通过 return 语句返回结果。
在函数调用时,可以使用变量来接收函数的返回值:
| 1 | int result = add(2, 3); | 
在上述示例中,函数 add 将返回结果 5,并将其赋值给变量 result。如果函数调用者对该函数的返回值不感兴趣,可以不去接收这个返回值,无视即可。
函数的返回值可以用于进行进一步的计算、输出、赋值或其他操作。通过返回值,函数可以将结果传递给调用者,使得函数的执行结果能够被利用和处理。
4. 内联函数
在C语言中,内联函数(Inline Function)是一种用于优化代码执行效率的机制。内联函数在编译时将函数的代码直接插入到调用它的地方,而不是通过函数调用的方式执行,从而减少了函数调用的开销,提高了代码的执行速度。
C语言的内联函数使用 inline 关键字来声明。将函数声明为内联函数只是给编译器一个建议,告诉它将函数的代码插入到调用的地方。编译器可以选择忽略内联函数的建议,继续将函数编译为常规函数。
以下是内联函数的一般格式:
| 1 | inline 返回类型 函数名(参数列表) | 
具体来说,内联函数的使用规则和特点包括:
- 关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。
- 内联函数的定义通常放在头文件中,以便在多个源文件中共享。
- 内联函数的函数体通常较简单,避免包含复杂的控制流程、循环或递归调用和大量的代码。
- 内联是以代码膨胀为代价,从而省去了函数调用的开销,从而提高了函数的执行效率(空间换时间)
下面是一个简单的内联函数的示例:
| 1 | 
 | 
在上述示例中,multiply 函数被声明为内联函数,它接受两个整数类型的参数 a 和 b,并返回它们的乘积。在使用这个内联函数时,编译器将尝试在调用该函数的地方插入相应的代码,而不会执行函数调用操作。
5. 递归函数
在C语言中,递归函数是一种函数调用自身的技术。递归函数可以用于解决需要重复执行相同操作的问题,将问题不断分解为更小的子问题,并通过函数调用自身来处理这些子问题,直到达到最简单的情况。
递归函数的基本原理是把大问题分解成一个或多个相同的小问题,然后通过调用自身来解决这些小问题。递归函数包括两部分:基本情况和递归调用:
- 基本情况指的是递归函数终止的条件,当函数遇到基本情况时,不再调用自身,直接返回结果。 
- 递归调用指的是在函数内部调用自身来解决规模较小的子问题。通过递归调用,问题的规模不断缩小,直到达到基本情况,然后逐层回溯,获得最终的结果。 
需要注意的是,递归函数必须具备终止条件,否则会陷入无限递归的循环中,导致程序异常。
5.1 阶乘
阶乘是一个正整数与比它小的所有正整数的乘积。一般来说,我们用符号 "!" 表示阶乘。
例如,5 的阶乘表示为 5!,计算方法为:
5! = 5 × 4 × 3 × 2 × 1 = 120
阶乘的计算可以通过循环或递归来实现。下面分别给出阶乘的循环和递归算法示例。
1. 循环计算阶乘:
| 1 | unsigned int factorial(unsigned int n) | 
2. 递归计算阶乘:
| 1 | unsigned int factorial(unsigned int n) | 
- 0和1的阶乘为1,当参数为 0 或者 1时,就是递归结束的条件
- 如果参数为n(n>1),需要用当前的值 - n乘以- n-1的阶乘,也就是- n * factorial(n - 1)
通过上图可知递归分为两个阶段
- 执行函数体内不满足递归结束条件的部分代码,继续递归(压栈)
- 递归结束条件满足之后,执行剩余代码,并返回数据,依次类推(出栈)
- 最后被调用的函数第一个返回数据(也就意味着这个函数第一个执行完函数体内的所有代码),第一个被调用的函数最后一个返回数据(此时得到的就是最终的值)
5.2 斐波那契数列
  斐波那契数列(Fibonacci Sequence)是一个数列,其中每个数字都是前两个数字之和。数列的前两个数字通常定义为0和1,然后通过递推关系计算后续的数字。斐波那契数列的定义如下:
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (n ≥ 2)
根据这个定义,斐波那契数列的前几个数字为:0, 1, 1, 2, 3, 5, 8, 13, 21, …
以下是两种常见的计算斐波那契数列的方法:迭代和递归。
1. 迭代方法:
| 1 | int fibonacci(int n) | 
2. 递归方法:
| 1 | int fibonacci(int n) | 
  在上述示例中,递归函数 fibonacci 接受一个整数参数 n,用于计算斐波那契数列的第 n 项。
- 当 n小于等于 1 时,达到基本情况,直接返回n。
- 当 n大于 1 时,通过递归调用fibo_recursive(n-1)和fibo_recursive(n-2)来计算第n项的值。
 
6. 多文件编程
6.1 代码模块化
当需求比较复杂的时候或者做一个比较大的项目的时候,不可能将所有的源码都写到一个文件中,此时就需要进行模块化处理,思路如下:
- 将需求拆分成若干个小模块,每个模块对应一个源文件
- 给每个源文件提供一个头文件,通过这种方式实现函数的复用- 头文件进行函数声明
- 源文件进行函数定义
 
假设我现在要做一个计算器,根据需求拆分成了加、减、乘、除四个模块,那么此时对应的源文件就是五个:
- 处理加法的源文件:add.c
- 处理减法的源文件:subtract.c
- 处理乘法的源文件:multiply.c
- 处理除法的源文件:divide.c
- 主函数main(入口函数)对应的源文件:test.c
add.h
| 1 | // add.h | 
add.c
| 1 | 
 | 
subtract.h
| 1 | // subtract.h | 
subtract.c
| 1 | 
 | 
main.c
| 1 | 
 | 
关于乘法和除法的例子在这里就不写了,如法炮制就可以,完全是一样的。对于上面的代码有一下细节需要说明一下:
- 一个头文件可能被多个源文件包含,只要包含了这个头文件,在源文件中就可以使用头文件中声明的函数了
- 一个头文件中也可以包含其它头文件
- 在包含多个头文件的时候,对顺序没有要求
- 要避免自定义头文件被重复包含
在C语言中,一个源文件对应一个头文件并不是必须的,有时候多个源文件可以对应同一个头文件,也就是说N个源文件中定义的函数的声明都被放到了同一个头文件中。
calc.h
| 1 | // add.h | 
add.c
| 1 | 
 | 
subtract.c
| 1 | 
 | 
main.c
| 1 | 
 | 
6.2 避免头文件重复包含
在包含头文件的时候可能由于失误将一个头文件包含了两次,或者是因为头文件的嵌套包含导致某个头文件被包含了多次(>1)
| 1 | // a.h | 
如果出现了以上情况,程序在编译的时候就会报错。为了避免同一个文件被include多次,C/C++中有两种方式:
- 使用文件保护宏 - 文件保护宏(header guard macro)是一种 - 传统的预处理指令,用于防止头文件重复包含。文件保护宏可以确保在编译时只包含一次特定的头文件,从而避免由于多次包含导致的重复定义错误。- 文件保护宏需要在头文件的开头和结尾进行定义和结束,如下所示:- 1 
 2
 3
 4
 5
 6- #ifndef HEADER_FILE_NAME_H 
 #define HEADER_FILE_NAME_H
 // 头文件的内容
 #endif // HEADER_FILE_NAME_H- HEADER_FILE_NAME_H是一个唯一的标识符,用于确保宏定义的唯一性,因此每个头文件都应该使用不同的宏名称。
- 可以根据需要自定义宏的名称,常见的做法是以头文件名称的大写形式作为宏的名称。
- 在编译过程中,首次包含头文件时,ifndef条件为真,进入#define块,并定义了该宏。随后的重复包含,由于宏已经有定义,条件为假,不再执行#define块内的代码。
 
- 使用 - #pragma once- #pragma once是一种预处理指令,用于确保头文件只被编译一次。它是一种用于防止头文件重复包含的非标准的预处理指令,被大多数主流编译器所支持。- 使用 - #pragma once可以替代传统的头文件保护宏,如- #ifndef、- #define和- #endif。通过使用- #pragma once,可以更简洁地确保头文件只被编译一次,而无需手动编写宏定义。- 示例使用 - #pragma once的头文件:- 1 
 2
 3- #pragma once 
 // 头文件内容- 需要注意的是,虽然 - #pragma once在大多数情况下提供了简洁的头文件包含机制,而且几乎所有主流的编译器都支持它,- 但它不是C或C++的标准预处理指令。因此,如果您希望代码更具可移植性,可以继续使用传统的头文件保护宏。





















