1 函数是什么
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组
成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
函数一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软
件库。
2 库函数
库函数
比如printf 、strcpy、pow这些函数,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
学习库函数可以参考这几个网站:https://www.cplusplus.com/
https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5
库函数大致有以下几种类型:
IO函数
字符串操作函数
字符操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数
可以使用工具MSDN来进行查询,比如我要查找strcmp函数。
可以看出这个函数的功能是拷贝一个字符串。第一个参数是目的地的地址,第二个参数是被复制的字符串的地址,返回的值是一个char*类型的地址。需要使用的头文件是<string.h>
没有必要将库函数全部记住!没有必要将库函数全部记住!没有必要将库函数全部记住!
自定义函数
如果库函数能干所有的事情,那还要程序员干什么?
所有更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
早期的c语言是没有库函数的,都是需要自己定义的,这就导致了代码的跨平台性不好。
在函数调用的时候,实参传给形参,形参是一份临时拷贝,所以形参的修改,不会影响实参。例如下面发生错误的代码:
void Swap(int x,int y)
{
int z = 0;
z = x;
x = y;
x = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap(a, b);//传值调用
printf("交换后:a=%d b=%d\n", a, b);//程序有问题
return 0;
}
正确的打开方式应该是这样:
void Swap2(int *px, int *py)
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
int m = get_max(a, b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);//传址调用,函数内部可以直接操作函数外部的变量,功能更加彻底。
printf("交换后:a=%d b=%d\n", a, b);//程序有问题
return 0;
}
3 函数参数
形参和实参在不同的函数中,即不同的作用域,所以形参和实参可以同名。
实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
函数的实参可能是变量,也可能是常量,也可能是宏,也可能是指针等等。
形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
在C语言中,函数的形参一般都是通过参数压栈的方式传递的。
第一个错误代码的流程图:
a,b是实参;x,y是形参,当函数调用的时候,形参其实是实参的临时拷贝不管如何修改x和y,对于ab的值都没有影响!
第二个正确代码的流程图,这个代码的形参其实是拷贝的实参a、b的地址,这里用到了指针的概念,如果我交换了两个地址,就间接地改变了实参的值。
4 函数调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
练习
- 写一个函数可以判断一个数是不是素数。
#include<math.h>
int is_prime(int n)
{
//是素数返回1,不是素数返回0
//2~n-1的数字试除
int j = 0;
for (j = 2; j < sqrt(n); j++)
{
if (n % j == 0)
{
return 0;//执行完这行代码就结束了。
}
}
return 1;
}
int main()
{
int i = 0;
for (i = 100; i <= 200; i++)
{
//判断i是否为素数,如果是素数,就可以打印
if (is_prime(i)==1)
{
printf("%d ", i);
}
}
return 0;
}
- 写一个函数判断一年是不是闰年。
写的函数要保证功能简单,(意思是不要既包含判断的功能,又包含打印的功能)不然别人不屑于使用你的代码。
闰年的定义:
1 能被400整除
2 能被4整除且不能被100整除
int is_leap_year(int n)
{
if (n % 400 == 0)
{
return 1;
}
else if ((n % 100 != 0) && (n % 4 == 0))
{
return 1;
}
else
return 0;
}
int main()
{
int y = 0;
for (y = 1000; y <= 2000; y++)
{
if (is_leap_year(y) == 1)
{
printf("%d ", y);
}
}
return 0;
}
1700不满足闰年的条件,所以没有打印出来,可见我们的程序没有什么问题。
- 写一个函数,实现一个整形有序数组的二分查找。
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz-1;
while (left<=right)
{
int mid = (left + right) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid;
}
}
if (left > right)
return -1;
}
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int k =7;
//数组arr传参给binary_search时,传过来的其实是首元素地址,唯一的办法是,传参之前算出来!
int sz = sizeof(arr) / sizeof(arr[0]);
int ret=binary_search(arr, k,sz);
if (-1 == ret)
{
printf("找不到\n");
}
else
{
printf("找到了,下标是%d\n",ret);
}
return 0;
}
- 实现一个函数,打印乘法口诀表,口诀表的行数和列数自己指定
#include<stdio.h>
void multi_chart(int x)
{
for (int i = 1; i <= x; i++)
{
for (int j = 1; j <= i; j++)
{
printf("%d*%d=%4d ", 1, j, (i * j));
}
printf("\n");
}
}
int main()
{
int a = 0;
scanf("%d", &a);
multi_chart(a);
return 0;
}
5函数的返回值
函数的返回值可以是void、 char、 int 、float。并不是说void类型不能有返回值,他也可以有返回值。
void test1()
{
int n = 5;
printf("hehe\n");
if (n == 5)
return;//这之后的代码就不会再执行了
printf("haha\n");
}
6 函数的嵌套调用和链式访问
函数不能嵌套定义,但是能够嵌套调用。
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;
}
链式访问:把一个函数的返回值作为另一个函数的参数。
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
//打印的结果:4321
MSDN上解释了函数的返回值,返回的值为打印字符的个数,如果出现错误,返回一个负数。
printf()返回值为打印的字符的个数。先打印43.然后中间的printf函数打印2,返回值为1,然后打印1。
6 函数的声明和定义
6.1函数的声明
1 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
如果在后面定义,会爆出警告:某函数未定义。
如果实在是想放在后面,要在main函数之前声明:
//函数声明
int Add(int x, int y);
int main()
{
int a = 10;
int b = 40;
int ret = Add(a, b);//函数调用
printf("%d\n", ret);
return 0;
}
//函数定义
int Add(int x, int y)
{
int z = x + y;
return z;
}
3 函数的声明一般要放在头文件中的。
在实际写代码的时候,通常把函数的声明放在头文件(.h),把函数的定义放在.c文件而且调用的时候要引入自己的头文件 用双引号括起来。
首先创建一个add.c文件,写入add函数的代码:
int Add(int x, int y)
{
int z = x + y;
return z;
}
然后创建add.h文件,写上声明
#pragma once
int Add(int x, int y);
最后可以在主函数中调用:
#include"add.h"
int main()
{
int a = 10;
int b = 40;
int ret = Add(a, b);//函数调用
printf("%d\n", ret);
return 0;
}
如果大家都在test.c里面写程序,那么久乱套了。公司写代码不会把所有的程序写在test.c,会分模块去写:甲写加法模块,只用搞定add.c和add.h文件;乙写减法模块……最后再整合起来。
封装:将add.c和add.h编译,产生一个静态库——add.lib的文件。
右击鼠标①的位置,然后找到静态库。
这个文件使用记事本打开会是一个乱码。
这个时候可以把这个add.lib和add.h文件卖给其他公司。
导入静态库需要使用:#pragma comment(lib,“add.lib”)
6.2函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
7 函数递归
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
7.1递归的两个必要的条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
每一次调用函数都会在栈区开辟一个空间,如果没有限制条件,那么总有一次会出现栈溢出。
7.1.1练习
1 接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4
void Print(unsigned int n)
{
if (n > 9)//递归必须要有限制的条件,否则会由于栈溢出导致报错。
{
Print(n / 10);//Print(123)每一次递归之后越来越接近这个限制条件。
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);
Print(num);
}
这里使用123作为测试:
2 接受一个整型值(无符号),按照逆序打印它的每一位。
例如:
输入:1234,输出 4 3 2 1
void Print1(unsigned int n)
{
if (n > 0)
{
printf("%d ", n % 10);
Print1(n / 10);
}
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);
Print1(num);
}
3 编写函数不允许创建临时变量,求字符串的长度。
这个题目相当于已经暗示你了,创造递归函数!
int my_strlen1(char* s)
{
if (*s == '\0')
return 0;
else
return 1 + my_strlen1(s + 1);//s+1就是b的地址
}
int main()
{
//求字符串长度
char arr[] = "abc";//arr是数组名,数组名是数组首元素的地址
int len = my_strlen1(arr);//char*
printf("%d\n", len);
return 0;
}
7.2递归与迭代
1 求n的阶乘。(不考虑溢出)
int fac(int a)
{
if (a <= 1)
return 1;
else
return a * fac(a - 1);
}
其实就是照着这样子的公式写出来的。
2 求第n个斐波那契数。(不考虑溢出)
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间,大概跑了10分钟的时间。
如果我们把代码修改一下,计算一下第三个斐波那契数计算了多少次:
int count = 0;//全局变量
int fib(int n)
{
if(n == 3)
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
输入40,可以发现,仅仅是第三个斐波那契数就计算了102334155次,这里面浪费了大量的时间!
更简单的方法如下:
int fib1(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 = fib1(n);
printf("%d", ret);
return 0;
}
7.3 如何解决递归解决不了的问题?
- 将递归改写成非递归。
- 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代
nonstatic 局部对象(即栈对象),这不
仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保
存递归调用的中间状态,并且可为
各个调用层所访问。
8 青蛙跳台问题和汉诺塔问题
关于青蛙跳台问题,我觉得这位博主的描述非常的到位!
https://blog.csdn.net/lilililililiki/article/details/102616291
我的参考程序(非递归版)
int fib1(int n)
{
int a = 1;
int b = 2;
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 = fib1(n);
printf("%d", ret);
return 0;
}
关于汉诺塔问题:
参考谭浩强 C语言程序设计(第五版)185页,例7.8:
如果要把64个盘子从A座移动到C座,只需要完成以下三个步骤:
①把63个盘子移动到B座
②把最大的盘子从A移动到C座
③把B座的63个盘子移动到C座
但是如何把63个盘子移动到B座,又是一个问题啊?
任然执行以下三个步骤:
①把62个盘子移动到B座
②把最大的盘子从A移动到C座
③把B座的62个盘子移动到C座
以此类推
参考程序如下:
#include<stdio.h>
void move(char x, char y)
{
printf("%c-->%c\n", x, y);
}
//这里的one two three分别代表ABC的形式参数
void hannuo(int n, char one, char two, char three)
{
if (n == 1)
move(one, three);//只有一个盘子的时候,直接把A的盘子移动到C
else
{
hannuo(n - 1, one, three, two);
//意思是将n-1个盘,从A移动到B,也就是步骤一
move(one, three);
//执行步骤二
hannuo(n - 1, two, one, three);
//执行步骤三
}
}
int main()
{
int m = 0;
printf("请输入盘子的数量:");
scanf("%d", &m);
printf("移动%d个盘子的顺序:\n", m);
hannuo(m, 'A', 'B', 'C');//ABC为实参
}
实验结果: