👦个人主页:Weraphael
✍🏻作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、C语言中函数的分类
- 1.1 库函数
- 1.2 自定义函数
- 1.3 void在自定义函数中的应用
- 二、简单介绍两个库函数
- 2.1 实现字符串拷贝(strcpy)
- 2.2 设置内存(memset)
- 三、函数的参数
- 3.1 实际参数(实参)
- 3.2 形式参数(形参)
- 四、函数的调用
- 4.1 传值调用
- 4.2 传址调用
- 五、函数的嵌套调用和链式访问
- 5.1 嵌套调用
- 5.2 链式访问
- 5.3 笔试题
- 六、函数的声明和定义
- 6.1 函数的声明
- 6.2 函数的定义
- 6.3 声明和定义的拓展
- 七、递归
- 7.1 什么是递归
- 7.2 递归的两个必要条件
- 7.3 递归练习
- 八、迭代及练习
一、C语言中函数的分类
在C语言中,函数分为库函数和自定义函数。
1.1 库函数
- 我们知道在学习C语言编程的时候,总是在一个代码写完之后就迫不及待想知道结果,想把结果打印到屏幕上,这时候我们就会频繁的使用
printf
函数。(printf
)- 在编程的过程中我们会频繁的做一些字符串的拷贝工作。(
strcpy
)- 在编程中,我们有时也会计算
n
的k
次方这样的运算。(pow
)以上的代码在开发的过程中每个程序员都可能用到的,为了支持可移植性和提高程序效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
那我们应该如何学习C语言呢?这里给大家一个网站(以往也提到过),可以帮我们深入了解库函数:点击跳转
但是库函数必须知道的一个秘密就是:使用库函数,必须包含#include
对应的头文件。
1.2 自定义函数
如果库函数能干所有事情,那还需要程序员干什么?所以自定义函数就显得尤为重要。自定义函数和库函数一样,有函数名,返回类型和函数参数。但是不一样的是这些都需要我们自己来设计,这个程序员一个很大的发挥空间。
例如写一个自定义函数来找出两个整数之间的最大值。
#include <stdio.h>
int find_max(int x, int y)
{
if (x > y)
return x;
else
return y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int Max = find_max(a, b);
printf("最大值是:%d", Max);
return 0;
}
再举一个例子:写一个自定义函数可以交换两个整型变量的内容。
我想大部分初学者一定会这么写:
#include <stdio.h>
void swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
printf("a = %d\nb = %d\n", a, b);
return 0;
}
但是结果却和我们想的不太一样,当输入10和20,会发现交换后和交换前一模一样
我们可以通过监视来找程序错误:
大家注意看a
,x
,b
和y
的地址,a
对应x
和b
对应y
的地址都不一样怎么可能交换的了值呢?
官方说法:a,b叫做实际参数,x和y叫做形式参数,当函数调用时,实参传递给形参,形参就会有自己的空间(地址),所以形参的修改不会影响实参。
因此,可以用指针来建立联系,代码修改后为:
#include <stdio.h>
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(&a, &b);
printf("a = %d\nb = %d\n", a, b);
return 0;
}
【程序结果】
代码详解:把a
的地址交给x
,b
的地址交给y
,此时x
中存的就是a
的地址,y
中存的是b
的地址,所以*x
就是a
,*y
就是b
,这里运用到指针中的解引用。对于指针不熟的同学们可以看看这篇博客:点击跳转
1.3 void在自定义函数中的应用
#include <stdio.h>
void tmp(void)
{
printf("hello world!\n");
}
int main()
{
tmp();
return 0;
}
代码详解:假设自定义函数tmp
,但是我不给它传任何参数,所以可以在定义函数括号里写上void
,表明这个函数在调用时不能传参,现在只希望函数内部打印hello world!
,因为不需要任何返回,所以它的返回类型为void
。
二、简单介绍两个库函数
2.1 实现字符串拷贝(strcpy)
【文档描述】
此函数的功能就是把一个空间的字符串拷贝到另一个空间去。(注意参数)
【代码样例】
#include <stdio.h>
#include <string.h> //使用strcpy需要包含的头文件
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello world!";
strcpy(arr1, arr2); //数组名就是指针
printf("%s\n", arr1); //%s用来打印字符串
return 0;
}
【程序结果】
2.2 设置内存(memset)
【文档描述】
功能:把
ptr
指向空间的前``num的数据设置成
value`这个值
【代码样例】
#include <stdio.h>
#include <string.h> //memset需要包含头文件
int main()
{
char arr1[20] = "welcome to China";
memset(arr1, 'x', 7); //'x'对应的是ASCII码,是整型
printf("%s\n", arr1);
return 0;
}
【程序结果】
三、函数的参数
3.1 实际参数(实参)
- 真实传给函数的参数,叫实参。
- 实参可以是:常量、变量、表达式、函数等
- 无论实参是何种类型,在进行函数调用时,它们都必须有确定的值,以便把这些值传递给形参。
3.2 形式参数(形参)
形式参数是值函数名后括号的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完之后就自动销毁了。因此形式参数只有在函数中有效。可以简单认为:形参实例化之后其实相当于实参的一份临时拷贝。
四、函数的调用
4.1 传值调用
特点:函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参。
在【2.1自定义函数】写了一个自定义函数来找出两个整数之间的最大值,就是传值调用。
4.2 传址调用
- 传址调用就是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外部的变量建立起真正的练习,也就是函数内部可以直接 操作函数外部的变量。
那【2.1自定义函数】写了自定义函数可以交换两个整型变量的内容,则就是传址调用。
五、函数的嵌套调用和链式访问
当代码写的越来越多时,就会发现,其实一个程序都是由函数组成的,你调用我,我调用你。所以,函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
5.1 嵌套调用
什么是嵌套调用呢?来看看下面的代码:
#include <stdio.h>
void new_line()
{
printf("hello world\n");
}
void three_line()
{
for (int i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
【程序结果】
注意,函数可以嵌套调用,但不能嵌套定义!
那什么是嵌套定义呢?举个例子大家就明白了
#include <stdio.h>
int main()
{
int Add(int x, int y)
{
return x + y;
}
return 0;
}
main
函数也是函数,main
函数只能调用Add
函数,而不能定义在函数内部。
5.2 链式访问
把一个函数的放回值作为另外一个函数的参数。
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d\n", strlen("abcdef"));
return 0;
}
strlen
的返回值做了printf
函数的参数,这就是链式访问。
5.3 笔试题
以下代码输出的结果是什么?
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d",printf("%d", printf("%d",43)));
return 0;
}
【解析】
首先,我们可以在cplusplus这个网站搜索printf
来查看它的返回值:
接下来我们从里向外分析,printf要打印一个整型,而这个整型是来自printf(“%d”, printf(“%d”, 43))的返回值,接下来的printf又要打印printf("%d", 43)
的返回值。
所以首先程序会先打印43,打印完43之后就会打印printf("%d", printf("%d", 43))
的返回值,而43的字符总数是2,所以会在屏幕中打印2,最后再打印整个printf("%d", printf("%d", printf("%d", 43)))
的返回值,也就是1,所以最后会在屏幕上打印4321。
六、函数的声明和定义
6.1 函数的声明
- 告诉编辑器有一个函数叫什么,参数是什么,返回类型是什么,但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足 先声明后使用。
- 函数的声明一般要放在头文件中的。
有的初学者可能会把函数定义写在main
函数后面,但我们要知道编辑器扫描代码是从上往下扫的,当扫到Add(a, b)
发现前面没有见过Add
函数,所以就会报错。
那如何纠正错误呢,只要在main
函数前声明就行了。
6.2 函数的定义
- 函数的定义是指函数的具体实现,交代函数的功能实现。
- 函数的定义也是一种特殊的声明。
像上面刚刚写的代码,可以直接把函数定义放到main
函数前,是不是更加简洁。
6.3 声明和定义的拓展
其实我想告诉大家,实际上函数的声明和定义不是这样用的,上面的定义和声明只是语法展示。真正一个工程中,函数的定义和声明又是如何写的呢?我们接着往下看
比方还是求两个数的和
先新建一个头文件Add.h
,然后再定义一个源文件Add.c
,接下来我把函数的定义放到Add.c
中,对于函数的声明,我放到Add.h
中。如果想使用Add
函数,只需在text.c
中加上#include "Add.h"
即可。(函数的声明一般都放在头文件中,函数的定义(实现)放在源文件中)。注意#include
只包含头文件,库里提供的函数用尖括号,自己写的头文件用双引号。
- 拆成三个文件的好处(了解)
最后,为什么一个.c
文件就可以写完这些代码,而要把它拆成3个文件呢?其实它是有好处的。
①模块化开发(分工)
假设要写一个计算器程序,A程序员写加法,B程序员写减法,C程序员写乘法,D程序员写除法,如果没有多个文件设计,这些程序员都要在text.c中完成,这根本实现不了。有了多文件的设计,能够有效提高效率。
②有利于代码的隐藏
七、递归
7.1 什么是递归
- 程序调用自身的编程技巧称为递归。
- 递归作为一种算法在程序设计语言中广泛应用,一个过程或函数在其定义或说明中直接或间接 调用自身 的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相识的规模较小的问题来求解。
- 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
- 递归的主要思考方式在于:把大事化小。
为了了解递归,首先写一个史上最简单的递归(会发生错误的递归):
main
函数在自己调用自己。但调用着就会发现,程序崩了,它不会一直死递归下去。
同样可以按F10来观察程序
接下来程序就会弹出下面的窗口,stack overflow
:栈溢出。
这就要牵扯到内存中的栈区、堆区和静态了
每调用函数,都会为本次函数,在内存的栈区上开辟一块内存空间。
接下来回到刚刚的代码,每调用一次main
函数就会在栈区开辟一块内存空间,一直开辟总会有一天把栈区给“榨干”了,这时栈就溢出了。
这里为大家推荐两个问答社区网站:
- 这个网站(国外)相当于一个程序员的问答社区:点击跳转
- 思否是国内的程序员问答社区:点击跳转
7.2 递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后就会越来越接近这个限制条件
7.3 递归练习
- 接收一个整型值(无符号),按照顺打印它的每一位。输入:1234,输出:1 2 3 4
【解题思路】
【代码实现】
#include <stdio.h>
void Print(unsigned int x)
{
if (x > 9) //判断两位数
{
Print(x/10);
}
printf("%d ", x % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//%u - 输入无符号值
//写一个函数打印num的每一位
Print(num);
return 0;
}
【画图解释递归】
首先大家要知道,递归其实是两个词,递:递推,归:回归。(很重要!!!)
先递推(黑色),后回归(红色)
- 编写函数不允许创建临时变量,求字符串长度。
首先如果能创建临时变量的话,我们应该怎么写呢?
#include <stdio.h>
int ch_len(char* arr)
{
int count = 0;//计时器
while (*arr != '\0')
{
count++;
arr++;
}
return count;
}
int main()
{
char arr[] = "hello";
int ret = ch_len(arr);
printf("%d\n",ret);
}
【代码详解】
那不允许创建临时变量该怎么写呢?既然讲到了递归,就得用递归来解决。
【解题思路】
求ch_len(“hello”)
,如果第一个字符不是'\0'
,是不是就能转化成1+ch_len(“ello”)
,接下来ch_len(“ello”)
的第一个字符又不是'\0'
,是不是又能转化成1+1+ch_len(“llo”)
,接下来以此类推直到'\0'
。
【代码实现】
int ch_len(char* arr)
{
if (*arr != '\0')
return 1 + ch_len(arr + 1);
else
return 0;//当碰上第一个字符为\0,就返回0;
}
int main()
{
char arr[] = "hello";
int ret = ch_len(arr);
printf("%d\n",ret);
}
【画图解释递归】
先递推(黑色),后回归(红色)
八、迭代及练习
所谓迭代,就是用 非递归 来解决问题。 循环也是一种迭代。
- 求n的阶乘(不考虑溢出)
【解题思路】
首先看看递归实现
【代码实现】
#include <stdio.h>
int Fac(int n) //形参的名字可以和实参一样
{
if (n <= 1)
{
return 1;
}
else
{
return n * Fac(n - 1);
}
}
int main()
{
int n = 0;
//输入n的值
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
【画图分析递归】
递推(黑),回归(红)
【非递归法】
#include <stdio.h>
int Fac(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
return ret;
}
int main()
{
int n = 0;
//输入n的值
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
- 求第n个斐波那契数
【递归解题思路】
【代码实现】
#include <stdio.h>
int Fib(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return Fib(n-1) + Fib(n - 2);
}
}
int main()
{
int n = 0;
//输入n的值
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
【递归图】
实际上,当输入50时,编辑器的光标还在闪烁,有的人可能会认为程序挂掉了,其实并没有,程序此时此刻还在长时间计算第50个斐波那契数
所以这题使用递归还存在局限性,那么接下来我们来尝试迭代(循环)
【迭代解题思路】
【代码实现】
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n>2) //当n = 1或者2时可以不用算,因为结果都是1,
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
//输入n的值
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}