在数学中,函数通常被定义为一种从一个集合(称为定义域)到另一个集合(称为值域)的映射关系。数学中的函数可以用公式、图形、表格等方式来表示,其主要特点是每个输入值都有唯一的输出值。
而在C语言中,函数是一组执行特定任务的代码块。C语言中的函数定义由函数头和函数体组成。函数头包括函数的返回类型、函数名和参数列表的定义,而函数体则包含了具体的代码实现。
一:函数
在c语言中,函数分为两种
-
库函数
-
自定义函数
1.1库函数
C语言中的库函数是预先编写好的可供调用的函数集合,它们实现了一些常见的功能和算法,并以库的形式提供给开发者使用。库函数的存在有以下几个主要意义:
-
提高代码的重用性:库函数实现了一些常用的功能和算法,开发者可以直接调用这些函数来完成相应的任务,而无需从头开始编写代码。这大大提高了代码的重用性,减少了开发时间和劳动成本。
-
提高代码的可维护性:库函数是经过充分测试和优化的,在使用过程中一般不需要关注其内部实现细节,只需了解函数的接口和功能即可。这使得代码更易于维护,开发者可以将精力集中在自己的业务逻辑上,而不必关注底层实现。
-
加速开发过程:库函数已经实现了一些常见的功能,包括数学运算、字符串处理、IO操作等,使用库函数可以快速地完成这些任务,加速开发过程。
简单的总结,C语言常用的库函数都有:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
1.2库函数的使用
使用库函数的方法如下:
-
引入头文件:库函数通常通过头文件进行声明,开发者需要在代码中引入相应的头文件来包含库函数的声明。
-
调用函数:在程序中需要使用库函数时,通过函数名和参数列表调用相应的函数即可完成相应的功能。
下面是一个简单的C语言例子,展示如何调用标准库函数 printf()
:
#include <stdio.h> // 包含标准库头文件
int main() {
int num = 10;
printf("The number is: %d\n", num); // 调用 printf() 函数打印输出
return 0;
}
通过在代码中包含 <stdio.h>
头文件,我们就能够使用标准库函数 printf()
。该函数可以格式化输出到标准输出流(通常是控制台),并接受格式化字符串和相应的参数。
1.3自定义函数
因为在实际开发中,需求和场景千变万化非常复杂,所以库中的函数并不能满足我们所有的需要,所以就有了自定义函数,自定义函数对于程序员来说更加重要。
当在C语言中编写自定义函数时,需要按以下步骤进行:
-
函数声明:在函数使用之前,需要提前声明函数的原型。函数声明包括函数返回类型、函数名以及函数参数列表。
-
函数定义:在函数声明之后,可以定义函数的实际逻辑。函数定义包括函数头和函数体。函数头包括函数返回类型、函数名和参数列表,而函数体则包括具体的代码逻辑。
-
函数调用:在其他地方需要使用函数时,可以通过函数名和参数列表来调用函数。
下面是一个示例,展示如何编写一个自定义函数来计算两个整数的和:
#include <stdio.h>
// 函数声明
int sum(int a, int b);
// 主函数
int main() {
int x = 5;
int y = 3;
int result = sum(x, y); // 函数调用
printf("The sum of %d and %d is %d\n", x, y, result);
return 0;
}
// 函数定义
int sum(int a, int b) {
int result = a + b;
return result;
}
上面的代码中,我们先在程序开始处进行函数声明,告诉编译器有一个名为 sum
的函数,接受两个整数参数并返回一个整数结果。
因为c语言是解释性语言,是从上至下一边编译一遍执行的,如果我们不写函数声明,那么在主函数中,主函数就会不认识sum这个函数,自然也就会报错了
在主函数中,我们通过调用 sum
函数来计算两个整数的和,并打印结果。随后,我们使用 sum
函数进行定义,函数体内部计算了两个整数的和并将结果返回。
1.4实际参数和形式参数
在C语言中,函数的参数分为实际参数(实参)和形式参数(形参)。实际参数是在调用函数时传递给函数的值,而形式参数是函数定义中声明的参数。
下面是一个简单的示例代码来说明实际参数和形式参数的概念:
#include <stdio.h>
// 定义一个函数,接受两个形式参数
void add(int a, int b) {
int sum = a + b;
printf("The sum is: %d\n", sum);
}
int main() {
int num1 = 5;
int num2 = 3;
// 调用 add 函数,num1 和 num2 作为实际参数传递
add(num1, num2);
return 0;
}
在上面的代码中,add
函数有两个形式参数 a
和 b
,它们的类型都是 int
。在 main
函数中,我们声明了两个整型变量 num1
和 num2
,并将它们作为实际参数传递给 add
函数。在函数中,形式参数 a
和 b
接受了实际参数 num1
和 num2
的值。
当我们调用 add(num1, num2)
时,实际参数 num1
的值被赋给了形式参数 a
,实际参数 num2
的值被赋给了形式参数 b
。在函数内部,我们可以使用形式参数执行计算操作,这里是将两个参数相加并打印结果。
下面是图解说明:
运行这段代码,输出将会是 The sum is: 8
,因为 add
函数计算了 num1
和 num2
的和,并打印了结果。
1.5函数的传值调用和传址调用
1.5.1 传值调用
在C语言中,有两种方式可以在函数之间传递参数,分别是传值调用和传址调用。
传值调用(Call by Value)是指在函数调用过程中,将实际参数的值复制给形式参数,但是函数内部对形式参数的修改不会影响到实际参数。
下面是一个传值调用的示例代码:
#include <stdio.h>
void changeValue(int num) {
num = 10; // 修改形式参数的值
}
int main() {
int num = 5;
printf("Before function call: %d\n", num);
changeValue(num); // 传值调用
printf("After function call: %d\n", num);
return 0;
}
输出结果为:
Before function call: 5
After function call: 5
可以看到,在函数changeValue
内部,形式参数num
被修改为10,但是在main
函数中,实际参数num
的值仍然是5,并没有改变。
下面通过图解说明
首先我们定义了一个int类型的num,接着把这个num的值10赋值给了形式参数num(这里也叫num,但是这两个num是不一样的),接着通过changeValue这个函数,对num的值进行修改,但是在函数内部修改形式参数的值并不会影响实参的值。
因此,当我们在main
函数中打印num
的值时,它仍然是初始值5,并没有被修改。
1.5.1 传址调用
如果你想要在函数中修改原始变量的值,你可以通过传递指针参数来实现。这样,在函数中就可以通过解引用指针来修改原始变量的值。
传址调用是指在函数调用过程中,将实际参数的地址传递给形式参数,函数内部可以通过解引用操作符*
修改实际参数的值。
下面是一个传址调用的示例代码:
#include <stdio.h>
void changeValue(int *num) {
*num = 10; // 修改实际参数的值
}
int main() {
int num = 5;
printf("Before function call: %d\n", num);
changeValue(&num); // 传址调用
printf("After function call: %d\n", num);
return 0;
}
输出结果为:
Before function call: 5
After function call: 10
可以看到,在函数changeValue
内部,通过解引用操作符*
修改了实际参数num
的值,所以在main
函数中,实际参数的值变为了10。
下面通过图解说明
因为虽然有一个形参,但是实参传递给形参的值是一个地址,形参可以通过这个地址解引用来找到num所在的空间,并进行值的修改
传值调用是将实际参数的值复制给形式参数,在函数内部对形式参数的修改不会影响到实际参数;而传址调用是将实际参数的地址传递给形式参数,函数内部可以通过解引用操作符修改实际参数的值。
1.6函数的嵌套调用和链式访问
函数的嵌套调用是指在一个函数内部调用另一个函数,而链式访问是指在连续的多个函数调用中使用返回值作为下一个函数调用的参数。下面我将通过代码示例来解释这两个概念。
首先,让我们看一个函数的嵌套调用的示例:
#include <stdio.h>
void innerFunction() {
printf("This is the inner function.\n");
}
void outerFunction() {
printf("This is the outer function.\n");
innerFunction(); // 在外部函数调用内部函数
}
int main() {
outerFunction(); // 在主函数中调用外部函数
return 0;
}
在上面的代码中,我们定义了两个函数innerFunction和outerFunction。在outerFunction函数中,我们调用了innerFunction函数。然后,在主函数中,我们调用了outerFunction函数。
当我们运行这段代码时,输出将是:
This is the outer function.
This is the inner function.
这是因为在外部函数调用内部函数后,内部函数的代码也会被执行。
接下来,让我们看一个链式访问的示例:
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int main() {
int result = multiply(subtract(add(5, 3), 1), 2);
printf("Result: %d\n", result);
return 0;
}
在上面的代码中,我们定义了三个函数add、subtract和multiply,在主函数中,我们使用连续的函数调用来进行链式访问。
具体来说,我们首先将5和3作为实际参数传递给add函数,然后将add函数的返回值作为实际参数传递给subtract函数,最后将subtract函数的返回值作为实际参数传递给multiply函数。
运行这段代码时,输出将是:
Result: 14
这是因为按照从左到右的顺序进行函数调用,并且每个函数的返回值作为下一个函数调用的参数。
注意:函数可以嵌套调用,但是不能嵌套定义。
1.7函数的声明
函数声明的作用:
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
对于第二点和为什么有函数声明,是因为c语言是一种解释性的语言,是从上向下边翻译边执行的,所以说如果函数在使用位置之前,则可以不用声明函数,而函数在使用位置之后,则需要声明函数了,因为不声明编译器就找不到,声明了编译器就会自动跳转到该函数体内执行代码
在C语言中,函数声明是指在程序中提前声明函数的名称、返回类型和参数列表的过程。函数声明在函数定义之前,通常放在头文件中,以便在其他部分使用该函数时能够正确地识别和调用它。
函数声明的一般形式如下:
返回类型 函数名(参数列表);
下面是一些函数声明的示例:
int max(int a, int b);
void greet();
float calculateAverage(float arr[], int size);
在这个示例中:
max
函数返回一个int
类型的值,它有两个int
类型的参数a
和b
。greet
函数没有返回值,也没有参数。calculateAverage
函数返回一个float
类型的值,它有一个float
类型的数组参数arr
和一个int
类型的参数size
。
函数声明在程序中的位置很重要。通常,我们将函数声明放在头文件中,并在需要使用函数的文件中包含该头文件。这样,当多个文件使用同一个函数时,它们可以共享同一个函数声明,避免重复的声明和定义。
1.8函数递归
程序调用自身的编程技巧称为递归,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的两个必要条件:
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
下面通过一个简单的递归求阶乘的例子讲解:
int factorial(int n) {
// 递归基
if (n == 0) {
return 1;
}
// 递归调用
return n * factorial(n - 1);
}
让我们对上述代码进行解释:
因为对于一个阶乘,我们可以把阶乘转化为一个数再乘以阶乘的形式,比如说 100!= 100 x 99!,而 99!又可以写成 99 x 98!这就是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
函数递归和我们之前学过的循环语句非常相似,都是在重复的做某一件事情,但是函数递归做的事是它本身,而循环语句做的事情则会更加普遍和随机
1.8.1栈溢出
在c语言中,内存的分配大概有三种空间,一个是栈区,一个是堆区,一个是静态区。
而函数栈帧的开辟则是在栈区,所以我们每一次的函数调用,都需要在栈区开辟一块内存空间,用来保存调用过程下的上下文消息,如果调用次数太多的话,就会导致栈区空间的满溢,也就是栈溢出,举个函数递归求阶乘的例子:
如果当我们递归的次数太多的时候,函数的栈会满,而函数只有当执行完所有的代码后才会销毁,所以我们在递归时,递归的次数不能过大