走廊灯关上,书包放,走到房间窗外望-------------------------------------------------------------------
目录
前言
函数是C语言中的重要工具,能够为我们提供很大的便利。
一、【函数是什么】
提到函数我们首先会想到数学当中学习的函数:y=f(x),函数y和自变量x之间的映射关系。
那么C语言中的函数又是怎样的呢?
维基百科中对函数的定义:子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
二、【函数的分类】
介绍完了,函数的定义接下来说一下函数的的主要分类。主要分为自定义函数和库函数接下来我 一 一介绍。
2.1【自定义函数】
首先要清楚什么叫做自定义函数,正如他的名字,自定义函数就是我们自己创造的用来帮助我们完成某些特定功能的工具。它包括,函数名,参数部分,传递参数的方式,返回类型,调用它的方式,以及他本身的声明和定义。接下来我会 一 一 进行说明。
函数名是自由创建的,建议创建的每个函数的函数名都有其本身的意义,且最好使用英语。
这里举例说明:比如我现在创建一个用来获取两个数之间最大值的函数,我可以为其命名为
GetMax,Max或者getmax,这些形式。
2.21【函数的参数】
参数包括实参和形参,其中实参:
是真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参。形参:形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单
元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效,出了函数就自动销毁了。这里举例说明:还是上面的Max函数,用来找出两个数之间的最大值,这里比如我需要比较3和5的大小,这里的3和5是要传给函数用来比较的值,所以他们叫做实参,而函数那边需要创建两个变量来进行接收,比如创建为下x,y这里的x,y就叫做形参。
2.22【函数的传参方式】
这里有个很重要的点,就是函数的传参方式除了上面图里直接把实参的值进行传递之外,我们也可以传递实参的地址,即传参的方式主要分为传值和传址。
为了便于理解我们写两个函数Swap1和Swap2来交换两个数的值:
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;
printf("Swap1运行的结果\n");
printf("%d %d\n", num1, num2);
Swap1(num1, num2);
printf("Swap1::num1=%d num2=%d\n", num1, num2);
printf("Swap2运行的结果\n");
printf("%d %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2::num1=%d num2=%d\n", num1, num2);
return 0;
}
运行结果:
其中Swap1是传值Swap2是传址,可以看到两个函数的结果并不相同显而易见Swap1并没有完成交换的功能我们在【函数的调用方式】中进行解释。上面 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。在main函数中传给 Swap1 的 num1 ,
num2 和传给 Swap2 函数的 &num1 , &num2 是实际参数。
这里对函数的参数进行分析:
代码的对应内存分配如下:
总结:这里传值和传址的区别在于
传值:由于,在传参过程中,并不是真正把实参传给了函数,而是临时创建了两个空间把实参的值拷贝了过去,也就成为了我们了解的形参。
传址:这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。也就是可以对实参本身进行修改。
所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
2.23【函数的调用方式】
1.传值调用:
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
2.传址调用:
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操
作函数外部的变量。3.嵌套调用:
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
4.链式访问:
把一个函数的返回值作为另外一个函数的参数。
举例说明:
传值调用和传址调用:
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;
printf("Swap1运行的结果\n");
printf("%d %d\n", num1, num2);
Swap1(num1, num2);
printf("%d %d\n", num1, num2);
printf("Swap2运行的结果\n");
printf("%d %d\n", num1,num2 );
Swap2(&num1, &num2);
printf("%d %d\n", num1, num2);
return 0;
}
嵌套调用:
void print()
{
printf("haha\n");
}
void ThreePrint()
{
int i = 0;
for (i = 1; i <= 3; i++)
{
print();
}
}
int main()
{
ThreePrint();
return 0;
}
链式访问:
int main()
{
printf("%d", printf("%d ", printf("%d ", 54)));
return 0;
}
2.24【函数的声明和定义】
函数的声明
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的。函数的定义
1.函数的定义是指函数的具体实现,交待函数的功能实现。
2.函数不能嵌套定义。
举例说明
声明和定义:
一般在模块化编程中会用到,可以提高维护代码的效率。任何函数都要先定义在使用。
不能嵌套定义:
正确的定义方法:
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;
}
错误的定义方法:
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;
}
}
2.25【函数的返回类型】
一个函数可以有返回值,返回值的类型在函数定义时有 “ 返回值类型” 定义。“ 返回值类型 ”只能是以下这几个数据类型之一:“void”、“int”、“char”、“double”、“byte”、“boolean”、“string”。
其中“void”是函数返回值特有的类型,表示空返回值,及表示函数没有实际的返回值。其实一个函数用不用void在于这个函数的功能需不需要让他返回值。
比如上面的Max函数比较两个数的大小需要知道最大的值是谁,所以需要返回值,因此返回值类型为int。
而Swap函数的功能只是把传过去的两个数进行交换即可,不需要返回值,还有print只是把haha打印出来也不需要返回值,所以他们的返回类型都是void。
2.26【return语句】
return (表达式);或 return;
分为带表达式形式和不带表达式形式。其中return是关键字。
注:
该语句常用在被调用函数中,在被调用函数中执行到该语句时,将结束对被调用函数的执行,并把控制权返回给调用函数,继续执行调用函数后边的语句。在带有返回值的情况下,将return语句所带的表达式的值返回给调用函数,作为被调用函数的值。
在被调用函数中,可以用return语句,也可以不用return语句。
如果要求被调用函数有返回值,则一定要用return语句,采用“return(<表达式>);”格式。如果被调用函数不需要返回值,并且当被调用函数的所有语句执行完后进行返回,则被调用函数可以不用return语句。
定义为void类型的函数,可以不写return,因为即使写了也无法返回数值。
2.2【库函数】
1.什么是库函数?
库函数(Library function)是将函数封装入库,供用户使用的一种方式。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include<>加到里面就可以了。一般是放到lib文件里的。
我的理解是库函数其实也是自定义函数,因为这些自定义函数非常常用,所以把他们保存起来,在需要使用时进行调用即可。
2.为什么会有库函数?
. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
3.那怎么学习库函数呢?需要学会查询工具的使用:
MSDN(Microsoft Developer Network)
www.cplusplus.com
http://en.cppreference.com(英文版)
http://zh.cppreference.com(中文版)
简单的总结,C语言常用的库函数都有:
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
我们参照文档,学习几个库函数:(教会学生怎么使用文档来学习库函数)。
strcpy(实现对字符串的拷贝,对应的头文件为<string.h>)char * strcpy ( char * destination, const char * source );
memset(对指定个数的字符进行替换,对应的头文件为<string.h>)void * memset ( void * ptr, int value, size_t num );
注:
但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。
举例说明
strcpy:
int main()
{
char arr1[] = "abcdef";
char arr2[20] = { 0 };
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
memset:
int main()
{
char arr1[] = "hello world";
memset(arr1, 'x', 4);
printf("%s", arr1);
return 0;
}
三、【递归和迭代的思想】
递归
1.递归思想
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小2.必要条件
.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
.每次递归调用之后越来越接近这个限制条件。迭代
1.迭代思想
利用变量的原值推算出变量的一个新值。如果递归是自己调用自己的话,迭代就是A不停的调用B。
递归中一定有迭代,但是迭代中不一定有递归。大部分可以相互转换,能用迭代的不用递归。2.必要条件
. 确定迭代变量;
. 确定迭代关系;
. 确定迭代过程的控制。
3.1【函数当中的递归和迭代】
举例说明
接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4.
void print(int i)
{
if (i > 9)
{
print(i / 10);
}
printf("%d ", i % 10);
}
int main()
{
int i = 1234;
print(i);
return 0;
}
我们如果想要得到一个数字的每一位,就需要我们先%10得到最后一位,后/10除去最后一位,因为/10最后一位为余数,可以继续向前查找,直到这个数字成为一个一位数停止程序(因为如果这里是个一位数,a/10的值就是0,我们并不想打印0的每一位),所以在这里我们定义一个函数print(),它可以按顺序打印每一个值。
分步解决就是这样:
print(1234)
print(123) 4
print(12) 3 4
print(1) 2 3 4
应用递归求斐波那契数列的第n项
斐波那契数列:1 1 2 3 5 8 13 ...(规律:第一二项为1,后一项等于前两项的和)
int Fib(int i)
{
if (i <= 2)
{
return 1;
}
else
return Fib(i - 1) + Fib(i - 2);
}
int main()
{
int i = 0;
scanf("%d", &i);
int ret=Fib(i);
printf("%d ", ret);
return 0;
}
但是我们发现有问题;
在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
那我们如何改进呢?
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)
这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一
直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
那如何解决上述的问题:
1. 将递归改写成非递归。
2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代
nonstatic 局部对象(即栈对象),这不
仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保
存递归调用的中间状态,并且可为
各个调用层所访问。
迭代形式
int Fib(int i)
{
int a = 0;
int b = 1;
int c = 1;
while (i > 2)
{
a = b + c;
b = c;
c = a;
i--;
}
return c;
}
int main()
{
int i = 0;
scanf("%d", &i);
int ret=Fib(i);
printf("%d ", ret);
return 0;
}
3.2【优缺点比较】
递归优缺点
优点:代码更简洁清晰,可读性更好。
缺点:时间和空间消耗比较大。每一次函数调用都需要在内存栈中分配空间以保存参数,返回地址以及临时变量,而且往栈里面压入数据和弹出都需要时间。迭代优缺点
优点:计算效率高,无额外内存开销。
缺点:代码不如递归简洁,有时不容易理解。
3.3【注意】
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
总结
-----------------------------------------------------------印象中的爱情好像顶不住那时间,所以你弃权
————《半岛铁盒》