C语言应该怎么玩——函数
一、函数的分类
1、库函数
为什么会有库函数?
比如将信息按照一定的格式打印到屏幕上(printf)、做一些字符串的拷贝工作(strcpy)、计算某个数的某次方的运算(pow)等,描述的这些基础功能,他们不是业务性的代码,但在开发的过程中每个程序员都可能用得到。为了支持可移植性和提高程序的效率,C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
注意:库函数必须知道的一个秘密:
使用库函数,必须包含 #include 对应的头文件。
这些库函数在不同的头文件中声明。比如:
math.h头文件中有:sin(x),cos(x),exp(x)(求e^x),fabs(x)(求x的绝对值)等库函数。
stdio.h头文件中有:scanf(),printf(),gets(),puts(),getchar(),putchar()等库函数。
string.h头文件中有:strcmp(),strcpy(),strcat(),strlen()等库函数。
2、自定义函数
库函数功能这么强大,为什么还要有自定义函数呢?
如果库函数能干所有的事情,那还要程序员干什么?所以更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数,但是不一样的是这些都是我们自己来设定,这给我们一个很大的发挥空间。
函数的组成:
ret_type fun_name (para1 )
{
statement;
}
下面提供中文版:
返回值类型 函数名 (函数参数)
{
语句项;
}
下面用代码来演示一下:
要求:写一个函数,可以找出两个整数中的最大值。
#include <stdio.h>
int get_max(int x, int y)
{
if (x > y)
return x;
else
return y;
}
int main()
{
int n1 = 10;
int n2 = 20;
printf("max = %d\n", get_max(n1, n2));//显示及结果为max = 20
return 0;
}
这里我们再看一个代码:
要求:写一个函数,交换两个整型变量的值。
void Swap(int x, int y)
{
int c = 0;
c = x;
x = y;
y = c;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
//显示结果为 交换前:a=10 b=20
//显示结果为 交换后:a=10 b=10
我们发现这个函数并没有完成交换两个值的任务,这是为什么呢?
先不着急,我们先来介绍一下函数参数和函数调用的概念。
二、函数参数
1、实际参数
实际参数又叫实参。真实传给函数的参数叫实参。实参可以是:常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传给形参。
2、形式参数
形式参数又叫形参。形式参数是函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了(生命周期跟局部变量是一样的)。因此形式参数只在函数中有效。
形参实例化以后相当于实参的一份临时拷贝。
三、函数调用
1、传值调用
传值调用是把实参的数值传给形参。
函数的形参和实参分别占有不同的内存块,如果进行传值调用,对形参的修改不会影响实参。
2、传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部的变量可以直接操作函数外部的变量。
了解完函数参数和函数调用的概念后,我们再来看之前那个要交换两个整型变量的值的函数。
传值调用的形式:
void Swap(int x, int y)
{
int c = 0;
c = x;
x = y;
y = c;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
//显示结果为 交换前:a=10 b=20
//显示结果为 交换后:a=10 b=20
传址调用的形式:
void Swap2(int* pa, int* pb)
{
int c = 0;
c = *pa;
*pa = *pb;
*pb = c;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
//显示结果为 交换前:a=10 b=20
//显示结果为 交换后:a=20 b=10
我们发现传址调用完成了交换的任务。
因为形参在实例化的时候才分配内存空间,这个空间与实参所在的空间的相互独立的。形参是实参的一份临时拷贝,如果进行传值调用,只把值传给形参,那函数交换的也只是形参所在内存空间里的值,与实参所在的内存空间没关系;如果进行传址调用,把实参的地址传过去,在函数内部进行解引用操作时,就是可以根据形参所在内存空间中存放的地址找到实参,也就是可以通过形参来操作实参。
四、函数的嵌套调用和链式访问
1、嵌套调用
void new_line()
{
printf("hehe ");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
//显示结果为hehe hehe hehe
函数可以嵌套调用,但是不能嵌套定义。
2、链式访问
把一个函数的返回值作为另一个函数的参数。
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d\n", strlen("abc"));
return 0;
}
//显示结果为3
五、函数的声明和定义
1、函数声明
1、告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关紧要。
2、函数的声明一般出现在函数的使用之前。要满足 :先声明,后使用。
3、函数的声明一般要放在头文件中。
2、函数定义
函数的定义是指函数的具体实现,交代函数的功能实现。
六、函数递归
什么是递归?
程序调用自身的编程技巧称为递归。
递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小。
1、递归的两个必要条件
1、存在限制条件,当满足这个限制条件后,递归便不再继续。
2、每次递归调用之后越来越接近这个限制条件。
代码演示:
要求:按照顺序,打印一个整型值的每一位。
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int n = 1234;
print(n);
return 0;
}
//显示结果为1 2 3 4
2、栈溢出
讲栈溢出之前,还是先进行代码演示:
void test(int n)
{
if (n < 10000)
{
test(n + 1);
}
}
int main()
{
test(1);
return 0;
}
在调试这个代码的时候就会发现这个栈溢出的问题。
在解释这个栈溢出的问题之前,我们先简单介绍一点关于内存的知识:
为了避免栈溢出,在写递归的时候要注意:
1、不能死递归,要有跳出条件,每次递归都要逼近这个跳出条件。
2、递归层次不能太深。