C语言不可或缺的一部分----函数

1.什么是函数?

(1)在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
(2)一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。

在C语言中,函数是编写代码不可或缺的一部分,因为我们不能把所有的功能都写在主函数里,因此需要各种各样的库函数和自定义函数。

库函数

库函数就像我们平常经常用到的输入输出函数printf和scanf,字符串拷贝函数strcpy,以及数学类的函数:开方函数sqrt和次方函数pow等等,这些是C语言的基础库里提供的函数,方便我们进行软件的开发。因此在C语言的学习中,最需要的一项技能就是会利用一些软件来学习不同的库函数并且为熟练使用。

常用的网站有:www.cplusplus.com
en,cppreference.com
常用的离线工具:MSDN(Microsoft Developer Network)
关于MSDN,有需要的自取:https://pan.baidu.com/s/1nHiYFsbrUATplBcs8Ff0Kw (提取码:ybhz)

但是有了库函数,我们就不需要自己写函数了吗?那必然是不可能的,库函数只是帮助我们集成了一些比较常用的函数,但是并不能满足我们所有的需求,因此自定义函数才是最重要的,它能按照我们的想法来实现各种各样的功能。

自定义函数

下面是函数的基本定义:
函数有函数名、返回值类型和函数参数,都是我们来进行设计的,根据不同的功能要求来设计不同的函数。

在这里插入图片描述
注:但是在写函数最重要的是,函数的功能一定要独立,就是说每个函数不仅仅你自己编写出来可以使用,别人拿来也要可以直接使用,也就是说要有适用性,因此在编写函数时,要使这个函数与其他的功能是隔离的。

比如下面的例子:
找出两个数之间的最大值

int get_max(int x, int y)
{
    
    
	return (x>y)?(x):(y);
}
int main()
{
    
    
	int num1 = 10;
	int num2 = 20;
	int max = get_max(num1, num2);
	printf("max = %d\n", max);
	return 0;
}

在get_max函数中,只有一个功能,就是找出最大值,而打印的功能则是在调用之后才完成的,这就保证了函数功能的独立性。因为可能你用这个函数需要打印出来,别人可能只想找出最大值,如果把打印的功能也放在函数里,就会使他人用的时候无法完成想要的结果。

2.函数的返回值和参数

在函数的定义中有两个比较重要的地方:
1.函数的返回值
函数的返回值并不不是一定要有的,但是一定要在函数定义中写出来。如果有返回值,就要把返回值的类型写上,如果没有的话,就用void表示。
2.函数的参数
函数的参数分为实际参数(实参)和形式参数(形参)。
实际参数:真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数:形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

在这里插入图片描述

比如在上面get_max函数中,num1和num2就是实际参数,是在主函数中传给get_max的参数,在函数调用之前有确定的值。而get_max内部的x和y就是形式参数,只有在调用的时候才会分配空间,并且出了函数的生命周期就会销毁。因此,形参的实例化相当于实参的一份临时拷贝,因为x和y的内容一模一样,只是所在的内存空间不同。

3.传值调用和传址调用

在函数写好之后,就要对函数进行调用。
函数的调用分为传值调用和传址调用:

传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用:传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量。

也就是说,如果自定义函数的内部实现的功能不需要在自定义函数改变外部传进来的参数的值,就用传值调用;如果要在自定义函数的内部改变外部传进来参数的值,就要用到传址调用,也就是把实际参数的地址给到自定义函数,使自定义函数能够通过地址找到实际参数所在的内存空间。这部分会在指针里面详细讲解,现在用下面的例子做一个解释:

#include <stdio.h>
void Swap1(int x, int y)
{
    
    
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}

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

int main()
{
    
    
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}

主函数中的Swap1是传值调用,Swap2是传址调用,函数的功能都是想实现交换两个整数。

我们看看两个函数的运行结果:
在这里插入图片描述
从运行结果可以看出,Swap1函数并没有改变a和b的值,这是因为Swap1函数是传值调用,只是将a和b的值传了过去,而Swap1中的x和y是存在于另外一个内存空间的,所以不能改变a和b的值。而Swap2则完成了a和b的交换,因为传过去的是a和b的地址,Swap2函数通过地址找到了a和b的值并做了修改,这就是传值调用和传址调用的区别。

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

嵌套调用

函数的嵌套调用就是在一个函数执行的内部调用其他的函数来实现该函数的功能。比如下面的代码:

#include <stdio.h>

void print()
{
    
    
	printf("hehe\n");
}
int main()
{
    
    
	print();
	return 0;
}

在主函数内部调用print函数把hehe打印在屏幕上,就是一次函数的嵌套调用,而在print函数中调用库函数printf同样也是一次嵌套调用。可以说,嵌套调用将不同的函数组合起来,形成一个整体。

链式访问

链式访问指的是把一个函数的返回值作为另外一个函数的参数,运行起来就像是一个链条一样,串联起来。

#include <stdio.h>

int mul(int x,int y)
{
    
    
	return x * y;
}
int sum(int x,int y)
{
    
    
	return x + y;
}
int main()
{
    
    
	int a=2;
	int b=3;
	printf("%d\n",mul(sum(a,b),3));
	return 0;
}

在上面的代码中,mul(sum(a,b),3)就是一次链式访问,sum(a,b)的返回值作为mul()函数的参数。同时mul()函数的返回值也是printf函数的参数,链式访问将这三个函数串联起来。

5.函数的声明和定义

1.函数的声明是为了告诉编译器函数叫什么、参数是什么、返回值是什么,但是这个函数存在与否并不重要。
2.函数的声明一般出现在函数的使用之前,要满足先声明后使用
3.一般函数的声明是写在头文件中的,而函数的定义是放在另一个.c文件中,这样做的目的是使整个代码模块化,便于修改和使用。

在这里插入图片描述
函数的定义是指函数的具体实现,交代函数的功能实现。

6.函数的递归

什么是递归?

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

递归的必要条件

递归有两个必要条件:
1.递归一定有一个限制条件,当满足这个限制条件时,递归便不再继续。也就是说,递归不能无限递归,否则会导致栈溢出。
2.每次递归调用之后,会越来越接近这个限制条件。

递归必须满足这两个条件,但是满足这两个条件的递归不一定更有效率。

下面举一个例子看看递归是什么样子的:

#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;
}

上面的函数功能是为了打印一个整数的每一位。
可以看出,n>9是限制条件,n/10每次丢掉一位,每次递归调用之后会不断接近限制条件。

但是运用递归有时候并不一定能够提高效率,比如我们用递归找斐波那契数列的第n个数:

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

在运行的时候发现计算第50个斐波那契数字的时候,非常耗费时间。
我们在代码中增加一个计数器,来看看计算第30个斐波那契数时,计算第3个斐波那契数计算了多少次。
在这里插入图片描述
计算了317811次,可见用递归算斐波那契数十分没有效率。

但是如果改用循环的话,就会快很多:

int fib(int n)
{
    
    
	int result;
	int pre_result;
	int next_older_result;
	result = pre_result = 1;
	while (n > 2)
	{
    
    
		n -= 1;
		next_older_result = pre_result;
		pre_result = result;
		result = pre_result + next_older_result;
	}
	return result;
}

因此有几点注意:

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

使用递归要分情况,如果用循环更有效率就用循环,递归效率更高就用递归。

猜你喜欢

转载自blog.csdn.net/qq_41490958/article/details/112971847