【C语言】从入门到入土(函数篇)

前言:
本篇为你讲解掌握函数的基本使用和递归。

C语言函数是一种函数,用来编译C语言,一般包括字符库函数,数学函数,目录函数,进程函数,诊断函数,操作函数等。“函数”是从英文function翻译过来的,其实 function在英文中的意思即是“函数”,也是“功能”。从本质意义上来说,函数就是用来完成一定功能的。


1. 函数是什么

数学中我们常见到函数的概念。但是你了解C语言中的函数吗?在C语言中,函数是十分重要的,在我们写代码的过程中,是离不开函数的。包含我们写的main函数它也是函数。

维基百科中关于函数的概念:

1.在计算机科学中,子程序是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。

2.一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

函数提供对过程的封装和细节的隐藏,可以让我们的代码更加的简洁,比如说printf,scanf这种输入输出函数,它的底层代码是什么样子的,我们不需要关注,我们只需要使用就可以了。使用函数可以减少冗余代码,代码标准化。

而C语言中的函数分类我们可以简单分为

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

2. 库函数

为什么要有库函数?

我们先来举个例子,我们知道在我们学习c语言的过程中,每次写出来的代码都是迫不及待想知道结果是否正确,那就得打印出来确认,那么我们就需要用到库函数printf打印;而我们需要计算n的k次方的时候,键盘是也没有次方的符号,那就可以用到库函数pow来计算次方。

所以我们很多时候都需要使用到的基础功能,他们不是业务性的代码,是我们在开发过程中每个程序员都有可能用到的。为了支持可移植性和提高效率,使用编译器 提供一系列的库函数,方便程序员软件开发。

但是注意:

C语言规定了一些库函数,规定了其函数名,参数标准,返回值类型,函数功能等,但库函数本身不提供库函数,库函数是由编译器的厂商提供的,比如VS,gcc等编译器。而且不同的编译器中,相同库函数的细节上可能有所差异,但功能上是差不多的。

那么如此丰富的库函数,我们该如何进行学习呢?

这里有一个链接,里面有丰富的库函数的解释和说明:
点这里!!! cplusplus.com

在这里插入图片描述

学习库函数的方法:

  1. 看库函数的说明解释
  2. 看举例说明中的运用
  3. 在编译器中使用起来

我们可以试着来学习一个库函数:strcpy

首先进入链接,搜索strcpy库函数:
在这里插入图片描述
然后我们可以看见关于strcpy的一些内容,有他的名字,作用,以及作用的详解,拷贝source的字符串到destination里面:
在这里插入图片描述
这时候就有人觉得哇我不会英语啊,但是很多时候页面翻译,还有一些翻译的软件都是十分方便的,我们可以通过他们的译文来理解,这样才可以进步。

然后接下来看见的是它的形参的内容,将内容覆盖掉。还有就是他的返回类型,这里说明他返回的是目标空间的(destination)起始地址 :
在这里插入图片描述
最后还可以看见他的举例,创建几个字符数组,然后初始化数组,调用strcpy()函数将str1的内容拷贝到str2上面,或者将直接输入的" "里面的内容拷贝进str3,还要注意这里的引头文件中引了string.h:
在这里插入图片描述
然后除了例子我在哪里可以看见我需要引的头文件呢,别急,板块旁边一样是有的:

在这里插入图片描述

然后我们可以自己尝试一下使用这个函数:

#include <string.h>
int main()
{
	char arr1[] = "heoooooo!";
	char arr2[20];

	strcpy(arr2, arr1);
	printf("%s\n", arr1);
	printf("%s\n", arr2);
	return 0;
}

然后我们刚刚还说到,strcpy库函数的返回值是目标空间也就是复制过去的空间的起始地址,所以我们接收返回值是不是也可以打印结果呢?

#include <string.h>
int main()
{
	char arr1[] = "heoooooo!";
	char arr2[20];

	char* ret = strcpy(arr2, arr1);
	printf("%s\n", arr1);
	printf("%s\n", arr2);
	printf("%s\n", ret);
	return 0;
}

答案是可以的,所以我们可以慢慢的了解去使用库函数。

那常见的库函数有:
在这里插入图片描述
库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。对照文档来学习上面几个库函数,就可以掌握库函数的使用方法了。


3. 自定义函数

那么有那么多的库函数,为什么还要自定义函数,肯定是需要的!如果库函数可以做所以的事情,那还需要程序员干嘛?所以自定义函数也很重要。

自定义函数和库函数一样,有函数名,返回值类型和函数参数。 但是不一样的是这些都是由我们自己来设计。这给程序员一个很大的发挥空间。
在这里插入图片描述
自定义函数的结构:

ret_type fun_name(para1, * )
{
 statement;//语句项
}
//ret_type 返回类型
//fun_name 函数名
//para1    函数参数

我们来举个栗子就很容易理解啦:

