zl程序教程

您现在的位置是:首页 >  后端

当前栏目

「C语言精华篇」春眠不觉晓,函数知多少?

C语言 函数 精华 知多少
2023-09-27 14:19:51 时间

🌟 前言

大家好,我是Edison😎

今天是我们C语言系列的第三篇「函数」;

「函数」在C语言中占据着重要的位置,是C语言中的主体和核心,所以它的重要性也就不言而喻了。

Let’s get it!

🛫送给所有正在努力的大家一句话:你不一定逆风翻盘,但一定要向阳而生🌅

「C语言初阶系列」


在这里插入图片描述



本章目标

在这里插入图片描述

1、函数是什么?

数学中我们常见到函数的概念。但是你了解C语言中的函数吗?
 
在计算机科学中,函数是一个大型程序中的某部分代码, 由一个或多个语句块组成。
 
它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
 
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

2、C语言中函数的分类

1、库函数
 
2、自定义函数

🍑2.1 库函数

为什么会有库函数?

1、我们在学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf);
 
2、在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy);
 
3、在编程时,我们有时会计算n的k次方这样的运算(pow)。
 
像上面我们描述的基础功能,它们不是业务性的代码。
 
我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

那怎么学习库函数呢?

这里推荐一个网站:cplusplus
在这里插入图片描述

简单的总结,C语言常用的库函数都有👇

IO函数:输入和输出函数,例如:input/outputprintf/scanfgetchar/putchar等等;
 
字符串操作函数:比如求字符串长度的strlen函数,字符串之间的比较strcmp、字符串拷贝strcpy等等;
 
字符操作函数:比如判断字符的大小写,将小写字母转化为大写字母用toupper、大写转换为小写tolower
 
内存操作函数:内存复制、查找等操作,例如:memcpymemsetmemmovememcmp等等;
 
时间/日期函数:获取时间,例如:time
 
数学函数:开平方sqrt,还有:absfabspow

那我们来参照文档,学习两个库函数

注意:使用库函数,必须包含 #include 对应的头文件

strcpy函数

打开库函数网站cplusplus可以找到strcpy的具体用法:
在这里插入图片描述
可以看到strcpy其实就是字符串拷贝的意思;
 
意思就是:将字符串 strSource 中的字符,复制到字符串 strDestination 中,包括结束字符\0
 
因此在使用这个函数的时候,我们需要向函数参数传入 strDestination (目标数组)和 strSource (源数组)两个字符串,同时该函数的返回值是char*,它会将拷贝后的 strDestination(目标数组)起始地址返回给我们。
在这里插入图片描述
注意使用strcpy时要引用头文件#include <string.h>

📝代码示例

#include <stdio.h>
#include <string.h>

int main()
{
	char arr1[] = "xxxxxxxxx";//目标空间

	char arr2[] = "edison";

	strcpy(arr1, arr2);//将arr2数组的字符串拷贝到arr1数组中

	printf("%s\n", arr1);

	return 0;
}

运行结果👇

在这里插入图片描述
注意:这里的复制其实并不是真正的复制,准确的说是覆盖!
 
即把 arr2 的所有内容(包括\0)都覆盖到 arr1 对应位置;
 
也就是说,虽然打印出来 arr1 是edison,但是实质上 arr1 等于e d i s o n \0 x x \0
 
我们可以调试看一下
在这里插入图片描述
补充: 1、源字符串必须以 \0结束。如果源字符串没有\0,就会一直拷贝源字符串地址后面的所有内容,直到找到\0为止;
 
2、也会将源字符串中的 \0拷贝到目标数组中;
 
3、目标空间必须足够大,以确保能存放源字符串;如果目标空间不够大,则会导致源字符串拷贝不进去。
 
4、目标空间必须可变,即目标空间没有const修饰;
 
