Hello,大家好!!!这里是小周为您带来的呕心沥血之作——C语言秘籍!!
C语言秘籍分为初阶和高阶两部!!跟着小周学定会让你C语言功力大成,称霸武林,话不多说,马上开讲!!!!!
三、函数
1、函数是什么?
数学中我们常见到函数的概念。但是你了解C语言中的函数吗?
维基百科中对函数的定义:子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2、C语言中函数的分类
库函数
自定义函数
2.1 库函数
为什么会有库函数?
我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
理解:C语言中把常用的功能进行了封装,封装成一个一个的函数,提供出来供大家使用,C语言并不去直接实现库函数,而是提供了C语言的标准和库函数的约定,库函数的实现一般是由编译器去实现的
比如 scanf C语言规定了它的功能,名字,参数,返回值
那怎么学习库函数呢?
这里给大家推荐一个网站也是经常出现在之前的讲解中的一个网站
[cplusplus.com – The C++ Resources Network](https://legacy.cplusplus.com/)
简单的总结,C语言常用的库函数都有:
IO函数 输入输出函数 scanf printf putchar getchar
字符串操作函数 strlen strcmp
字符操作函数 islower isupper
内存操作函数 memset memcmp
时间/日期函数 time
数学函数 sqrt pow
其他库函数
我们参照文档,学习几个库函数:
strcpy
char * strcpy ( char * destination, const char * source );
memset
void * memset ( void * ptr, int value, size_t num );
首先我们打开网站
在上方的搜索框中搜索我们要学习的库函数strcpy,然后点击go
在这里大家可以看到库函数的各项信息
首先是我们看这个绿色的小字,图片中已经标注
其次就是这个函数功能的英文介绍
如果说屏幕前的你真的英语看不懂也可以选中之后右键翻译,这个函数的功能是
将源头指向的 C 字符串复制到目标指向的数组中,包括终止 null 字符(并在该点停止)。
英文字体中的斜体字与函数参数是对应的
其次是对函数参数的具体解释
source—是被复制的字符串,destination—是被覆盖的字符串
这个是返回值,告诉我们这个函数返回destination的首地址
然后有列举的例子和输出的样例
红色框框我们点击可以直接跳转运行
在这里可以运行看看,同时我们可以看到其他类似的库函数
我们对strcpy初步的学习就基本完事了,然后我们代码详细说一下
函数的返回值如果我们需要它我们就用对应的类型接收,如果实际情况不需要的话,可以不接受函数返回值
#include #include //库函数的使用要调用头文件int main(){char arr1[] = "good good study";//源头char arr2[20] = "xxxxxxxxxxxxxxxxxxx";//目的地//对于数组,数组名其实是数组第一个元素的地址,也就是起始地址strcpy(arr2, arr1);printf("%s\n", arr2);return 0;}
我们调试来看一看这个\0到底是否复制过来了
我们发现确确实实复制过来了,那到这里是不是对strcpy库函数的学习有了自己的认识呢?
好的我们接着来看memset
首先来看memset的返回类型,以及函数参数,函数的功能是将 ptr 指向的内存块的第一个字节数设置为指定值
这里有英文的介绍,话不多说代码演示
通俗易懂来说就是把数组arr的地址传进去,然后,从首地址开始,将num个位置的值全部更改为x
2.2 自定义函数
那我们说所有如果库函数能干所有的事情,那还要程序员干什么?所以更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
函数的组成:
ret_type fun_name(para1, *){statement;//语句项}ret_type 返回类型fun_name 函数名para1函数参数
一整个{ }是函数体
我们举一个例子:
写一个函数可以找出两个整数中的最大值
int get_max(int x , int y){int z = (x > y " /> num2 ? num1 : num2);//用函数来得到最大值int max2 = get_max(num1, num2);//输出最大值printf("max = %d\n", max1);printf("max = %d\n", max2);return 0;}
3、函数的参数
好,我们接着往下看,再举个例子
写一个函数可以交换两个整形变量的内容
void Swap1(int x, int y){int tmp = 0;tmp = x;x = y;y = tmp;}int main(){int num1 = 1;int num2 = 2;Swap1(num1, num2);printf("Swap1::num1 = %d num2 = %d\n", num1, num2);return 0;}
我们来运行看看
哎?我们发现怎么没有实现交换的功能呢?这不是出现问题了吗?这就要提到接下来讲的两个不同的东西
我们先来调试看看
上述的代码在内存中是这样表示的
3.1 实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
int get_max(int x, int y){int z = (x > y " />
大家可以自己下去运行,看看是否能成功运行
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
3.2 形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数
中有效。
实际参数 - 实参 当实参传递给形参的时候,形参是实参的一份临时拷贝,所以对形参的修改不会影响实参
//形式参数 - 形参void Swap(int* pa, int* pb){int tmp = *pa;//tmp = a*pa = *pb;//a = b*pb = tmp;//b = tmp}int main(){int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;}
这回我们发现我们成功实现了两个数的互换
上述的代码在内存中是这样表示的
这里可以看到 Swap1 函数在调用的时候,x ,y 拥有自己的空间,同时拥有了和实参一模一样的内容。
所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝
4、函数的调用
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。在这种设计中,形参是实参的一份临时拷贝,对形参的修改不会影响实参
4.2 传址调用
通过形参的指针就能够访问到函数外部的变量,并进行操作
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操
作函数外部的变量。
void Swap1(int x, int y)//传值调用{int tmp = 0;tmp = x;x = y;y = tmp;}void Swap2(int* pa, int* pb)//传址调用{int tmp = *pa;//tmp = a*pa = *pb;//a = b*pb = tmp;//b = tmp}int main(){int num1 = 0;int num2 = 0;scanf("%d %d", &num1, &num2);printf("交换前:num1 = %d num2 = %d\n", num1, num2);Swap1(num1, num2);printf("交换后:num1 = %d num2 = %d\n", num1, num2);return 0;}
4.3 练习
在这里找了几个很常见的习题给大家一点时间自己动手做做,相信你一定可以
练习1:写一个函数可以判断一个数是不是素数
#includeint is_prime(int n){int j = 0;for (j = 2; j <= sqrt(n); j++){if (n % j == 0){return 0;}}return 1;}int main(){int i = 0;for (i = 100; i <= 200; i++){//判断i是否是素数if (is_prime(i) == 1){printf("%d ", i);}}return 0;}
练习2: 写一个函数判断一年是不是闰年
int is_leap_year(int y){if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))return 1;elsereturn 0;}int is_leap_year(int y){return((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0);}int main(){int y = 0;for (y = 1000; y <= 2000; y++){//函数怎么使用//TDD - 测试驱动开发//test driven developmentif (is_leap_year(y) == 1){printf("%d ", y);}}return 0;}
练习3: 写一个函数,实现一个整形有序数组的二分查找
int binary_search(int arr[], int k, int sz){int left = 0;int right = sz - 1;while (left<=right){//int mid = (left + right) / 2;int mid = left + (right - left) / 2;if (arr[mid] k){right = mid - 1;}else{return mid;}}return -1;}int main(){int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int k = 7;int sz = sizeof(arr) / sizeof(arr[0]);//二分查找//找到了:返回下标//找不到:返回-1int ret = binary_search(arr, k, sz);if (ret == -1)printf("找不到\n");elseprintf("找到了,下标是:%d\n", ret);return 0;}
练习4: 写一个函数,每调用一次这个函数,就会将 num 的值增加1
方法一:
void test1(int* p){(*p)++;}int main(){int num = 0;test1(&num);printf("%d\n", num);test1(&num);printf("%d\n", num);return 0;}
方法二:
int test2(int n){return (n + 1);}int main(){int num = 0;num = test2(num);printf("%d\n", num);num = test2(num);printf("%d\n", num);return 0;}
5、函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的
5.1 嵌套调用
void test(){printf("请跟着小周好好学习!\n");}void fun(){test();printf("我们一起加油!\n");}int main(){fun();return 0;}
我们可以在函数內部去调用其他已经写好的函数,这就是嵌套调用
5.2 链式访问
把一个函数的返回值作为另外一个函数的参数
#include int main(){//int len = strlen("abcdef");//printf("%d\n", len);//链式访问printf("%d\n", strlen("abcdef"));return 0;}
我们在这里把strlen的返回值作为参数,传给了printf
这样就把字符串abcdef的长度打印了出来
我们再看一个例子
#include int main(){printf("%d", printf("%d", printf("%d", 43)));//结果是啥?//注:printf函数的返回值是打印在屏幕上字符的个数return 0;}
请大家思考一下结果是什么
那为什么结果是4321呢?因为printf函数的返回值是打印在屏幕上字符的个数
printf("%d", 43)这个printf结果是打印43,返回值是2
printf("%d", printf("%d", 43))就相当于printf("%d", 2),打印2,返回值是1
printf("%d", printf("%d", printf("%d", 43)))就相当于printf("%d", 1),打印1,返回值是1
所以结果是4321
6、函数的声明和定义
6.1 函数的声明
告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
函数的声明一般出现在函数的使用之前。要满足先声明后使用。
函数的声明一般要放在头文件中的
//函数声明int Add(int, int);int Add(int x, int y);int main(){int a = 0;int b = 0;//输入scanf("%d %d", &a, &b);//加法int c = Add(a, b);//函数调用//打印printf("%d\n", c);return 0;}//函数的定义int Add(int x, int y){return x + y;}
程序是在main函数开始运行的,准备调用函数时发现在main函数之前没有函数的定义,这时候我们就需要在main函数之前先提前声明函数,告诉计算机,这个函数我有,而函数的声明有两种方式
int Add(int, int);int Add(int x, int y);
函数参数的变量名字可以省略
6.2 函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现
//函数的定义-定义是一种特殊的声明int Add(int x, int y){return x + y;}int main(){int a = 0;int b = 0;//输入scanf("%d %d", &a, &b);//加法int c = Add(a, b);//函数调用//打印printf("%d\n", c);return 0;}
在之前的代码中,大家会发现,我总是会把Add这种函数定义放在main函数的上面,其实定义是一种特殊的声明
函数的声明和定义其实通常不是这样使用的,函数的声明是放在头文件中
那我们将代码分离,我们该怎么写呢?我们再创建一个add.c的文件,一个add.h的头文件
我们将函数声明放在头文件中,函数实现放在add.c中,函数调用放在test.c中,我们在test.c中需要包含头文件,因为add函数的是放在add.h文件里对它进行声明的,我们运行看看
成功运行,这个
#include"add.h"
相当于把 add.h里的东西,拷贝一份放到 test.s文件中
test.c中的add函数来自add.c文件,编译器编译的时候也可以找到这个函数,其他源文件定义的函数在test.c
中的main函数中可以被使用,具有外部连接属性
那我们说,为什么一定要把一段简单的代码拆成三个文件呢?
我们重新创建一个项目
但是add.h中的声明是我需要让买家知道的
我们右键项目的属性打开
将配置类型中的应用程序改为静态库的.lib,然后应用确定,这样就可以把代码编成一个静态库
Ctrl+F5后,
我们生成了一个静态库,我们来找一下
这里的add.lib就是把add.c和add.h经过编译生成的静态库文件,我们用记事本的方式打开这个add.lib
我们发现这文件里面是乱码,为什么呢?这个.lib文件已经是二进制文件了
这时候我们可以将add.lib和add.h一起卖了,add.h中有函数的描述,这时候买家就可以使用了,但是它仍然不知道你代码的实现是什么样的
现在转换视角我们是买家,我们看看买走以后怎么办,我们再创建一个项目,同时打开项目文件夹,将买的add.lib和add.h放到买家文件夹
这时再将买的add.h文件导入,
我们运行发现还是出现了问题,说Add是无法解析的外部符号,Add没有定义啊,只是对函数声明了一下,这就出问题了,实际上不是这么用的
我们依然得在函数使用之前,得让编译器知道有这么个东西
这样就可以成功使用了!!!
7、函数递归
7.1 什么是递归
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问
题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程
序的代码量。
递归的主要思考方式在于:把大事化小
在函数内部调用函数,一直在打印,会导致死递归,导致栈溢出,下图是调试的结果
7.2 练习
7.2.1 练习1
接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4.
#include void print(int n){if (n > 9){print(n / 10);}printf("%d ", n % 10);}int main(){int num = 1234;print(num);return 0;}
相信到了这里思路已经清楚了,我来画一下代码运行的逻辑图
在这里再给大家画一下内存图
7.2.2 练习2
编写函数不允许创建临时变量,求字符串的长度
首先我们先将这个问题简单化,来求字符串的长度
我们来模拟这个strlen函数
int my_strlen(char* s){int count = 0;while (*s != '\0'){count++;s++;}return count;}
好,我们再用递归的思想来看这个问题,将大问题转换成相同类型的小问题
my_strlen("abc")
1+my_strlen("bc")
1+1+my_strlen("c")
1+1+1+my_strlen("")
1+1+1+0
3
//递归int my_strlen(char* s){if (*s == '\0')return 0;elsereturn 1 + my_strlen(s + 1);//指针后移}int main(){char arr[] = "abc";//[a b c \0]int len = my_strlen(arr);printf("%d\n", len);return 0;}
大家下去可以自己尝试尝试画画逻辑图
根据上述得练习我们总结发现,当我们修改了限制条件(下图中的注释处),我们就会发现这个递归会一直进行下去,所以我们说递归要想成功实现,这两个限制条件缺一不可
void print(int n){if (n > 9)//(n){print(n / 10);//(n)}printf("%d ", n % 10);}int main(){int num = 1234;print(num);return 0;}
7.3 递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
7.4 递归与迭代
7.4.1练习3
求n的阶乘。(不考虑溢出)
这里我们用两种写法来实现
//n! = 1*2*3*4...*n//循环(迭代)int Fac(int n){int r = 1;int i = 0;for (i = 1; i <= n; i++){r = r * i;}return r;}//递归int Fac(int n){if (n <= 1)return 1;elsereturn n * Fac(n - 1);}int main(){int n = 0;scanf("%d", &n);int ret = Fac(n);printf("%d\n", ret);return 0;
这个练习很简单,我不再过多讲解,这个代码我们下去在运行的时候会发现,输入50,会发现程序的结果是错误的,那是因为结果超过了int类型的最大值
7.4.2练习4
求第n个斐波那契数。(不考虑溢出)
//求第n个斐波那契数//1 1 2 3 5 8 13 21 34 55 ...//前2个的数的和是第三个数int Fib(int n){if (n <= 2)return 1;elsereturn Fib(n - 1) + Fib(n - 2);}int main(){int n = 0;scanf("%d", &n);int ret = Fib(n);printf("%d\n", ret);return 0;}
思路:
但是我们发现有问题
使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间
为什么呢?
我们发现 fib 函数在调用的过程中很多计算其实在一直重复。我们可以通过计数来看一下单独是n=3时计算了多少次
int count = 0;int Fib(int n){if (n == 3)count++;if (n <= 2)return 1;elsereturn Fib(n - 1) + Fib(n - 2);}int main(){int n = 0;scanf("%d", &n);int ret = Fib(n);printf("%d\n", ret);printf("count = %d\n", count);return 0;}
我们看count,是一个很大很大的值
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出
那我们如何改进呢?
将递归改写成非递归
int Fib(int n){int a = 1;int b = 1;int c = 1;//5 4 3while (n >= 3)//从3开始往后才需要换位置{c = a + b;a = b;b = c;n--;}return c;}int main(){int n = 0;scanf("%d", &n);int ret = Fib(n);printf("%d\n", ret);//printf("count = %d\n", count);return 0;}
思路:
这样是不是就一下子明朗了呢?
提示:
许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
好啦!!!学到这你已经很厉害了!!!给自己竖一个大拇指!!!你是最棒的!!!今天小周就带大家学到这里。小周写一篇高质量详细的博客也也花了好多时间,看到这里请动动你的小手来为小周点赞推广评论一下吧!!!你们的点赞推广评论是我继续输出高质量博客的动力!!!谢谢大家!!!
欲知后事如何,且听下回分解