//实现计算两数相加的和

int jia(int a, int b)//创建函数
//返回类型int 函数名jia  (函数参数创建int a,int b)
{
	int sum = 0;
	sum = a + b;//语句块
	return sum;
}

int main()
{
	int a = 20;
	int b = 10;
	int sum = jia(a, b);//调用函数,传入参数
	printf("%d", sum);
	return 0;
}

其实,自定义函数就是我们将一个功能写好后封装在一个函数内,当我们需要用的时候调用它就可以了。这大大简化的main函数中的内容,而且把一个个完好功能的代码包装起来,让代码更加整洁。

我们再来看一下这个函数:

//写一个函数可以交换两个整形变量的内容。
#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;
 printf("交换前:num1 = %d num2 = %d\n ",num1,num2);
 Swap1(num1, num2);//调用
 printf("交换后:num1 = %d num2 = %d\n", num1, num2);
 return 0;
 }

这个函数可行吗?很多人觉得函数也是将两个互换了呀,但是这里是没有实现两个数的交换的。我们可以看看结果。

为什么会没有变化呢,因为我们这里传过去的只是一个值,而不是num1num2的地址,所以那个值改变,并不会改变num1num2中地址存放的数字,也没有返回值回来赋给num1num2。我们再来看看这个函数:


void Swap2(int* px, int* py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
int main()
{
 int num1 = 1;
 int num2 = 2;
 printf("交换前:num1 = %d num2 = %d\n",num1,num2);
 Swap2(&num1, &num2);//调用
 printf("交换后:num1 = %d num2 = %d\n", num1, num2);
 return 0;
 }

这一代码中,我们可以看见num1num2的值交换了,而不同的就是我们传参的时候传过去的是数值还是地址,在第二个函数中,我们将num1num2的地址传了过去,而在函数中改变的,也会是该地址上的值。

4. 函数参数

而创建函数时传入的函数参数和调用函数中的传参,实际上是我们函数的实际参数和形式参数:

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

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

比如上面的函数:

//实现计算两数相加的和
int jia(int c, int d)//这里的c,d是形式参数
//形式参数实例化后有自己的空间存储,有和实际参数一样的数值
{
	int sum = 0;
	sum = c + d;
	return sum;
}
int main()
{
	int a = 20;
	int b = 10;
	int sum = jia(a, b);//这里的a,b是实际参数
	printf("%d", sum);
	return 0;
}

在这里插入图片描述
形式参数实例化之后有自己的地址,也有和实际参数一样的值。但是当函数完成退出后,形式参数的地址又会被销毁,所以我们通常可以认为:

形参实例化之后其实相当于实参的一份临时拷贝。


5. 函数调用

接下来是函数的调用:

我们在上面的学习中,看见了传参过去的时候可以是数值,也可以是地址,那这两者有什么区别呢?

其实在函数调用中,我们可以分为传值调用和传址调用:

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

传值调用,就是把数值传过去调用函数中,当我们调用函数结束后,函数中的临时变量都会被销毁,无法继续保留,如果不是return将值返回,那么传过去的值对原函数中没有影响。

传址调用:
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

传址调用,就是将函数中的变量的地址传到调用函数中,这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量。

我们可以在理解传值调用和传址调用中,可以简单的理解为:

传值调用是当我们要装修家里的墙壁的时候,我们把想法告诉设计师函数,然后设计师函数在他自己家把装修稿画出来,但是如果他不给我最后的稿子(即返回值),也影响不了我家中的装修。

而传址调用是就是将把我家地址给设计师函数,让他来我家在我家中设计出来,那么就直接把我的墙壁装修了,直接是在我的地址上操作了。那么设计师函数离开之后,我家(地址)中的墙壁已经装修好了。
在这里插入图片描述
当然这只是帮助理解,和真正的调用还是有很大的差别的。


6. 函数的嵌套调用和链式访问

嵌套调用:函数和函数之间可以有机的组合的。也就是说可以在调用函数中调用函数

嵌套定义是不被允许的,但嵌套调用是可以使用的。

嵌套定义:

//错误代码:
int main()
{
    void fun()//函数内定义函数是不行的!
    {
        printf("123");
    }
    return 0;
}

没错,俄罗斯套娃牛刀小试:

嵌套调用:

void second_fun()
{
	printf("a+b的值为:");
}

int first_fun(int a, int b)
{
	int sum = 0; 
	second_fun();
	sum = a + b;
	
	return sum;

}

int main()
{
	int a = 20;
	int b = 10;
	int ret = first_fun(a,b);

	printf("%d", ret);
	return 0;
}

在这里插入图片描述

再举一个例子:

#include <stdio.h>
void shuai()
{
    printf("我好帅!\n");
}
void hao()
{
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        shuai();
    }
}
int main()
{
    hao();
    return 0;
}

在这里插入图片描述
而链式访问就是把一个函数的返回值作为另外一个函数的参数。