(关于const的用法可以看这篇文章:深入理解const的用法

memset函数

打开库函数网站可以找到memset的具体用法:
在这里插入图片描述
可以看到,这个函数的意思是:把 dest 指向这块空间的前 count 个字节的内容,替换成我们想要的 ch
 
注意使用memset时要引用头文件#include <string.h>

📝代码示例

#include <stdio.h>
#include <string.h>

int main()
{
	char arr[] = "hello Edison";

	int n = 5;

	//把arr1的内容替换成xxxxx Ediosn
	char *ret = (char*)memset(arr, 'x', n);

	printf("%s\n", ret);

	return 0;
}

运行结果👇

在这里插入图片描述
把arr1数组里面的字符串的前5个字符,替换成x
 
我们定义的n=5,这里的5是以字节为单位的;
 
字符'x'对应的ASCII码值就是int类型。

以上就是我们参照文档,学的两个库函数

🍑2.2 自定义函数

如果库函数能干所有的事情,那还要程序员干什么?
 
所有更加重要的是自定义函数
 
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
 
但是不一样的是这些都是我们自己来设计。

自定义函数的定义语法为👇

//自定义函数
ret_type fun_name(para1, *)
{
	statement;//语句项
	
    return 返回值;//该返回值与返回类型必须相同,如果是void型函数,则不需要返回值,因此可以不写。
}

ret_type 返回类型
fun_name 函数名
para1    函数参数

在使用函数的过程中,我们用 函数名(函数参数) 的形式来调用自定义函数;
在这里插入图片描述

这里直接看个例题

写一个函数可以找出两个整数中的最大值。

📝代码示例

#include <stdio.h>

int get_max(int x, int y)
{
	return (x > y) ? (x) : (y); //三目操作符,在操作符中讲过,如果x>y则返回x,反之则返回y;
}

int main()
{
	int a = 20;
	int b = 10;

	int max = get_max(a, b);//定义一个max变量,用来接受最大值;

	printf("max=%d\n", max);
}

运行结果👇

在这里插入图片描述
在程序运行过程中,给 get_max 这个函数传入a、b 这两个参数,函数调用完后会返回a和b中的最大值
 
因此可以用max来接收这个返回值。
 
当然也可以不用接收,因为在函数运行完以后,get_max(a, b)就相当于这个返回值,该返回值可以当做printf的参数可以直接进行打印操作。
 
如下
在这里插入图片描述

再来看一道例题

写一个函数可以交换两个整形变量的内容。

📝代码示例

#include <stdio.h>

void Swap(int* pa, int* pb) //我们只需要交换a和b当中的内容,不需要返回值,所以用void
{
	int t = 0;//定义一个临时变量用于交换
	t = *pa;
	*pa = *pb;
	*pb = t;
}

int main()
{
	int a = 10;
	int b = 20;

	printf("交换前: a=%d b=%d\n", a, b);

	Swap(&a, &b);

	printf("交换前: a=%d b=%d\n", a, b);

	return 0;
}

运行结果👇

在这里插入图片描述

3、函数的参数

函数的参数分为:

1、实际参数,称为实参;
 
2、形式参数,称为形参

🍑3.1 实际参数

真实传给函数的参数,叫实参。
 
实参可以是:常量、变量、表达式、函数等。
 
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

🍑3.2 形式参数

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
 
形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

📝代码示例

#include <stdio.h>

void Swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}

void Swap2(int* px, int* py) //我们只需要交换a和b当中的内容,不需要返回值,所以用void
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}

int main()
{
	int a = 1;
	int b = 2;

	Swap1(a, b);
	printf("Swap1: a=%d b=%d\n", a, b);

	Swap2(&a, &b);
	printf("Swap2: a=%d b=%d\n", a, b);

	return 0;
}

运行结果👇

在这里插入图片描述
Swap1函数中的x、y,和 Swap2函数中的px、py,都叫做形式参数
 
main函数中传给Swap1函数的 a,b 和 传给Swap2函数的 &a,&b,叫做实际参数
 
