一、函数是什么?
在第一站的时候我们已经初步的了解了函数,而且我们在前面的小游戏中也应用到了一部分函数的内容,那么函数到底是什么呢?
下面是函数(子程序)的定义:
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
二、C语言中函数的分类
1.自定义函数
2.库函数
1.库函数
(1)库函数简介
首先我们要思考为什么要有库函数
①我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
②在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
③在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
那么如何学习库函数呢?其实我们在猜数字游戏中已经使用了几次库函数了,这里附上链接 猜数字小游戏,建议大家看完本篇文章后再次去回顾一下这个小游戏,会有很不一样的收获。而且在这篇文章中已经很多次的提到一个网站了 c语言库函数,在这个网站中,我们进去以后它是这样的
在上面的search中可以搜素想要使用的库函数,但是这个必须得知道函数的名字,比如我们搜索一个strcmp函数
如上图所示,红色方框中的绿色方框是头文件,可以很明显的看到,string.h的头文件变为了灰色,代表着strcmp函数属于string.h这个库里面
而且我们可以直接点击这些头文件,进去以后我们可以在里面找到,这个头文件里面都有哪些库函数,如下图所示,我们点击stdio.h并往下翻,可以看到下图中的内容
我们可以点击这些函数,然后就可以跳转到他们对应的解析上
当然有人看了之后,想起之前使用过windows.h这个库,但是为什么这里没有呢?其实window.h是visual studio提供的,这个网站里面都是c语言标准库的函数。
(2)库函数的分类
我们将库函数大致分为以下几类
①IO函数 也就是input output函数 ,我们见过的有printf,scanf,getchar,putchar等
②字符串操作函数 有strlen,strcmp等
③字符操作函数 大小写转换,字符分类等
④内存操作函数 memcpy ,memmove,memset等
⑤时间/日期函数 有time等
⑥数学函数 pow,sqrt等
⑦其他库函数
(3)举几个学习库函数的例子
①strcpy函数
我们进入网页直接搜索strcpy,其中黑色框中代表的是函数原型,char*是返回类型,strcpy是函数名,括号里面的是参数。
其中参数中,我们destination是目标的意思,第二个参数中const暂时不用去了解,source是源头的意思。并且我们呢函数原型下面的黑体字是拷贝字符串的意思。下面的字的直译是,拷贝这个C字符串,这个被source所指向的字符串,到这个数组,这个被destination所指向的。包含那个null,也就是\0字符。
上图中,parameter是形式参数的意思,destination下面的解释中,pointer是指针的意思,直译是,指针所指向的目标空间被拷贝进去。
上图的意思是返回destination这个指针。
读到这块,相信读者已经明白了,这个函数的意思就是把source所指向的字符串拷贝到destination中,并且把destination返回。
我们来实践一下这个函数吧
#include<stdio.h> #include<string.h> int main() { char arr1[20] = { 0 }; char arr2[] = "hello world"; strcpy(arr1, arr2); printf("%s", arr1); return 0; }
注意,数组名就是首元素的地址。这个代码的内部逻辑是这样的,如下图所示,将下面的数组中每一个元素一个一个的拷贝上去,直到将\0拷贝上去为止。
而且我么这个拷贝字符串函数还可以返回一个char*的指针,如下图所示是这个返回值的演示。
#include<stdio.h> #include<string.h> int main() { char arr1[20] = { 0 }; char arr2[] = "hello world"; char* ret=strcpy(arr1, arr2); printf("%s", ret); return 0; }
这就是我们如何使用一个文档来学习一个函数的方法,当然如果阅读文档不理解的话,这个文档的下面是有一个例子的。可以通过这个例子来加深理解。
②memset
我们同样去搜索一下这个函数
我们英文中有一个单词叫做memory,英文的意思是记忆,但是在计算机的意思就是内存的意思,而set是设置的意思,所以memset大概的意思就是内存设置。而我们继续看下面的黑体字,意思是内存填充。
下面的意思是,设置前num字节的空间,这个空间被ptr指向,把他设置成value这个值
这个中也是这三个参数的介绍
其中:
ptr的介绍是,要指向填充的那个内存
value的意思是要设置成的那个值
num是要设置的字节个数
这代表了返回值就是ptr这个指针
同样的我们举一个例子,假如说我们有一个数组arr里面有"hello world"这个字符串,我们想要将前五个字符改为'x'。我们的代码实现如下
#include<stdio.h> #include<string.h> int main() { char arr[] = "hello world"; memset(arr, 'x', 5); printf("%s", arr); return 0; }
运行结果为
(4)其他的学习库函数工具
下面这个是c/c++的官网
https://en.cppreference.com/w/ (英文版)
https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5 (中文版)
下面是上文使用的网站
https://legacy.cplusplus.com/info/
以及一个软件(可以离线查找函数)
MSDN
这是MSDN的下载安装包
链接:https://pan.baidu.com/s/1lyqyqxH6vgGx8H8UVg_jag
提取码:919h
复制这段内容打开「百度网盘APP 即可获取」
2.自定义函数
库函数是很方便,但是库函数不是万能的,试想一下,如果库函数能干所有的事情,那还要程序员干什么呢?所以更重要的是我们的自定义函数。
自定义函数和库函数一样,都有函数名,返回类型,参数。但是不一样的是这些都需要由我们自己来设置,这就有了很大的发挥空间。
ret_type fun_name(para1, ......... )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
这是我们函数最基本的组成,由返回类型,函数名,函数参数,以及函数体构成。那我们下面举一个简单的例子吧。
我们写一个函数可以求出两个数的较大值
代码实现如下
#include<stdio.h> int get_max(int x, int y) { return x > y ? x : y; } int main() { int a, b; scanf("%d %d", &a, &b); int ret = get_max(a, b);//推演函数的使用场景 printf("%d", ret); return 0; }
在这里我们需要注意一个点,我们看一下下面这段代码,这段代码中有两个void,test前的void代表着这个函数的返回类型是void,参数中写void是代表着一定不需要参数,否则报错,但是如果没有void的话,传参数是不会报错的。
我们在写一个函数交换两个整数的值
在这个地方有很多人都会犯一个经典的错误标准的零分,我们先看这样一段代码
#include<stdio.h> void swap(int x, int y) { int tmp = 0; tmp = x; x = y; y = tmp; } int main() { int a, b = 0; scanf("%d %d", &a, &b); swap(a, b); printf("%d %d",a,b); return 0; }
我们试着运行一下
我们会发现这不符合我们的预期。那么这是为什么呢?
其实,这里这是因为函数传参后,会将其进行一份临时拷贝,也就是说x和y只是把a和b拷贝了一份,但是x,y和a,b是不一样的,改变x和y并不会改变我们的a和b。
如下图所示我们输入了,2和3,但是我们然后我们通过一个函数想要改变他的值,但是最终结果仅仅是x和y交换了,a和b没有交换,我们观察地址就会发现,x和y与a和b地址不一样,所以我们改变x和y就不会改变a和b
所以说x和y其实就是形参,而a和b是实参,也就是说形参不会改变实参
当函数调用时,实参传递给形参,形参仅仅是实参的一份临时拷贝,对形参的修改不会改变实参
那么我们正确的做法是什么呢?其实我们在上面已经提到了地址,所以答案就很明显了,使用指针。
代码实现如下
#include<stdio.h> void swap(int* x, int* y) { int tmp = 0; tmp = *x; *x = *y; *y = tmp; } int main() { int a, b = 0; scanf("%d %d", &a, &b); swap(&a, &b); printf("%d %d", a, b); return 0; }
运行结果如下
可见使用指针就可以完美的解决的问题了。因为使用指针就相当于远程操控解决了
三、函数参数
1.实际参数(实参)
真实传递给函数的参数
实参可以是:变量、常量、表达式、函数等
无论实参是何种类型的参数,在进行函数调用时,他们都必须有确定的值,以便把这些值传给形参。
2.形式参数(形参)
形式参数就是定义函数时候,括号中的变量。形式参数只有在调用的过程中才会实例化(分配内存单元),所以叫形式参数,形式参数当函数调用完成之后就自动销毁了。因此形式参数只有在函数中有效。
在我们上文中讲到自定义函数的时候,我们已经讲解了形参和实参的一些区别,从下图中可以看出, 函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。
所以我们可以简单的认为:形参实例化后相当于实参的一份临时拷贝。
四、函数的调用
1.传值调用
函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参
我们看之前的一个传值调用的例子,如下代码所示,get_max函数直接将a和b的值传给了函数,这叫传值调用。
#include<stdio.h> int get_max(int x, int y) { return x > y ? x : y; } int main() { int a, b; scanf("%d %d", &a, &b); int ret = get_max(a, b);//推演函数的使用场景 printf("%d", ret); return 0; }
2.传址调用
1.传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式
2.这种传参方式可以让函数和函数外边建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
我们举一个传址调用的例子,如下代码所示,我们将a和b的地址传递给了这个函数,然后我们就可以通过解引用远程操控a和b的数据,从而达到交换的目的。
#include<stdio.h> void swap(int* x, int* y) { int tmp = 0; tmp = *x; *x = *y; *y = tmp; } int main() { int a, b = 0; scanf("%d %d", &a, &b); swap(&a, &b); printf("%d %d", a, b); return 0; }
3.几个经典的例子
(1)写一个函数判断一个数是不是素数。
此题难度不大,如下所示,为我们的代码实现。我们使用is_prime函数作为我们判断素数的函数。sqrt的意思是开根号,所需要的头文件是<math.h>
#include<stdio.h> #include<math.h> int 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++) { if (is_prime(i) == 1)//判断是否为素数 { printf("%d ", i);//是素数则打印 } } return 0; }
如下所示为运行结果
(2)写一个函数判断是不是闰年
如下图所示,我们使用了is_leap_year这个函数来判断是不是闰年,但是这要求我们要对闰年的判断规则有所了解,也就是能被4整除且被100不能整除则为闰年,或者能被400整除也是闰年。
#include<stdio.h> int is_leap_year(int n) { if (((n % 4 == 0) && (n % 100 != 0)) || n % 400 == 0) { return 1; } else return 0; } int main() { int y = 0; for (y = 1000; y <= 2000; y++) { if (is_leap_year(y) == 1) { printf("%d", y); } } }
(3)写一个函数,实现有序数组的二分查找
这个二分查找,我们在之前的文章中专门出了一期来详细讲解的二分查找的过程,这里给贴出链接,读者可以前往对于的博客详细了解:分支与循环,经典题目详解(二分查找超详细解读)
了解完二分查找的基本原理以后,我们现在所需要做的就是将二分查找改为一个函数。我们这里给出最终实现代码
在下面的代码中,需要注意的是数组的传参,注意数组的传参我们暂时只需要记住,数组名可以直接传参,并且形式参数只需要给出给出数组的形式即可,不需要数组的元素个数,当然如果想给元素个数的话也是可以的。数组的传参我们后续详细讲解。
#include<stdio.h> int binary_search(int arr[], int k, int sz) { int left = 0; int right = sz - 1; while (left<=right) { int mid = (left + right) / 2; if (arr[mid] < k) { left = mid + 1; } else 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; //找到了就返回下标 //找不到就返回-1 int pos = binary_search(arr, k, 10); if (-1 == pos) { printf("找不到\n"); } else { printf("找到了,下标是:%d\n", pos); } return 0; }
当然呢,这个代码还可以进一步优化,试想一下,如果我不知道数组有多少个元素,或者说数组元素太多,数不过来,不想数了,怎么解决呢?有什么办法吗?我们说,是有的。其实在我们之前介绍过一个操作符,sizeof,用来计算字节的大小,如果我们计算数组的字节大小会发生什么呢?我们来试一下
#include<stdio.h> int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", sizeof(arr)); printf("%d\n", sizeof(arr[0])); return 0; }
下面的代码中,第一个是计算一个数组的字节,第二个是计算一个元素的字节。运行结果如下
我们便得知了,原来,sizeof传一个数组名,会计算整个数组的大小,传首元素,会计算出每个元素的大小,那么数组的长度就可以用这两个进行相除得到。于是经过整理后,源代码可优化为以下
#include<stdio.h> int binary_search(int arr[], int k, int sz) { int left = 0; int right = sz - 1; while (left<=right) { int mid = (left + right) / 2; if (arr[mid] < k) { left = mid + 1; } else 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; //找到了就返回下标 //找不到就返回-1 int sz = sizeof(arr) / sizeof(arr[0]); int pos = binary_search(arr, k, sz); if (-1 == pos) { printf("找不到\n"); } else { printf("找到了,下标是:%d\n", pos); } return 0; }
(4)写一个函数,每调用一次这个函数,就会使num的值+1
此题难度不大应用一个简单的传地址调用即可,下面是代码
void Add(int* p) { *p = *p+1; } int main() { int num = 0; Add(&num); printf("%d\n", num);//1 Add(&num); printf("%d\n", num);//2 Add(&num); printf("%d\n", num);//3 return 0; }
当然这里还会犯一个经典的错误标准的零分
如果将函数中的代码换成这样呢?
#include<stdio.h> void Add(int* p) { //*p = *p+1; *p++; } int main() { int num = 0; Add(&num); printf("%d\n", num);//1 Add(&num); printf("%d\n", num);//2 Add(&num); printf("%d\n", num);//3 return 0; }
运行结果如下
显然不符合预期,那么是什么原因呢?其实,这是因为++的优先级比*的优先级要高,也就是说这个++作用到p上了,而不是*p上,关于优先级的问题,后续会详细讲解。而这道题的修改也很简单,就是给*p加上括号即可,代码如下所示
#include<stdio.h> void Add(int* p) { //*p = *p+1; (* p)++; } int main() { int num = 0; Add(&num); printf("%d\n", num);//1 Add(&num); printf("%d\n", num);//2 Add(&num); printf("%d\n", num);//3 return 0; }
4、代码风格,起名字风格
关于这个代码风格问题,我们在这里先简单的谈一谈。以后会详细的探讨
对于一个程序员来说,代码风格一定要好,该缩进的一定要缩进。这样可以使得代码可读性更高。
然后就是起名字问题,起名字一般使用英文名字,并且我们一般使用两种风格的名字,如下代码所示
is_leap_year //带有下划线的起名字,并且一定是小写 IsLeapYear(int n) //不带下划线,但是单词首字母必须是大写
起名字时候建议使用以上两种风格的名字,不要杂交!!!不要将下划线和大写杂交,也不要中文英文杂交。一定要有一个良好的代码风格和起名字风格。这里推荐一本书《高质量的C/C++编程》
总结
本小节主要讲了库函数的学习方法,以及如何自定义函数,实参和形参的区别,传值调用和传址调用的区别,最后讲了一些代码风格上的问题。本节课对初学者有一定难度,尤其是库函数的学习方法,他不是一日就能学会的,这些库函数也不是背下来的,而是在日复一日的练习代码中慢慢熟悉的。总而言之,需要坚持下去!!!
本站未完,欲知后事,请看下节