1. 函数是什么?
维基百科中对函数的定义:子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
比如常用的
printf——打印函数
scanf——输入数据
被c语言封装到一起了 库函数(stdio.h)
包含这个库函数
2. C语言中函数的分类:
1. 库函数
c语言把常用的功能,进行了封装,封装成一个个函数,提供出来大家一起使用。
比如:scanf,printf,strlen,rand,srang,time-----c语言直接提供的(库函数)
c语言并不去直接实现库函数,而是直接提供了C语言的标准和库函数的约定
规定:scanf 功能,名字,参数,返回值 下面会具体介绍
2. 自定义函数
2.1 库函数: 为什么会有库函数?
简单的总结,C语言常用的库函数都有:
IO函数 输入/输出函数 scanf printf getchar putchar
字符串操作函数 strlen strcmp...
字符操作函数 islower isupper
内存操作函数 memset memcmp memory-记忆 (计算机:内存)
时间/日期函数 time
数学函数 sqrt pow..
其他库函数
为什么会有库函数?
1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想 把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格 式打印到屏幕上(printf)。
2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。 像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员 进行软件开发。
那怎么学习库函数呢?
这里我们简单的看看:www.cplusplus.com
大家就可以看到熟悉的库函数了(stdio.h,math.h等等)
也可以对函数进行查找
该怎么学习文档
上机测试更加容易理解
并且strcpy会拷贝‘/0’过去
这里arr2[]实际的字符个数(‘0’)是比arr1[]多的 但是我们运行完查看没有看到多余的‘0’
查看内存
我们可以看到在arr1[10]存储的‘\0’也被拷贝到arr2[10]去了
所以arr2[ ]原本长度相比arr1[ ]更多的‘0’不会显示出来
我们再来查看一下内存的函数
不理解没关系 我们直接上机测试
注: 但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。 这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。
2.1.1 如何学会使用库函数?
需要全部记住吗?No
需要学会查询工具的使用:
MSDN(Microsoft Developer Network)
www.cplusplus.com http://en.cppreference.com(英文版)
http://zh.cppreference.com(中文版)
都是类似的文档,大家只看一个即可。英文很重要。最起码得看懂文献。
2.2 自定义函数
如果库函数能干所有的事情,那还要程序员干什么?
所有更加重要的是自定义函数。
函数的组成:
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
我们举一个例子: 拿库函数strlen举例子
接下来在编译器完成自定义函数
实现我们用普通的方法计算两个值中较大的一个值
接着我们可以实现一个同样功能的自定义函数
首先我们看下面的主函数部分 只修改了//计算 行 意为调用函数
对比
之后我们看自定义函数的部分
就相当于把刚刚计算的功能给独立了出来,并进行了调用(使用),同样能实现功能。
3. 函数的参数
3.1 实际参数(实参):
真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等。 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
3.2 形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内 存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数 中有效。
为了更方便大家理解,我们运用上面学的知识来再写一个自定义函数来交换两个值
首先我们实现成函数:交换num1和num2的数值;并进行打印,但是运行后不能完成任务
本应该输出结果为2,1;但是这样并没有进行正确的交换!
遇到问题F10调试查看原因
我们发现在num1,num2給x,y传参数的时候,数值都是正确的
成功传递1,2
但是我们仔细看取地址的值(&)
发现相对应的地址传递后并不是一样的
所以我们可以这样理解:
在用这种方法传递参数,传递的是变量的值;例如(a=1)传递出去的是1。
而计算机会用一个新的内存地址来接受这个1.
在这个自定义函数运行结束后,就会自动销毁这个新的内存地址,而原来的值(a)并没有发生变化,而传递的这个参数,就叫做形式参数。
所以我们要用原本的内存地址来进行参数传递,利用指针来传递,这样传递的参数,就叫实际参数
现在调试模式下观察内存
我们发现内存地址也是相同的了
正确的编译出结果(自信脸)
所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
4. 函数的调用:
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
通过上边的概念大家应该是可以更加理解的吧,这边我继续写一个代码给大家加深印象
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//写一个函数,每调用一次这个函数,就会将 num 的值增加1。
void test(int num)
{
num++;
}
int main()
{
int num = 0;
//调用函数
test(num);
//二次调用
test(num);
//输出num
printf("%d", num);
return 0;
}
大家觉得运行输出应该是多少
答案:0
答对的话大家应该就是已经理解的了,赞!!
还不理解的话看看下面的定义并且已经附上正确的代码了
4.2 传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
void test(int* num)//指针接收
{
(*num)++;
}
int main()
{
int num = 0;
//调用函数(并且传递地址)
test(&num);
//二次调用
test(&num);
//输出num
printf("%d", num);
return 0;
}
运行结果就是2
5. 函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
5.1 嵌套调用
上代码
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for(i=0; i<3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
代码的意思是在主函数中首先调用three_line()函数,之后在three_line()函数中再调用new_line()函数 这就是嵌套。
函数可以嵌套调用,但是不能嵌套定义。
错误代码
void test()
{
int fun()
{
printf("错误的");
}
}
int main()
{
test();
return 0;
}
5.2 链式访问
把一个函数的返回值作为另外一个函数的参数。
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));//strlen函数是计算字符个数的
printf("%d\n", ret);
return 0;
}
此处是吧strcat的返回值(arr地址)传给strlen让strlen直接算出arr数组的长度
不好理解的话再看更简单的
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//注:printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
答案:4321
6. 函数的声明和定义
6.1 函数声明:
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
还是先看代码,更好理解
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//函数定义
int add(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b);
//加法运算
int c = add(a, b);//函数调用
//打印
printf("%d", c);
return 0;
}
这边写的是一个调用可以相加的函数的代码
有些书籍里会将函数声明放在代码末端,不推荐,因为可能代码运行后会报错
调试模式,在箭头还没有运行到add()函数时会报警告。
为避免这种问题 就要在函数第一行加上函数声明
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
或者直接把函数定义放在第一行
3. 函数的声明一般要放在头文件中的。
我们可以把add()函数单调分离开 放在头文件里面 这样可以把函数和主函数区分开。
然后在原来主函数的文件头上应用add.h文件,就可以直接使用文件内的函数add()。
开头记得写上#pragma once用于表示头文件
有些读者可能认为为什么要这么麻烦来创建一个新文件,全放一起不好吗
我们先看一个例子
在公司里写代码,是需要协作分工的,而不是把所有的代码都写在一个文件中。
所以你们要分模块来写,方便协作,最后做整合。
你在一个”计算器“公司,有三个员工,分别实现加法,减法,除法的功能。
而大家为实现这三个功能每人都写了两个文件
.h就是实现函数的定义
.c就是主函数,可以实现函数的调用
当我们减法的员工想要去用加法的函数时就可以直接引上头文件来调用加法的函数
箭头意思相当于在程序中此步骤(sub.c调用add.h内的函数)
这是add.h文件内部(实现加法)
所以这个步骤就可以在sub.c文件中实现add.h(加法)的功能
同理我们也可以根据需求来包含其他的头文件(add.h,sub.h,div.h)
所以大家明白了吧 这样可以根据自己的需求来使用对应函数,就不会让函数都堆在一起形成混乱
再举个例子
假如你是个程序员,你写了一个代码实现add()函数。你想要把这个功能卖出赚钱,但你不想开源,就可以将函数功能和函数声明给分开,然后进行加密。
在解决方案下的add文件中右键,选择属性
选择静态库(.lib)
运行代码成功之后我们打开目标目录
以记事本方式打开就发现文件变成二进制文件难以查看
当你把add.lib与add.h文件一起卖出后,买家就可以直接使用功能了,但不能看到你具体实现的代码。
所以 对函数声明和定义进行分离是十分重要的
7. 函数递归
7.1 什么是递归?
程序调用自身的编程技巧称为递归( recursion)。
简单来说就是函数调用自己就是递归
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接 调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问 题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程 序的代码量。
递归的主要思考方式在于:把大事化小
7.2 递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
老规矩 上代码
7.2.1 练习1:
接收一个整型值(无符号),按照顺序打印它的每一位。
例如: 输入:1234,输出 1 2 3 4.
我们画图分析
当我们需要打印1234的每一位数时。
我们可以分为打印4 3 2 1
而1234%10得到的是4,1234/10得到的是123。
再者123%10得到的是3,123/10得到的是12。
12%10得到的是2,12/10得到的是1。
1%10得到的是1,1/10得到的是0。
所以我们可以实现一个函数print(),负责把1234每一位打印到屏幕上
之后我们可以把打印1234拆分成两个步骤:递归调用print(123)然后再输出4,以此类推。
void print(unsigned int n)
{
if (n > 9)//判断此处的n是否为两位数
{
print(n / 10);//递归传值并且去掉最后一位数
}
printf("%d ", n % 10);//输出最后一位数
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);
print(num);
return 0;
}
主函数输入1234获得num数,进入print()函数
进入函数后传值1234给n,if判断为Turn进入,并且再次调用print()函数
传值123给下一个print(),if判断为Turn进入,并且再次调用print()函数
传值12给下一个print(),if判断为Turn进入,并且再次调用print()函数
传值1给下一个print(),if判断为fales不进入,跳过if()语句直接打印1
在这最后一次print()函数结束后,上面的其实还没有执行完,所以现在依次执行
返回上一个函数还没有执行完的printf,在屏幕打印了2,打印完后函数结束,继续执行上一个没有执行完的函数。
打印3之后继续执行上一个函数
打印出4,print()函数结束,并且代码也结束了。
7.2.2 练习2: 编写函数不允许创建临时变量,求字符串的长度。
首先我们不管限制条件,直接写出答案
#include <string.h>
int main()
{
char arr[] = "abc";
int len = strlen(arr);
printf("%d", len);
return 0;
}
非常简单
之后我们进行一个小小的升级,不使用strlen函数,自己来编写实现一样的功能。
int my_strlen(char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
char arr[] = "abc";//数组中存放是这样的[a b c \0]
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
接下来,就离我们答案更进一步了,之后把限制条件加入。
我们运用递归的思想
首先放入函数my_strlen("abc")
之后判断第一个字符是否为‘\0’,如果不是,去掉第一位数再调用my_strlen("bc")并且+1;
以此类推
再最后字符为‘\0’时,返回0;
实现函数:
int my_strlen(char* s)
{
if (*s == '\0')
{
return 0;
}
else
{
return 1 + my_strlen(s + 1);
}
}
int main()
{
char arr[] = "abc";//数组中存放是这样的[a b c \0]
int len = my_strlen(arr);
printf("%d ", len);
return 0;
}
7.3 递归与迭代
7.3.1 练习3: 求n的阶乘。(不考虑溢出)
参考代码:
int factorial(int n)
{
if(n <= 1)
return 1;
else
return n * factorial(n-1);
}
7.3.2 练习4: 求第n个斐波那契数。(不考虑溢出) 参考代码:
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
但是我们发现有问题;
使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
为什么呢?
我们发现 fib 函数在调用的过程中很多计算其实在一直重复。
要重复计算2的48次方次,计算量非常的大
如果我们把代码修改一下:
int count = 0;//全局变量
int fib(int n)
{
if(n == 3)
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
最后我们输出看看count,是一个很大很大的值。
那我们如何改进呢?
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
那如何解决上述的问题:
1. 将递归改写成非递归。
2. 使用 static 对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
比如,下面代码就采用了,非递归的方式来实现:
//求n的阶乘
int factorial(int n)
{
int result = 1;
while (n > 1)
{
result *= n ;
n -= 1;
}
return result;
}
//求第n个斐波那契数
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. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。