这里我们可以通过调试,对函数的实参和形参进行分析:
在这里插入图片描述
可以看出:
 
实参a、b形参x、y不是同一空间;
 
这里可以看到Swap1函数在调用的时候,x,y拥有自己的空间,同时拥有了和实参一模一样的内容。
 
所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。

4、函数的调用

函数的调用分为:

1、传值调用
 
2、传址调用

🍑4.1 传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。

📝代码示例

#include <stdio.h>

void Swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 10;
	int b = 20;

	Swap1(a, b);
	printf("Swap1: a=%d b=%d\n", a, b);
	              
	return 0;
}

运行结果👇

main函数中,Swap(a, b)中,括号内传的就是a、b的值,也就是10、20
 
那么传值调用,对形参的修改不会影响实参
 
由于形参并不影响实参,函数在调用过程中只是对形参x,y进行了交换,并没有影响到a,b;
 
所以程序运行以后,并不会把a和b的值就行交换
在这里插入图片描述
并且函数在调用结束以后,x,y就已经被销毁了。
在这里插入图片描述

🍑4.2 传址调用

1、传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
 
2、这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

📝代码示例

#include <stdio.h>

void Swap2(int* px, int* py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}

int main()
{
	int a = 1;
	int b = 2;

	Swap2(&a, &b);
	printf("Swap2: a=%d b=%d\n", a, b);

	return 0;
}

运行结果👇

main函数中,Swap(&a, &b)中,括号内传的就是a、b地址
 
那么传址调用,让函数内部可以直接操作函数外的变量;
 
通过这种方式可以使得变量进行真正的交换。
在这里插入图片描述
通过监视可以看出,pxpy就是一个指针,其中放的就是a,b的地址
 
*解引用操作符,它可以通过地址找到地址中存放的变量值
在这里插入图片描述
比如:px就是a的指针,它的值是a的地址,对px解引用就可以找到a地址中存放的变量1,然后我们就可以对变量1进行操作了。
在这里插入图片描述

传值和传址的使用场景

函数内部的形参只需要借用函数外部实参的值的时候用传值调用,比如求两个数的较大值;
 
当函数内部需要对函数外部变量进行操作时用传址调用,比如交换两个数。

5、函数的嵌套调用和链式访问

函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。

🍑5.1 嵌套调用

📝代码示例

#include <stdio.h>

void fun2()
{
	printf("hello\n");
}

void fun1()
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		fun2();
	}
}

int main()
{
	fun1();
	return 0;
}

运行结果👇

在这里插入图片描述
我们通过main函数调用fun1
 
通过fun1调用三次fun2,这就是函数的嵌套调用。
 
注意:函数可以嵌套调用,但是不能嵌套定义。

🍑5.2 链式访问

把一个函数的返回值作为另外一个函数的参数。

📝代码示例

int main()
{

	int len = strlen("abc");

	printf("%d\n", len);

	printf("%d\n", strlen("abc"));//链式访问
}

运行结果👇

在这里插入图片描述

下面我们看一个有趣的代码🎃

#include <stdio.h>

int main()
{
    printf("%d", printf("%d", printf("%d", 43))); //结果是啥?
    
    //注:printf函数的返回值是打印在屏幕上字符的个数
    return 0;
}

运行结果👇

在这里插入图片描述
那么如何理解这段代码呢?
 
这个程序实际上就是用printf的返回值作为printf的参数;
 
因此想要弄明白这个程序,我们得先知道printf的返回值;
在这里插入图片描述
所以printf函数返回的是打印在屏幕上的字符的个数
 
代码分析:1、最内层的printf打印43;
 
2、第二层的printf打印的是最内层printf的返回值,也就是43这个内容的元素个数2;
 
3、最外层打印的是printf("%d", 2)的返回值,返回值是其元素个数1;
 
4、最后屏幕会打印出4321这四个数。

6、函数的定义和声明

🍑6.1 函数的声明

1、告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
 
