本文将通过 知识讲解+代码演练 来具体阐述C语言中的函数,相信老铁们在看完这篇文章后会对C语言中的函数这个知识点有一个更深的了解。
1. 什么是函数
相信大家很了解的是数学中的函数,那么你了解C语言中的函数么?维基百科中对函数是这样定义的:子程序。
1. 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
2. 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2. C语言中函数的分类
C语言函数主要分为两种:1.库函数 2.自定义函数。
2.1 库函数
2.1.1 定义
C语言中把常用的功能,进行了封装,封装成一个个函数,提供出来大家都可以使用,这种封装起来可以直接调用的函数就称之为库函数。如:scanf,printf,strlen,rand,srand,time,strcmp等。
当然C语言并不直接去实现库函数,而是提供了C语言的标准和库函数的约定。如:scanf函数规定其功能、名字、返回值、参数等,在调用时满足这些约定即可直接使用。注:使用库函数,必须包含 #include 对应的头文件。
库函数的实现一般是由编译器去实现的,如VS2022、gcc,CLion这些编译器。
2.1.2 为什么会有库函数
1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
2.1.3 如何学习库函数
这里我给大家提供一个学习各种函数的网址: C/C++学习网站, 这个网站的好处是大家想学习函数、关键字等知识时可自己进行搜索,类似于MSDN。下图是C语言中库函数对应的一些头文件。
2.1.4 C语言中常用的库函数
C语言中常用的库函数有:
1. IO函数 —— 输入/输出函数(scanf、printf、putchar、getchar...)
2. 字符串操作函数 —— strlen、strcmp、strcpy...
3. 字符操作函数 —— islower(判断字符小写)、isupper(判断字符大写)...
4. 内存操作函数 —— memset、memcmp...
5. 时间/日期函数 —— time...
6. 数学函数 —— sqrt、pow...
7. 其他库函数
下面我来讲解strcpy函数,帮助大家快速掌握上述学习网站并懂得如何学习函数。
strcpy,顾名思义就是字符串拷贝函数,将源地址(source)空间的字符串拷贝到目的地址(destination)的空间上去,并要求目的地址的空间足够大。strcpy返回目的空间的起始地址,在使用时需要调用头文件<string.h>。见如下代码:
#include<stdio.h>
#include<string.h>
int main() {
// strcpy
char arr1[] = "hello nihao"; // 源头
char arr2[20] = { 0 }; // 目的地
strcpy(arr2, arr1);
printf("%s\n", arr2);
printf("%s\n", strcpy(arr2, arr1));
return 0;
}
将字符数组arr1作为源字符串拷贝到目的地址空间arr2中,观察结果知拷贝成功,且strcpy函数返回的是字符数组arr2的起始地址。
注:对于数组,数组名其实是数组第一个元素的地址,也就是起始地址。
大家可以参照这个案例自行进行学习其余的库函数哦。
2.2 自定义函数
在刚了解完库函数后,也许有人不经疑惑:如果库函数能干所有的事情,那还要程序员干什么?这时候自定义函数就显得尤为重要了。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
自定义函数的组成为:
ret_type fun_name ( para1 , * ){statement ; // 语句项}ret_type 返回类型fun_name 函数名para1 函数参数
举两个例子:
案例1:写一个函数可以找出两个整数中的最大值。
#include<stdio.h>
int get_max(int x, int y) {
int z = (x > y ? x : y);
return z;
}
int main() {
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
// 计算
int n = (a > b ? a : b);
printf("自行计算的较大值为:%d\n", n);
// 使用函数进行计算
int m = get_max(a, b);
// 输出
printf("函数计算的较大值为:%d\n", m);
return 0;
}
案例2:写一个函数实现交换两个整型变量的值。
#include <stdio.h>
//实现了函数,但是并不能交换两个整型变量的值
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
//正确版本
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
那为什么案例2中的Swap1不能够实现交换两个变量的内容呢?先让我们一起来了解函数参数和函数调用两个知识点吧。
3. 函数参数
函数的参数又分为实际参数(实参)和形式参数(形参)两种。
3.1 实际参数(实参)
1. 真实传给函数的参数,叫实参。
2. 实参可以是:常量、变量、表达式、函数等。
3. 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
4. 在上述案例2中传给Swap1和Swap2函数的num1、num2就是实际参数。
3.2 形式参数(形参)
1. 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
2. 上述案例2中 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。
下面通过代码来说明实参可以是哪些。
#include<stdio.h>
int get_max(int x, int y) {
int z = (x > y ? x : y);
return z;
}
int main() {
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
// 使用函数来完成
int n = get_max(a, b); // 变量
n = get_max(a, 7); // 变量、常量
n = get_max(a, 2+5); // 变量、表达式
n = get_max(a, get_max(3,9)); // 变量、函数的参数是函数调用
// 输出
printf("较大值为:%d\n", n);
return 0;
}
看最后一个n = get_max(a, get_max(3,9)),其中get_max(3,9)得到的值为9,9与a进行比较,自然是a较大,所以n的最终值为10,代码能够正常编译运行说明实参可以是上述代码中所演示的。
4. 函数调用
4.1 传值调用
1. 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
2. 上述案例2中的Swap1(num1,num2)就是传值调用。
4.2 传址调用
1. 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
2. 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
3. 上述案例2中的Swap1(&num1,&num2)就是传址调用。
5. 回看案例2
#include <stdio.h>
//实现了函数,但是并不能交换两个整型变量的值
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
//正确版本
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
那为什么Swap1不能对两个变量的内容进行交换呢?这是因为Swap1进行的是传值调用,在传值调用时,当实参传递给形参的时候,形参是实参的一份临时拷贝,也就是形参和实参并不是占用同一块内存空间(通过下图代码调试可知),所以对形参的修改不会影响实参。
而在Swap2中,Swap2进行的是传址调用,虽然px、py和num1、num2的地址也不一样,但是在进行传址调用时,num1、num2会将自己内存空间的地址传递给px、py,px、py本质上就是指针,记录着num1和num2的地址(也可以说是指向num1和num2的地址空间),对px、py进行解引用修改值时,本质上就是对num1和num2的值进行修改,因此能够交换两个实参变量的内容。(后续我们会对指针进行详细讲解)
6. 函数的嵌套调用和链式访问
6.1 嵌套调用
#include<stdio.h>
int test() {
int a = 1;
int b = 12;
return a + b;
}
void fun() {
int x = test();
printf("%d\n", x);
printf("haha\n");
}
int main() {
fun();
return 0;
}
上述代码中就是在fun()函数内部进行调用test()函数,在main()函数内部进行调用fun()函数,这就是函数嵌套。
注:函数可以嵌套调用,但是不能嵌套定义(如下)。嵌套定义是不被允许的。
6.2 链式访问
链式访问就是把一个函数的返回值作为另外一个函数的参数。
#include<stdio.h>
int main() {
// 链式访问
printf("%d\n", strlen("abcdefg"));
// printf 的返回值是字符个数
printf("%d ", printf("%d ", printf("%d ", 43))); // 43 3 2
return 0;
}
本篇文章就先讲到这里了,有什么不懂的小伙伴们欢迎评论留言一起探讨。