没错,俄罗斯套娃梅开二度:

int main()
{
	int len = strlen("abc");
	printf("%d\n", len);

	printf("%d\n", strlen("abc"));//链式访问
	//把strlen库函数的返回值直接printf函数输出
	//printf输出函数也是函数
	return 0;
}

知道了什么是链式访问,那我们来看看这个代码:

int main()
{
    printf("%d",printf("%d",prinft("%d",43)));
    //输出结果是什么?
    return 0;
}

43?不不不,答案是4321,printf函数也是函数,有返回值!

这时候我们又可以到cplusplus中查看一下:

进入cplusplus中搜索printf然后下滑到return value查看,这里说成功后,即成功输出后,返回的是写入字符总数。这里我们就了解了,也就是输出多少的数字个数,就是返回值。

所以答案可以解释为:
输出为:4321
在cplusplus中查找可得: printf有返回值,返回值为输出数字个数。 所以最里面的一个printf输出为43,输出了两个数字个数,然后返回值给到第二个printf,输出就会为2,同理到最外面的一个printf输出为1。


7. 函数的声明和定义

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

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

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

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

函数的声明就是告诉编译器我有这个函数了,后面使用的时候就不要翻脸不认码。其中一定要先声明后使用,因为编译器开始工作是将代码从上到下进行工作的,如果你使用的函数先使用,那么编译器就会报错了。

比如:

//由上到下编译
int main()
{
	int a = 20;
	int b = 10;
	int ret = first_fun(a,b);
    //无法识别,函数未定义

	printf("%d", ret);
	return 0;
}

int first_fun(int a, int b)
{
	int sum = 0; 
	second_fun();
	sum = a + b;
	
	return sum;
}

正确的代码:

int first_fun(int a, int b);//函数声明

int main()
{
	int a = 20;
	int b = 10;
	int ret = first_fun(a,b);//已声明可使用
	printf("%d", ret);
	return 0;
}

int first_fun(int a, int b)
{
	int sum = 0; 
	second_fun();
	sum = a + b;
	
	return sum;

}

当然你可以直接把调用函数放在最上面,但是当需要写更多的代码的时候,为了可读性更高,分块更清晰,我们还是应该写函数声明的,比如在用c语言制作三子棋小游戏:

【C语言】三子棋小游戏详解

我们可以建一个头文件在里面包含函数声明。


8. 函数递归

什么是递归?

程序调用自身的编程技巧称为递归( recursion)。递归的主要思考方式在于:把大事化小。

递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

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

我们来举个栗子:

写这样一个代码,接收一个整形数(无符号),打印他的每一位数,比如接收的是1234,就打印1 2 3 4。
在这里插入图片描述
在这里插入图片描述

我们可以看到,在main函数中调用print函数中,print函数内部又调用了自己,这就是程序调用自身的编程技巧,这就是递归。

但有时候可以使用递归也不一定要用递归,比如求斐波那契数列:

int fib(int n)
{
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

int main()
{
    int n = 0;
    printf("请输入你想知道第几位斐波那契数:>\n");
    scanf("%d", &n);
    int ret = fib(n);
    printf("第%d位的斐波那契数是%d",n, ret);
    return 0;
}

这个代码中,如果我要求第50个斐波那契数字的时候特别耗费时间。这就是我们发现的问题在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。使用 fib函数求10000的斐波那契数(不考虑结果的正确性),程序会崩溃。

因为我们很多的计算都在重复!我们可以加入count变量来看看每一个数需要调用多少次得出:

int count = 0;//全局变量
int fib(int n)
{
    if (n == 3)
        count++;
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

int main()
{
    int n = 0;
    printf("请输入你想知道第几位斐波那契数:>\n");
    scanf("%d", &n);
    int ret = fib(n);
    printf("第%d位的斐波那契数是%d\n", n, ret);
    printf("循环%d次", &count);
    return 0;
}

在这里插入图片描述
单单是30位,就已经循环了9281852次了,50位更是难以想象。所以这个代码是需要改进的:

将递归改写成非递归。

int fib(int n)
{
    int ret;
    int pre_ret;
    int next_ret;
    ret = pre_ret = 1;
    while (n > 2)
    {
        n -= 1;
        next_ret = pre_ret;
        pre_ret = ret;
        ret = pre_ret + next_ret;
    }
    return ret;
}

int main()
{
    int n = 0;
    printf("请输入你想知道第几位斐波那契数:>\n");
    scanf("%d", &n);
    int ret = fib(n);
    printf("第%d位的斐波那契数是%d",n, ret);
    return 0;
}

所以我们总结为:

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

而递归的例子还有很多,如经典的汉诺塔游戏:递归汉诺塔


好啦,本篇的内容就到这里,小白制作不易,有错的地方还请xdm指正,互相关注,共同进步。

还有一件事:

  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恒等于C

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值