目录
前言
函数是C语言的基本组成单元,充分发挥函数功能,可以使程序容易编写、阅读、调试和修改。本文是对函数的总结。
一、什么是函数?为什么要用函数?
1、函数是完成特定任务的独立程序代码单元。
2、使用函数的原因:
(1)使用函数可以省去编写重复代码的苦差事
(2)及时程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而增加程序代码的可读性,更方便后期修改、完善。
二、函数的分类
库函数和自定义函数
1.库函数
(1)如printf()、strlen()......,由C语言提供定义,编程时直接调用即可的函数。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
(2)总结:C语言常用的库函数有:IO函数(如:scanf()、printf())、字符串操作函数(如:strlen())、字符操作函数(如:tolower())、内存操作函数(如:memset())、时间/日期函数(如:time())、数学函数(如:sqrt())、其他库函数
(3)注意:使用库函数,必须包含 #include 对应的头文件。
2.自定义函数
程序员自己设计的函数
三、创建并使用简单的函数
void starbar (void); //函数原型
int main()
{
starbar(); //函数调用
printf("Hello World!\n");
starbar();
return 0;
}
void starbar(void) //定义函数
{
int count;
for(count = 1; count <= 12; count++)
{
putchar('*');
}
putchar('\n');
}
编译并运行程序,输出如下:
************
Hello World!
************
1、函数原型:告诉编译器函数的类型;函数定义:明确地指出函数要干什么;函数调用:表明在此处执行函数
主调函数:调用其它函数的函数;被调函数:被调用的函数
2、函数原型:
(1)函数原型指明了函数返回返回值类型和函数接受的参数类型。在本代码中,函数原型可以放在main()的前面,也可以放在main()里面的声明变量处
(2)当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。根据个人喜好,你也可以省略变量名。如:
void show_char(char ch, int num)也可以写成void show_char(char, int )。
(3)在函数原型中使用变量名并没有实际创建变量
(4)C语言规定函数要先先定义后使用。如果自定义函数放在主调函数的后面,就需要在函数调用之前加上函数原型声明(函数声明),有一种方法可以省略函数原型却保留函数原型的优点,即把自定义函数放在主调函数的前面。我们要明白使用函数原型声明的原因:为了让编译器在第一次执行到该函数之前就知道如何使用它。
(5)注意:函数原型声明是一条C语句,后面要加分号!
3、函数定义:
(1)函数定义的一般形式为:函数类型名 函数名(形式参数) //函数首部
{
函数实现过程 //函数体
}
(2)函数名:函数名是函数整体的称谓,需要使用一个合法的标识符表示。
(3)函数体:由一对大括号内的若干条语句组成,并用return语句返回运算的结果给主调函数,使用return语句的另一个作用就是,终止函数并把控制返回给主调函数的下一条语句。return 语句只能返回一个值。定义在函数中的变量是局部变量,属于函数私有,这意味着在其它函数使用同名变量不会引起名称冲突。
(4)函数类型:带返回值的类型应该与其返回值类型相同,没有返回值类型的函数应该声明为void类型。
(5)形式参数:函数定义的函数头中声明的变量,简称:形参。和定义在函数中的变量一样,形式参数也是局部变量,属于函数私有,这意味着在其它函数使用同名变量不会引起名称冲突。注意:每个变量之前都要声明其类型,即void dubs(int x, y, z)是无效的函数头,必须这样写void dubs(int x, int y, int z)
(5)注意:函数定义时的函数首部不是语句,后面不能跟分号
4、函数调用:
(1)函数调用过程:任何C程序执行,首先从主函数main()开始,如果遇到某个函数调用,主函数被暂停执行,转而执行相应函数,该函数执行玩完后将返回主函数,然后再从原先暂停的位置继续执行
(2)函数调用的形式:函数名(实际参数表)。
对于实现计算功能的函数,函数调用通常出现在两种情况:
①赋值语句:volume = cylinder(radius, height);
②输出函数的实参:printf("%f",cylinder(radius, height));
(3)实际参数:真实传给函数的参数,叫实际参数,简称实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
(4)参数传递:程序在遇到函数调用时,实参的值依次传给形参。形参和实参必须一一对应,两者数量相同,类型尽量保持一致。
(5)函数结果返回
(6)传值调用和传址调用:
①传值调用函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
②传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
如:看一段代码:
#include <stdio.h>
void mikado(int bah)
{
int pooh = 10;
printf("In mikado(),pooh = %d and &pooh = %p\n",pooh, &pooh);
printf("In mikado(),bah = %d and &bah = %p\n",bah, &bah);
}
int main()
{
int pooh = 2, bah = 5;
printf("In main(),pooh = %d and &pooh = %p\n",pooh, &pooh);
printf("In main(),bah = %d and &bah = %p\n",bah, &bah);
mikado(pooh);
return 0;
}
编译并运行代码,输出如下:
In main(),pooh = 2 and &pooh = 0xfff5fbff8e8
In main(),bah = 5 and &bah = 0xfff5fbff8e4
In mikado(),pooh = 10 and &pooh = 0x7fff5fbff8b8
In mikado(),bah = 2 and &bah = 0x7fff5fbff8bc
可以看到两个pooh地址不同和两个bah的地址也不同,这说明计算机把它们4个看成独立的变量。每个C函数都有自己的变量,这样做可以防止原始变量被被调函数中的副作用意外修改。
然后看一道题:写一个函数可以交换两个整形变量的内容。
#include <stdio.h>
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;
Swap1(num1, num2);
printf("Swap1:num1 = %d,num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("Swap2:num1 = %d,num2 = %d\n", num1, num2);
return 0;
}
编译并运行代码,输出如下:
Swap1:num1 = 1,num2 = 2
Swap2:num1 = 2,num2 = 1
①对于Swap1(): Swap1()下的结果显示num1和num2的值没有交换。问题不在Swap1函数里面,而在于把结果传回main()时。Swap1()使用的是普通变量的调用(值调用),在函数调用时,将实参num1,num2的值传递给形参x,y,Swap1()通过tmp将x和y值交换,但是当返回主调函数后,Swap1()中定义的变量都被销毁,而num1,num2的值并没有发生变化。
②对于Swap2():Swap2()的实参是num1,num2的地址,在函数调用时,将实参num1,num2的地址传递给形参px,py,px指向num1,换言之,px和num1代表同一内存单元,只要在函数中改变了*px的值,就改变了该存储单元的值,所以Swap2()可以达到交换值的目的
那么,这样可以交换num1,num2的值吗?
void Swap2(int *px, int *py)
{
int tmp = 0;
tmp = px;
px = py;
py = tmp;
}
答案是不能!我们要交换的是num1,num2的值,而不是它们的地址,Swap3()直接交换了px,py的值,由于和Swap1()同样的理由,px,py的改变并不会影响num1,num2的值。
四、函数的嵌套调用和链式访问
1.函数的嵌套调用:
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i;
for(i = 0 ;i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
编译并运行该代码,输出如下:
hehe
hehe
hehe
此处是main()调用three_line(),three_line()调用new_line()
注意:函数可以嵌套使用,但是不能嵌套定义,如:
#include <stdio.h>
int main()
{
void test()
{
printf("haha\n");
}
return 0;
{
这样的代码是错误的!!
2.函数的链式访问:
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
编译并运行该代码,输出如下:
4321
//先打印43,注意printf函数的返回值是打印在屏幕上字符的个数,43为2个字符,所以紧接着打印2;2为1个字符,所以打印1
五、编译多源代码文件的程序
使用头文件:把函数原型和#define定义的值放在头文件中,然后再每个源文件中使用#include指令包含该头文件即可。自定的头文件用“”,如:#include "hotel.h"
main.c源文件可以提供控制模块,hotel.c源文件可以提供函数支持模块(举个例子)
六、函数递归
1.什么是函数递归:函数自己调用自己(主要思考方式:大事化小)
2.递归的基本原理:
(1)每级函数调用都有自己的变量
(2)每级函数调用都会返回一次
(3)递归函数中,位于递归调用之前的语句,均按被调函数的顺序执行
(4)递归函数中,位于递归调用之后的语句,均按被调函数的相反的顺序执行
(5)虽然每级递归都有自己的变量,但是并没有拷贝函数的代码
(6)递归函数必须包含能让递归调用停止的语句
3.两个必要条件:
(1)存在限制条件,当满足这个限制条件的时候,递归便不再继续。
(2)每次递归调用之后越来越接近这个限制条件。
看一道题:把十进制正整数转换为二进制数
提示:十进制正整数转换为二进制方法:将十进制数除以2,得到的商再除以2,依次类推直到商为0或1时为止,同时在旁边标出各步的余数(0或1),最后倒着写出来。一般而言,对于数字n,其二进制的最后一位是n%2,因此,计算的第一位数字实际上是待输入二进制数的最后一位数字。这一规律提示我们,在递归调用之前计算n%2,在递归调用之后打印结果。当与2相除的结果小于2时停止计算,因为只要结果>=2,就说明还有二进制位。
#include <stdio.h>
void to_binary(int n)
{
int r;
r = n % 2;
if(n >= 2)
{
to_binary(n/2);
}
putchar(r == 0 ? '0' : '1');
}
int main()
{
int number;
scanf("%d",&number);
to_binary(number);
return 0;
}
编译并运行该代码:
9
1001
3.尾递归:把递归调用置于函数末尾,即正好在return语句之前。尾递归是最简单的递归形式。
4.递归与循环:
看一道题:求n的阶乘(两种方法实现)
#include <stdio.h>
int fact(int num) //循环实现
{
int i,ans;
ans = 1;
for(i = 1; i <= num; i++)
{
ans *= i;
}
return ans;
}
int factorial(int num) //递归实现
{
if(num <= 1)
return 1;
else
return num* factorial(num-1);
}
int main()
{
int n;
scanf("%d",&n);
printf("%d\n",fact(n));
printf("%d\n",factorial(n));
return 0;
}
编译并运行该代码:
5
120
120
既然循环和递归都可以计算,那么我们选哪一个呢?选择循环,每次递归都会创建一组变量,所以递归用的内存更多,而且每次递归调用都会把创建的一组新变量放在栈中,递归调用的数量受限于内存空间,每次递归调用都要花费一定的时间,所以递归执行的速度较慢。但是,在某些情况下,不能用简单的循环代替递归。
5.递归的优点和缺点:
(1)优点:为某些编程问题提供了最简单的解决方案
(2)缺点:一些递归算法会快速消耗计算机的内存资源;不方便阅读和维护
看一道题:求第n个斐波那契数
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
该函数使用了双递归,即每一级递归都要都要调用本身两次,这暴露了一个问题,每一级递归创建的变量都是上一级递归调用的两倍,变量的数量在呈指数增长,很快就会消耗掉计算机的大量内存,使程序崩溃。