第三节 初识函数

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1. 函数是什么

维基百科中对函数的定义子程序

  • 在计算机科学中子程序英语Subroutine, procedure, function, routine, method,

subprogram, callable unit是一个大型程序中的某部分代码 由一个或多个语句块组 成。它负责完成某项特定任务而且相较于其他代 码具备相对的独立性。

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

2.C语言中函数的分类

2.1 库函数 

定义:将函数封装入库供用户使用的一种方式

 2.1.1为什么会有库函数

1.我们知道在我们学习C语言编程的时候总是在一个代码编写完成之后迫不及待的想知道结果想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能将信息按照一定的格式打印到屏幕上printf。

2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作strcpy。

3.在编程是我们也计算总是会计算n的k次方这样的运算pow。 

像上面我们描述的基础功能它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到为了支持可移植性和提高程序的效率所以C语言的基础库中提供了一系列类似的库函数方便程序员进行软件开发。 

学习库函数的网站:www.cplusplus.com


但是库函数必须知道的一个秘密就是使用库函数必须包含#include对应的头文件。 这里对照文档来学习上面几个库函数目的是掌握库函数的使用方法。

 2.1.1 如何学会使用库函数

需要全部记住吗No

需要学会查询工具的使用
MSDN(Microsoft Developer Network)
www.cplusplus.com http://en.cppreference.com英文版 http://en.cppreference.com中文版
英文很重要。最起码得看懂文献。

2.2 自定义函数

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

 函数的组成

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

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

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

 

3. 函数的参数

3.1 实际参数实参

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

 3.2 形式参数形参

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

上面Swap1和Swap2函数中的参数xypxpy都是形式参数。在main函数中传给Swap1的num1
num2和传 给Swap2函数的&num1&num2是实际参数。

这里我们对函数的实参和形参进行分析

 代码对应的内存分配如下 

这里可以看到Swap1函数在调用的时候xy拥有自己的空间同时拥有了和实参一模一样的内容。所 以我们可以简单的认为形参实例化之后其实相当于实参的一份临时拷贝。 

 4. 函数的调用

4.1 传值调用

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

4.2 传址调用

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

4.3 练习

1. 写一个函数可以判断一个数是不是素数。
2. 写一个函数判断一年是不是闰年。
3. 写一个函数,实现一个整形有序数组的二分查找。
4. 写一个函数,每调用一次这个函数,就会将num的值增加1。

1. 写一个函数可以判断一个数是不是素数。 

 

 

 2. 写一个函数判断一年是不是闰年。

3. 写一个函数,实现一个整形有序数组的二分查找。

 

4. 写一个函数,每调用一次这个函数,就会将num的值增加1

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

5.1 嵌套调用

函数和函数之间可以有机的组合的。

函数可以嵌套调用但是不能嵌套定义。 

5.2 链式访问

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

 

6. 函数的声明和定义

6.1 函数声明

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

6.2 函数定义

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

 

 7. 函数递归

7.1 什么是递归

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

7.2 递归的两个必要条件

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

 7.2.1 练习1

接受一个整型值无符号按照顺序打印它的每一位。

例如
输入1234输出 1 2 3 4.

 7.2.2 练习2画图讲解

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

 

7.3 递归与迭代

7.3.1 练习3

求n的阶乘。不考虑溢出

//求阶乘ret 和 阶乘和sum
int main()
{
	int n = 0;
	int ret = 1;
	int sum = 0;
	for (n = 1; n <= 4; n++)
	{
		ret = ret *n;//阶乘 n!=(n-1)!*n
		sum = sum + ret;//阶乘和
	}
	printf("%d", sum);
	return 0;
}

 7.3.2 练习4

求第n个斐波那契数。不考虑溢出

 

但是我们发现有问题
在使用fib这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。

使用factorial函数求10000的阶乘不考虑结果的正确性程序会崩溃 

为什么呢
我们发现fib函数在调用的过程中很多计算其实在一直重复。 如果我们把代码修改一下

 

那我们如何改进呢
在调试factorial函数的时候如果你的参数比较大那就会报错`stack overflow栈溢出这样的信息。系统分配给程序的栈空间是有限的但是如果出现了死循环或者死递归这样有可能导致一直开辟栈空间最终产生栈空间耗尽的情况这样的现象我们称为栈溢出。

那如何解决上述的问题
1. 将递归改写成非递归。
2. 使用static对象替代nonstatic局部对象。在递归函数设计中可以使用static对象替代nonstatic局部对象即栈对象这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销 而且static对象还可以保存递归调用的中间状态并且可为各个调用层所访问。

比如下面代码就采用了非递归的方式来实现

//int main()
//{
//	int i = 0;
//	scanf("%d", &i);
//	int j = 0;
//	int ret = 1;
//	for ( j= 1; j <= i; j++)
//	{
//		ret *= j;
//	}
//	printf("%d", ret);
//	return 0;
//}
//int Fib(unsigned long n)
//{
//	if (n <= 2)
//		return 1;
//	else
//		return Fib(n - 1) + Fib(n - 2);
//}
//int main()
//{
//	unsigned long n = 0;
//	scanf("%d", &n);
//	int ret = Fib(n);
//	printf("%d\n", ret);
//
//	return 0;
//}

//高效的斐波那契数列
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;

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

	return c;
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);

	//	printf("count = %d\n", count);

	return 0;
}

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

 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6