2、函数的声明一般出现在函数的使用之前。要满足先声明后使用
 
3、函数的声明一般要放在头文件中的。

🍑6.2 函数的定义

函数的定义是指函数的具体实现,交待函数的功能实现。

这里我们可以简单的说一下

add.h

放置函数的声明

#ifndef __TEST_H__
#define __TEST_H__

//函数的声明
int Add(int x, int y);

add.c

放置函数的实现

#include "test.h"

//函数Add的实现
int Add(int x, int y)
{
	return x + y;
}

test.c

main函数里面去调用这个函数,要使用之前,只需要包括头文件:#include <add.h>即可;

#include <stdio.h>
#include "add.h"

int main()
{
	int a = 10;
	int b = 20;
	
	int ret = Add(a, b);
	
	printf("%d\n", ret);
	return 0;
}

这种分文件的书写形式,在后期写三字棋和扫雷的时候,就可以用分模块来写。

7、函数递归

🍑7.1 什么是递归?

递归就是程序自己调用自己的过程;
 
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解;
 
递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
 
递归的主要思考方式在于:把大事化小

🍑7.2 递归的条件

存在限制条件,当满足这个限制条件的时候,递归便不再继续。
 
每次递归调用之后越来越接近这个限制条件。

这里我们通过例题来详解函数的递归

📃练习一

接受一个整型值(无符号),按照顺序打印它的每一位。
 
输入:1234
 
输出:1 2 3 4

🧐思路

我们想要从高位向低位输出,就必须要依次获取:最高位到最低位的数字;
 
因此我们可以用1234除以10,并把1234 / 10的结果进行判断,看其是否小于10
 
如果小于10则不再进行除以10的操作,这样我们就可以得到最高位1了;
 
为了获取其他位的数字,我们可以在每次除以10之前进行取模10的操作,得到剩下的位。
在这里插入图片描述

📝代码示例

#include <stdio.h>

void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}

int main()
{
	int num = 1234;
	print(num);

	return 0;
}

运行结果👇

在这里插入图片描述

📃练习二

编写函数不允许创建临时变量,求字符串的长度。

这道题就交给你们😘

🍑7.3 递归与迭代

迭代就是重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。
 
迭代可以理解成循环。

📃练习三

求第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;
	scanf("%d", &n);

	int ret = Fib(n);
	printf("%d\n", ret);

	return 0;
}

运行结果👇

我们算第10个斐波那契数的结果
在这里插入图片描述
计算成功了!!!

但是我们发现有问题

在使用 Fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
 
为什么呢?
 
我们发现 Fib 函数在调用的过程中很多计算其实在一直重复。
在这里插入图片描述
而且还占用了大量的内存空间;
 
计算的规律就是: 2 n − 1 2^n-1 2n1次;

那我们如何改进呢?

在调试函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。
 
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出
 
解决上述的问题的话,我们可以将递归改写成非递归
在这里插入图片描述

📝代码改进

#include <stdio.h>

int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;

	while (n > 2)
	{
		a = b;
		b = c;
		c = a + b;
		n--;
	}
	return c;
}

int main()
{
	int n = 0;
	scanf("%d", &n);

	int ret = Fib(n);
	printf("%d\n", ret);

	return 0;
}

运行结果👇

先计算第10个斐波那契数
在这里插入图片描述
再计算第50个斐波那契数
在这里插入图片描述
可以看到非常快的就计算出来了
 
这里之所以是负数,是因为我们题目说了,不考虑溢出的情况,所以不要在意;
 
这下子,我们的这个程序就已经优化的很好了

提示:

1、许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
 
2、但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
 
3、当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开
销。

🌟总结

🤗作者水平有限,如有总结不对的地方,欢迎留言或者私信!
 
💕如果你觉得这篇文章还不错的话,那么点赞👍、评论💬、收藏🤞就是对我最大的支持!
 
🌟我是Edison,你知道的越多,你不知道越多,我们下期见!
在这里插入图片描述