自己整理的长篇详细学习笔记分享给大家,如有错误,欢迎评论区指正
目录
一、 函数是什么?
C语言中函数的分类
二、库函数
为什么会有库函数
库函数的优点
如何学习库函数
C语言常用的库函数
三、自定义函数
为什么要有自定义函数
函数的组成
自定义函数的使用
比较两数中的较大值
交换两个整形变量的内容
四、函数的参数
五、函数的调用
传值调用
传址调用
六、函数的嵌套调用和链式访问
嵌套调用
链式访问
七、函数的声明和定义
案例分析
函数声明
函数定义
八、函数递归
经典案例
什么是递归?
请看如下案例(用递归解决问题):
递归的两个必要条件
案例再分析
迭代与递归
一、 函数是什么?
在数学中我们就经常使用函数,eg.f(x)=x+1,知道未知数x就可以求得函数值f(x)
那么C语言中的函数呢?维基百科中对函数的定义:子程序
- 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。 - 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
C语言中函数的分类
- 库函数
- 自定义函数
二、库函数
为什么会有库函数
- 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)
- 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)
- 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)
C语言在早期出现的时候,没有库函数,在写代码的时候有很多的功能是经常都要用的,eg.打印,输入等功能。然后人们就在想,能不能自己写一些函数,以便于后期更加方便的使用。后来发现有一些函数是会被频繁的使用,如果想要实现这种功能,如果每个人每次都自己去写效率就太低了,于是就把常用的一些功能用C语言封装成了函数,放在C语言中,就是现在的库函数
库函数的优点
- 提高了开发的效率
- 更加标准
如何学习库函数
- 推荐网站:
- cplusplus.com - The C++ Resources Network
- cppreference.com
- 也可以用线下的工具:MSDN
参照文档去学习库函数,从文档中查取函数的功能,返回值,函数的参数等
C语言常用的库函数
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
我们下面以 strcpy 这个函数为例来介绍一下如何用文档来学习库函数
1.文档中(上面推荐的网站就可以)搜索函数名strcpy
会看到下图:
从文档我们可以看出这是一个字符串拷贝函数,那既然要拷贝字符串,必然涉及拷贝源头和目的地,会发现函数的参数中恰好一个是目的地,一个是源头,这两个参数都是指针类型,返回值也是指针类型,且返回的是目的地的地址。头文件:<string.h>
通过代码来体验一下
#include <string.h>
#include <string.h>
int main()
{
char arr1[] = "hello world";
char arr2[20] = { 0 };
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
运行结果:
再举一个例子:memset函数,这个函数是什么功能呢?
在文档中查找之后:
通过文档了解到这个函数的作用就是将ptr所指向的那一块空间的num个字节设定为指定的值
函数的三个参数:
- ptr: 指向要填充的那个内存块的指针
- value: 设置的那个值
- num: 想要填充value值的空间内存大小
函数头文件<string.h>
通过以下代码体验一下
#include <string.h>
#include <stdio.h>
int main()
{
char arr[] = "hello";
memset(arr, 'a', 2);
printf("%s\n", arr);
return 0;
}
通过这段代码我们来将arr数组里面的前两个元素修改为'a'
代码运行结果:
到这里其实我们就应该做到平时有啥函数不会用,直接查文档学习就行了,掌握库函数的使用方法
注意:使用库函数,必须包含#include 对应的头文件
不需要将库函数全部记住,学会使用文档查询就可以
三、自定义函数
为什么要有自定义函数
我们要知道,库函数是有限的,所能实现的功能就那么多,完全依赖库函数也是不行的,不可能说开发一个软件直接全部使用库函数,如果库函数能干所有的事情,那还要程序员干什么?自己什么也不用干那也是不可能的,一些业务性的代码还是需要自己去写的,那么我们就必须要掌握的一个东西就是自定义函数,所以更加重要的就是自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数。 但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间
函数的组成
ret_type fun_name(para1, *)
{
statement;
}
//ret_type 返回类型
//fun_name 函数名
//para1 函数参数
自定义函数的使用
之前我们在❤️整理2万字带你走进C语言(详细讲解+代码演示+图解)❤️(强烈建议收藏!!!)这篇文章中已经对函数做过一个介绍,在这里我们再详细介绍一下
比较两数中的较大值
首先通过以下代码来体验一下:
前面我们还没学函数的时候,要想比较两个函数的较大值,都是通过if语句来做的,如下代码:
#include <stdio.h>
int main()
{
int x = 0;
int y = 0;
printf("请输入两个数:>\n");
scanf("%d %d", &x, &y);
if (x > y)
{
printf("%d\n", x);
}
else
{
printf("%d\n", y);
}
return 0;
}
现在在这里我们用函数来实现一下:
#include <stdio.h>
int get_max(int x, int y)
{
if (x > y)
{
return x;
}
else
{
return y;
}
}
int main()
{
int x = 0;
int y = 0;
printf("请输入两个数:>\n");
scanf("%d %d", &x, &y);
int ch = get_max(x, y);
printf("%d\n", ch);
}
运行结果:
分析:
这里我们自己定义了一个get_max函数,向它传进去两个参数(即我们想要比较的两个数),在这个函数内部,还是通过if语句来判断大小,将较大值作为返回值返回,所以这里这个函数的返回类型应该是整型,我们通过整型变量ch来接受这个返回值,并在后面用printf函数打印较大值
再看下面的代码:
void test()
{
printf("success\n");
}
int main()
{
test();
return 0;
}
运行结果:
分析:
这段代码中我们自己定义了一个测试函数,函数里面没有参数,函数也没有返回值,但这并没有什么影响,所以我们应该知道,函数可以没有参数,也可以没有返回值,自己视情况而定
交换两个整形变量的内容
写一个函数将x和y的值交换
#include <stdio.h>
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;
}
分析:
这里我们自定义一个交换函数Swap1来交换两数,调用函数并传参,直接将num1和num2传给交换函数,交换函数在函数内部完成两个数的交换,然后又去将num1和num2打印出来,代码没有任何问题
我们看运行结果:
这里会发现num1的值和num2的值并未交换,出了bug
原因分析:
这里传参传的是num1与num2的值,在函数Swap1中用形参x和y来接受传过来的值,并将x和y做了交换,但是要知道,这只是单纯的交换了x和y,并未对num1和num2产生影响,可以通过VS2019的监视器窗口来调试代码再看看
通过监视器也可以看到num1和num2的值并未交换,而只是交换了x与y的值
可以再通过下图理解一下
我们在将下面的函数的参数学完之后再来修改这个代码
四、函数的参数
- 实际参数(实参)
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类
型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参
- 形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配
内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在
函数中有效。
注意:
- 当实参传给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参
接下来我们再返回去看前面的代码:交换两数
分析:
要知道num1与num2和上面的x,y没有任何联系,所以形参的修改是不会实参的。那我们能不能让他们建立联系呢?答案是肯定可以的。我们在❤️整理2万字带你走进C语言(详细讲解+代码演示+图解)❤️(强烈建议收藏!!!)这篇文章中已经对指针有过一定的了解了,创建指针的本质目的不是为了存地址,而是希望后面能够通过指针来访问变量,所以这个地方我们可以通过指针来让它们之间建立联系。通过指针就能找到它所指向的那个对象
#include <stdio.h>
void Swap2(int* px, int* py) //不需要返回值,所以用void
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
分析:
这里我们调用Swap2函数的时候传参传的是指针(num1和num2的地址),以便于在Swap中能够修改实参的值,Swap2函数中用指针来接收,并通过间接访问的方式成功交换两数
运行结果:
上面Swap1和Swap2函数中的参数x,y,px,py 都是形式参数。在main函数中传给Swap1的num1,num2和传给Swap2函数的&num1,&num2是实际参数。
这里可以看到Swap1函数在调用的时候,x,y拥有自己的空间,同时拥有了和实参一模一样的内容。所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝
通过交换两数这个案例我们感受到了传的参数分别是值和指针时的不同,可以来总结一下:
五、函数的调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参
传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
温馨提示:
函数的内容一般我们不在函数内部打印,让函数的功能更加独立(提高函数的复用性)
六、函数的嵌套调用和链式访问
嵌套调用
函数和函数之间可以有机的组合的
嵌套调用就是某个函数调用另外一个函数(即函数嵌套允许在一个函数中调用另外一个函数)
如下代码:
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line(); //嵌套调用,调用new_line()函数
}
}
int main()
{
three_line();
return 0;
}
运行结果:
链式访问
把一个函数的返回值作为另外一个函数的参数
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(arr);
printf("%d\n", ret);
return 0;
}
分析:这段代码我们在第一篇文章中有讲过,用strlen求数组中字符串的长度,用ret来接受返回值,并将ret打印出来,那通过链式访问我们可以直接这样来做:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
printf("%d\n", strlen(arr));
return 0;
}
直接将strlen函数的调用放在原来ret的位置,将strlen函数的返回值作为了printf函数的参数
运行结果:
接下来我们来看下面这个代码:
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
仔细想一下,运行结果是什么呢?
分析:
这段代码中是三个printf的链式访问,执行的时候,肯定是先调用最内层的函数,打印出来43,然后,再调用中间的printf,它要打印的值是printf("%d", 43) ,而printf这个函数的返回值是什么呢?我们可以查一下文档发现printf这个函数的返回值其实就是它所要打印的字符总个数(如果发生错误的话会返回负值),那么最内层的printf的返回值就应该是43的字符个数2,所以中间的printf打印的就是2,返回值为1,最外层的printf打印1,最终结果应该是4321
运行结果:
七、函数的声明和定义
案例分析
先来一段我们之前写过的代码:
代码1:这段代码我们前面运行过,没有任何问题
#include <stdio.h>
int get_max(int x, int y)
{
if (x > y)
{
return x;
}
else
{
return y;
}
}
int main()
{
int x = 0;
int y = 0;
printf("请输入两个数:>\n");
scanf("%d %d", &x, &y);
int ch = get_max(x, y);
printf("%d\n", ch);
}
代码2:将定义的get_max()函数放在main函数的下面
#include <stdio.h>
int main()
{
int x = 0;
int y = 0;
printf("请输入两个数:>\n");
scanf("%d %d", &x, &y);
int ch = get_max(x, y);
printf("%d\n", ch);
}
int get_max(int x, int y)
{
if (x > y)
{
return x;
}
else
{
return y;
}
}
编译代码2
我们会发现代码2编译的时候编译器会报一个警告:“get_max函数未定义”,这是怎么回事呢?
这是因为我们之前写的时候都是在main函数的前面定义函数的,编译的时候,前面的函数定义编译器已经扫过去了,就知道已经有这个函数的存在了或者说这个函数已经定义了,声明了,在main函数里面调用这个函数,编译器也不会有任何问题;
而如果将函数定义放在main函数的下面,编译的时候在main函数立案看到get_max()这个函数完全不知道它,就会报警告
我们在课本上会经常看到以下写法:
代码3:
#include <stdio.h>
int get_max(int x, int y); //声明函数
int main()
{
int x = 0;
int y = 0;
printf("请输入两个数:>\n");
scanf("%d %d", &x, &y);
int ch = get_max(x, y);
printf("%d\n", ch);
}
int get_max(int x, int y)
{
if (x > y)
{
return x;
}
else
{
return y;
}
}
在main函数前面对自定义函数进行了声明----首先告诉编译器,有一个函数名字叫get_max,有两个参数都是int类型,返回类型为int
函数声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关
紧要 - 函数的声明一般出现在函数的使用之前。要满足先声明后使用
- 函数的声明一般要放在头文件中的
函数定义
函数的定义是指函数的具体实现,交待函数的功能实现
注意:
前面的代码3这个地方的函数声明从语法方面来将没有任何问题,但是我们一般都不会这么去写,因为其实函数声明主要不是在这里用的,一般情况下,在一个工程中,有3个最基本的文件,测试文件(.c),头文件(.h),功能模块文件(.c)
函数声明案例
我们继续以上面的案例为例来介绍函数声明,创建test.c,test.h,function.c这三个文件,函数声明放在test.h里面,函数的定义放在function.c里面,在test.c里面完成测试,
如下图:
要注意:在使用function.c里面的函数的时候一定要包含自己的头文件test.c(用双引号),函数声明要放在函数使用之前
八、函数递归
经典案例
首先请看如下代码:
#include <stdio.h>
int main()
{
printf("hello\n");
main();
return 0;
}
这段代码我们会发现main函数在自己调用自己,理论上这会死循环下去
运行结果:
程序本来是main函数在不断的自己调用自己,在最后挂掉了,下面我们正式学习递归
什么是递归?
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可
描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在
于:把大事化小
请看如下案例(用递归解决问题):
接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:1234,输出 1 2 3 4
分析:
这里我们要用递归的话,首先要自己定义一个函数print来打印1234的每一位
想得到1234的每一位
1234%10==4 1234/4==123
如果这个数大于9那就说明它是一个两位数,需要拆解
比如要打印1234,那我们可以这样:
先将123打印出来,再打印4
要打印123,先打印12,再打印3
要打印12,先打印1,再打印2
代码实现:
#include <stdio.h>
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 1234;
print(num);
return 0;
}
运行结果:
图解:
递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续
- 每次递归调用之后越来越接近这个限制条件
案例再分析
我们现在再回过头看开始的案例:main函数自己调用自己,程序运行到一般自己挂了,这是什么原因呢?
首先我们通过下图来了解一下计算机内存区,在C语言的学习中我们一般将内存划分为栈区,堆区,静态区
我们来调试一下程序会出现下图:
main函数在调用是会在栈区开辟空间,上面的案例代码就是在一个main函数还未调用结束时又重新调用main函数,这就会一直不停的在栈上开辟空间,栈上的内存空间也是有限的,当栈空间被耗尽的时候就会导致栈溢出的问题,如上图所示。所以说写递归必须遵循上面提到的递归的两个必要条件
迭代与递归
- 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰
- 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些
- 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销
--------------------------------------------------------------------------------
-------------------------C语言函数部分完结-----------------------------
关于C语言,每个知识点后面都会单独写博客更加详细的介绍
欢迎大家关注!!!
一起学习交流 !!!
让我们将编程进行到底!!!
--------------整理不易,请三连支持------------------