数学中我们常见到函数的概念。但是你了解C语言中的函数吗?
维基百科中对函数的定义:子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
为什么会有库函数?
在早期的时候,C语言并没有库函数,这个时候,人们就发现,总会使用一些功能,如:
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
那怎么学习库函数呢?
这里我们简单的看看:http://www.cplusplus.com/reference/
简单的总结,C语言常用的库函数都有:
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
我们参照文档,学习几个库函数:
strcpy
译文:将源指向的 C 字符串复制到目标指向的数组中,包括终止的 null 字符(并在该点停止)。
为避免溢出,目标指向的数组的大小应足够长,以包含与源相同的 C 字符串(包括终止空字符),并且不应在内存中与源重叠。
意思就是把源头的字符串复制到目标数组中 包括\0 头文件是 string.h
memset
译文:将 ptr 指向的内存块的第一个字节数设置为指定值(解释为无符号字符)。
注:
但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。
需要全部记住吗?No
需要学会查询工具的使用:
MSDN(Microsoft Developer Network)
http://www.cplusplus.com/reference/
http://en.cppreference.com(英文版)
http://zh.cppreference.com(中文版)
英文很重要。最起码得看懂文献。
如果库函数能干所有的事情,那还要程序员干什么?
所有更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
函数的组成:
ret_type fun_name(para1, * )
{
statement; //语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
真实传给函数的参数,叫实参。
实参可以是: 常量、变量、表达式、函数 等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数是指函数名后面括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
实际参数和形式参数的名字可以相同,也可以不同。
举个例子:
写一个函数可以交换两个整形变量的内容。
原因分析:当实参传递给形参的时候,形参是实参的一份临时拷贝,所以对形参的修改不会影响实参。
正确的代码如下:
分析:
如果想改变变量a和b,就需要把a和b的地址传进去,如果只是想得到a和b的值,直接把a和b的值传过去就可以了,不需要传地址。
总结:
当实参传值给形参的时候,实参与形参使用的并不是同一空间,改变形参的值并不会改变实参的值。
而当实参传地址给形参的时候,实参与形参使用的是同一空间,改变形参的值同样会改变实参的值。
当实参传递给形参的时候,形参实际上等于实参的一份临时拷贝 。
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。(通过形参的指针就能访问到函数外部的变量,并进行操作)
简单来说就是 传值调用是把值给传过去 改变形参不会改变实参,而传址调用是把地址传过去,改变形参可以改变实参,一个是不同的空间,一个是相同的空间。
1.写一个函数可以判断一个数是不是素数。
先看一下不使用函数的for循环写法
for循环基础写法:
此处count记录数字个数。
我们还可以优化一下写法
for循环优化写法:
sqrt为数学库函数,开平方,头文件为math.h。
如果在小于开平方(i)的范围内找到一个因子,整除了 i,说明 i 就不是素数;如果在小于开平方(i)的范围内找不到因子,想必在小于开平方(i)的范围之外也找不到另外一个因子。试除2到开平方(i)之间的数字,如果能被整除,则结束循环,如果不能被整除,则为素数。
例如m等于a乘b,16等于2乘8或4乘4,那么a和b中一定有一个数字是 <= 4(平方根数)的。
现在我们看使用is_prime函数的写法
is_prime函数写法:
2.写一个函数判断一个年份是不是闰年。
现在我们看使用is_prime函数的写法
is_leap函数写法:
3.写一个函数,实现一个整形有序数组的二分查找。
今天我们学习函数写法
binary_search(二分查找)函数写法:
以下是一个错误示范
数组传参实际上传递的是数组首元素的地址,而不是整个数组,所以在函数内部计算一个函数参数部分的数组的元素个数是不靠谱的。形参arr看上去是数组,本质是指针变量。
补充一个C99编译系统C++的bool类型 bool 用来表示真假的变量
bool flag = ture; 真
= false; 假
4.写一个函数,每调用一次这个函数,就会将num 的值增加1。
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
函数可以嵌套调用,但是不能嵌套定义。
把一个函数的返回值作为另外一个函数的参数。
链式访问的前提是函数得有返回值。
举个例子
当printf函数进来,调用第二个printf函数,然后第二个调用第一个printf函数,第一个printf函数先打印43;
然后第一个printf函数把43两个参数返回给第二个printf函数,而printf函数的返回值是返回打印的字符个数,所以43是两个字符,返回2给第二个printf函数,打印2;
最后第二个printf函数把2返回给最外面的printf函数,而此时2的字符个数为1位,打印1;
所以最后打印为4321。
1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体这个函数是不是存在,函数声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的。
函数的定义是指函数的具体实现,交待函数的功能实现。
test.h的内容
放置函数的声明
test.c的内容
放置函数的实现
如果把函数的定义放到后面,这个时候要调用函数,会发现Add未定义,因为是从main函数进入的,而调用Add的时候找不到下面的函数,这个时候可以把Add函数放到main函数前面。
如果想在后面使用,可以在前面加一个函数的声明,如:
一般情况下,可以把声明放到 .h 头文件中,把定义放入一个 .c 源文件中,然后使用放入一个 .c 源文件中,在main文件中想要使用这个函数,只需要引用这个头文件就可以了,如:
在初学编程时,我们往往会觉得把所有的代码写到一个文件中最方便,但在公司不是这样写代码的。公司会把业务拆分,由许多人共同完成一个项目,把一个项目拆分成多个文件,方便每个人完成不同的模块,实现分工合作。或者也可以在仅出售代码使用权时,编译成静态库,这样买方只能使用,不能看到源代码,保护源代码不被泄露。
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
递归策略:
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4
如何打印出来 一个无符号整数的每 一位呢?
递归是把一个复杂对象拆分为一个简单对象。
如1234这个数,可以发现个位数4最容易打印,这个4是等于1234%10得到的 ,那么我们可以把1234拆分为:
(123) 4 (12) 3 4 (1) 2 3 4
这样就很好的把每一位给拆出来,然后再把拆分出来的值返回去。
完整程序如下:
如果递归编写不当,递归会无限开辟栈区空间,而栈区空间是有限的,最后会栈溢出,调试时会出现Stack overflow(栈溢出)。
下面用画图的方式来分析一下:
递归 就是递推和回归 上图中蓝色代表的是递推 红色代表的就是回归。
编写函数不允许创建临时变量,求字符串的长度。
我们先来看一个创建临时变量的写法(虽然能计算出结果,但不符合题目不许创建临时变量的要求)
那么如何不借助临时变量求一个字符串的长度呢?
可以用递归写法:
如"abc"这个字符串,求长度是求\0之前的位“abc\0”,那么可以拆分成
1+“bc” 1+1+“c” 1+1+1+0
这样就很好的把每一位给拆出来,然后在把拆分出来的值返回去。
求n的阶乘。(不考虑溢出)
循环(迭代)写法:
上图有两种方式,都可以求出结果,但有时候我们会发现用递归解决问题时并不太好。
求第n个斐波那契数。(不考虑溢出)
第十个斐波那契数就是55。
但是我们发现有问题:
如果使用递归写法,在使用fib 这个函数的时候如果我们要计算斐波那契数字的时候特别耗费时间。
为什么呢?
如上图:当求第40个斐波那契数的时候,这个时候我们来算一下当n等于三的时候有多少次,出现一次count加一次,这个时候发现,当在第40个的时候,三重复计算了将近四千万次,这个时候就会发现问题,是倒着往回推,第四十个需要先知道前两个,很麻烦很复杂,效率就会非常慢。
在调试factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。比如使用factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
因为系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
那如何解决上述的问题:(可能的方法,但不是一定有效)
这时候就要考虑迭代写法了。
为什么不从头开始呢?从前往后数,从第三个数字开始,这样不是很简单吗?
1 + 1 = 2 (第三个数)
a + b = c
1 + 2 = 3 (第四个数)
b赋给a c赋给b
2 + 3 = 5 (第五个数)
………
如下列代码:(不考虑溢出的情况下)
速度会很快,但如果求的数字大时,不一定算的对,因为可能会溢出。
使用static对象替代nonstatic 局部对象。在递归函数设计中,可以使用static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic 对象的开销,而且static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
提示:
3. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
4. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
5. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
具体要选择递归和迭代哪种方法,还是要看需求。
函数递归的几个经典题目:
感谢观看,欢迎三连,如有错误,欢迎指正。