- 前言
- 库函数
- 自定义函数
- 函数的参数
- 函数的调用
- 函数的声明
- 函数递归
在计算机科学中,函数是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。而C语言中,对于函数的分类有库函数和自定义函数。
库函数像一些基础功能或常用功能,它们不是业务性的代码,我们在开发的过程中每个程序员都可能用的到,
为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员
进行软件开发。如我们之前使用的最多的 printf 函数等等都是属于库函数。那怎么去学习C语言中的库函数呢?这里推荐一个网站:https://cplusplus.com/reference/
简单分类,C语言常用的库函数都有以下几种。
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
例如想学习 strcpy 函数,可以在网站直接搜索,然后阅读英文文档。此函数大致意思是将第二个参数中的字符串复制到第一个参数的字符串中,且将’ ’一样复制(null character)。
下面自己使用以下这个函数试验一下,请看代码,最后输出abcdef,为了验证猜想,不妨进行调试,可以看到,确实将字符串2复制到字符串1中了(包括’ ’),故最后输出是abcdef。
#define _CRT_SECURE_NO_WARNINGS 1 #include#include int main() { char arr1[] = "abcdef"; char arr2[20] = "ssssssssss"; strcpy(arr2,arr1); // 也被拷贝 printf("%sn",arr2); //abcdef return 0; }
下面学习 memset 函数,大致意思是从起始地址开始填充整形(字符),第一个参数是起始地址,第二个参数是填充的内容,第三个参数是填充的个数(以字节为单位,size_t 指无符号整形)。
下面开始具体代码演示,“Hello World” 变成 “XXXXX World”。
#define _CRT_SECURE_NO_WARNINGS 1 #include#include int main() { char arr1[] = "Hello World"; //以字节为单位 memset(arr1,'X',5); printf("%sn", arr1); //XXXXX World return 0; }
总结: C语言中的库函数都可以通过看文档 + 代码演练的形式学习,要学会多看英文文档,且使用库函数必须包含 #include 对应的头文件。
自定义函数库函数不能做所有的事情,所以需要我们自定义函数,用来完成某项功能,自定义函数和库函数一样,有函数名,返回值类型和函数参数。
下面举一个例子:写一个函数可以找出两个整数中的最大值。请看代码,这就是自定义函数。
#includeint max(int a , int b) { return a > b ? a : b; } int main() { //写出一个函数 求两个整数的较大值 int a = 10; int b = 15; int m = max(a,b); printf("%dn",m); return 0; }
下面再举一个例子:写一个函数交换两个整形变量的值。请看代码。
#includeint swap(int x,int y) { int temp = x; x = y; y = temp; } int main() { int a = 15; int b = 10; printf("交换前:a = %d b = %dn",a,b); swap(a,b); printf("交换前:a = %d b = %dn",a,b); return 0; }
第一眼看上去感觉没有什么问题,我们来运行以下,下面是输出结果,发现 a 和 b 的值并没有发生变化,这是为什么呢?
当我们调试程序的时候,问题就可以解答了,我们发现 x,y 的地址与 a,b 不一样,也就是他们在内存中使用的是不同的空间,当传参过去,a ,b 的值赋给了 x,y ,此时在内存中开辟新的空间,本质上是这新开辟的空间中的变量进行交换,等到函数执行结束,此局部变量声明周期结束,所以,a,b 并没有进行交换。用一句话来总结原因,那就是:形参实例化之后其实相当于实参的一份临时拷贝。
那么解决的办法是什么呢?其实如果函数的形式参数实例化后,能够通过地址找到原来的变量,并对此内存块中进行交换,问题就可以解决了,所以,函数形式参数改变为接收地址,调用函数的时候传入地址即可。请看代码,函数形参接收传入的地址,并实例化后对其进行解引用操作,这时候再进行交换,就可以成功交换了。
#include函数的参数int swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } int main() { int a = 15; int b = 10; printf("交换前:a = %d b = %dn", a, b); swap(&a, &b); printf("交换前:a = %d b = %dn", a, b); return 0; }
实际参数: 真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数: 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
传值调用: 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用: 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
广义上函数的调用分为传值调用和传址调用,但实际上传址调用也是属于传值调用,只不过此时传的值是地址,但我们传入地址后,能够与函数外部建立联系,所以才会广义上分为这两种调用加以区分。
下面写一些函数的练习题,很多都是做过的基础题。
1.写一个函数可以判断一个数是不是素数。
#includeint isShuShu(int n) { if (n == 1) return 0; for (int i = 2; i <= sqrt(n); i++) { if (n % i == 0) { return 0; //不是的话 返回0 } } return 1; //是的话 返回1 }
2.写一个函数判断一年是不是闰年。
int isLeap(int year) {
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
return 1; //如果是 返回1
}
return 0; //不是 返回0
}
3.写一个函数,实现一个整形有序数组的二分查找。
这里需要注意一下,传参的时候要传一个数组的长度,不然在函数里面无法找到数组的长度,原因是C语言传入数组名作为参数时,本质传入的是首元素的地址,在64位机器上,如果在函数里面使用sizeof(arr) / sizeof(arr[0]) 会得到 8 / 4 = 2 这样的结果。
binary_search(int arr[], int k, int len) {
int left = 0;
int right = len - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] == k) {
return mid;
}
else if (arr[mid] <= k){
left = mid + 1;
}
else {
right = mid - 1;
}
}
return -1;
}
4.写一个函数,每调用一次这个函数,就会将 num 的值增加1。
void add(int* num) {
//*num++//错的
(*num)++;
}
下面介绍一下函数的嵌套调用,函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。请看下面代码,main 函数首先调用 test01 函数,打印"test01",随后在函数内又调用 test02 函数,然后打印"test02",这就是函数的嵌套调用。
#includevoid test02() { printf("test02n"); } void test01() { printf("test01n"); test02(); } int main() { test01(); return 0; }
下面介绍函数的链式访问,就是把一个函数的返回值作为另外一个函数的参数,请看下面的代码,此程序调用 strlen 函数,随后又将 strlen 的返回值传入 printf 函数中。
#include#include int main() { int len = strlen("abcdef"); printf("%dn",len); //6 return 0; }
如果仅仅是想打印这个返回值,那么其实也可以不用创建变量去存储,请看下面代码,直接将这个函数写在参数的位置,其实上还是传入的返回值,这就是链式访问。
#include#include int main() { printf("%dn",strlen("abcdef")); //6 return 0; }
请看下面一道题,分析代码输出结果。printf的返回值是打印的字符个数,最先执行的是最里面的函数,首先打印43,然后返回值作为参数传递给中间函数,打印2,最后的返回值作为最外层函数的参数,所以打印1,最后输出结果是4321。
#include函数的声明int main() { printf("%d",printf("%d",printf("43"))); //4321 return 0; }
当函数写在主函数后面,那我们在使用的时候,编译器会报出警告,原因是程序在编译期间会自上到下依次扫描,当扫描到主函数调用其他自定义函数的时候,编译器会觉得函数未定义,所以会发出警告,这时候需要函数的声明,告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了,函数的声明一般出现在函数的使用之前。要满足先声明后使用。请看下面代码。
#includeint add(int a,int b); //函数的声明 形参的名字可以省略 int main() { int a = 10; int b = 15; int sum = add(a, b); //函数的使用 printf("%dn",sum); return 0; } //函数的定义 int add(int a , int b) { return a + b; }
但实际上我们函数的声明一般放在头文件中,比如上述的add加法功能,我们可以这样写。
在add.h中进行函数的声明
在add.c中进行函数的定义
add.c和add.h合称加法模块,我们只需要在主函数中包含头文件即可使用这个函数,注意是用双引号引入自己写的头文件。请看代码。
#include#include "add.h" #include int main() { //add.c add.h加法模块 int a = 10; int b = 15; int sum = add(a, b); //函数的使用 printf("%dn", sum); return 0; }
补充: 为什么加法模块要分成两个文件夹写?原因是如果不想让别人看到我们实现的函数内部细节,可以用一些手段将函数定义的文件细节隐藏起来,对外提供一个接口,只需告诉别人函数是做什么的,需要传入什么参数,返回值是什么即可,无需暴露自己内部实现的细节。
函数递归通俗来讲,函数自己调用自己就是递归。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归主要的思想是:把大问题化为小问题。
递归的两个必要条件:1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。2.每次递归调用之后越来越接近这个限制条件。
下面请看一个例子,接受一个整型值,按照顺序打印它的每一位。输入:1234,输出 1 2 3 4。
请看代码,如果 n 的值是两位数及以上,那么就进行递归,直到 n 的值变为一位数,那么打印出此时的个位,结束这个函数,返回到上一次调用时的位置,进行第二位的打印…所以可以输出1 2 3 4.
#includevoid print(int n) { if (n > 9) { print(n / 10); } printf("%d ",n % 10); }
下面再看一个例子,编写函数,求字符串的长度。我们的常规思路是遍历这个字符串,用一个变量进行计数,直到遇到’ ’。请看代码。
int my_strlen(char* str) {
int count = 0;
while (*str != ' ') {
count++;
str++;
}
return count;
}
那么,如果题目是编写函数,不允许创建临时变量,求字符串的长度。这时候可以用到递归,将大问题转化为小问题,例如字符串"abcdef"的长度,可以转变思路,把事情化为1+"bcdef"的长度,就可以了,一直递归下去,直到遇到’ ’。
int my_strlen(char* str) {
if (*str == ' ') {
return 0;
}
return 1 + my_strlen(++str);
}
下面再看一道简单题,求n的阶乘。我们可以很容易用递归写出。
#includeint fac(int n) { if (n == 1) return 1; return n * fac(n - 1); }
再来看一道题,求第n个斐波那契数。我们只需要知道斐波那契数列是什么,就可以很容易写出。
#includeint fib(int n) { if (n == 1 || n == 2) { return 1; } return fib(n - 1) + fib(n - 2); }
但是在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间,原因是每次递归都调用出两个函数进行递归,fib 函数在调用的过程中很多计算其实在一直重复。而如果传递的参数过大的话,程序会报错(stack overflow),系统分配给程序的栈空间是有限的,如果参数过大或发生死递归的时候,这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。所以这道题我们可以不用递归的去写程序。请看代码。
int fib(int n) {
int a = 1;
int b = 1;
int c = 1;
for (int i = 0; i < n-2; i++) {
c = a + b;
a = b;
b = c;
}
return c;
}
最后我们来一道难一点的题,使用递归编写一个函数 reverse_string(char * string),将参数字符串中的字符反向排列(不是逆序打印),不能使用C函数库中的字符串操作函数(可以使用strlen)。
例如"abcdef"逆序之后字符数组的内容为"fedcba"。
这道题的思路是两边依次交换,慢慢想中间递归,难点就是每次递归后右边指针指向最右边的位置不好确定(只有第一次比较好确定),所以每次递归前,我们把右边指针指向的字符设置为’ ’,这样就可以利用 strlen 找到右边的指针位置了。请看代码。
#includevoid reverse_string(char* string){ char* left = string; char* right = left + strlen(string) - 1; char temp = *left; *left = *right; *right = ' '; if(strlen(string) >= 2){ reverse_string(left+1); } *right = temp; }
到这里本章函数的内容全部结束了,下一章将对数组做一个详细介绍,请朋友们多多点赞关注。



