C语言程序设计(Part Ⅴ)的整理笔记,若有错误,欢迎指正。
C语言程序设计(Part Ⅳ)
C语言程序设计(Part Ⅵ)
- 函数就是功能,每一个函数用来实现一个特定的功能。在设计一个较大的程序时,往往把它分成若干个程序模块,每一个模块包含一个或多个函数,每一个函数实现一个特定的功能。用函数实现模块化程序设计便于分别编写、分别编译,提高调试效率。
- 一个C程序可由一个主函数和若干个其他函数构成。由主函数调用其他函数,其他函数也可以相互调用(但不能调用main函数)。同一个函数可以被一个或多个函数调用任意多次。
- 一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
- 所有函数都是平行的,即在定义函数时是分别进行的,是相互独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义(但可以相互调用)。
- 在程序中用到的所有函数,必须“先定义,后使用”。
定义函数应包括:
- 函数的名字。(应反映其代表的功能)
- 函数的类型。(即函数值的类型)
- 函数的参数的名字和类型。(无参函数不需要这项)
- 函数应当完成什么操作。(即函数的功能)
从用户使用的角度来看,函数有两种:
- 库函数 :它是由编译系统提供的,用户不必自己定义而可以直接使用它们(如:sqrt、fabs、sin、cos),但此时需要在本文件模块的开头写上:#include<math.h>
- 用户自定义函数:是用户根据实际需求自己设计的,用来实现用户指定的还能。
从函数的形式来看,函数分为两类:
- 无参函数 :一般用来执行一组单纯的操作,在调用无参函数时,主调函数和调用函数之间不发生传递的数据。
定义无参函数的一般形式为:
类型名 函数名()
{
函数体
}
例:
void print_message() //定义print_message
{
printf("Today is Saturday");
}
//函数为void类型,表示不需要带回返回值
- 函数体包括声明部分和执行语句部分。在定义函数时要用“类型名”指定函数值的类型,即函数带回来的值的类型。
- 声明部分包括对函数中用到的变量进行定义以及对将要在本函数中调用的函数进行声明等。
- 对函数的“定义”和“声明”不是一回事。
- 函数的定义是指对函数功能的确立,包括指定函数的名字,函数值类型、形参的类型以及函数体等,它是一个完整的、独立的函数单位
- “声明” 的作用则是把函数的名字、函数值类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如,函数名是否正确,实参与形参的类型和个数是否一致)。!它不包含函数体。
- 有参函数:在调用函数时,主调函数在调用被调函数时,通过参数向被调函数传递。一般情况下,执行被调函数时会得到一个函数值,供主调函数使用。
无参函数的调用形式为:函数名()
例:print_message();
定义有参函数的一般形式为:
类型名 函数名(形式参数表列)
{
函数体
}
例:
int max(int x,int y)
{
int z; //函数体中的声明部分
z=x>y ? x : y;
return (z);
}
- return(z)的作用是将z的值作为函数的值带回到主掉函数中,它又称为函数返回值。return后面的返回值两侧的圆括号可以省写,可简化为“return z;”或“return”。
- 当函数没有指明返回值,或没有返回语句时,函数返回一个不确定的值。为了使函数不返回任何值,可以使用void定义无类型函数。
有参函数的调用形式为:函数名(实参表列)
例:max(a,b);
- 如果实参表列包含多个实参,则各参数间用逗号隔开。
- 实参与形参的个数应相等,类型应匹配。
- 实参与形参按顺序对应,向形参传递数据。
- 在定义函数时函数名后面括号中的变量名称为形式参数(形参);在主调函数中调用一个函数时,函数名后面括号中的参数(可以是一个表达式)称为实际参数(实参)。
- 在定义函数中指定的形参,在未出现函数调用时,它们并不占用内存中的存储单元。在发生函数调用时,函数中的形参被分配内存单元。
- 调用结束,形参被释放。!实参单元仍保留并维持原值,没有改变。如果在执行一个被调用函数时,形参的值发生改变,不会改变主调函数的实参的值。
- C语言中,实参向形参的数据传递是“值传递”,单向传递,只有实参传给形参,而不能由形参传回来给实参。
例:输入4个整数,找出其中最大的数(用函数实现)
#include <stdio.h>
int main()
{
int max_4(int a, int b, int c, int d); //max_4函数的声明
int a, b, c, d, max;
printf("Please enter 4 integer numbers:");
scanf("%d %d %d %d",&a,&b,&c,&d);
max = max_4(a, b, c, d); //调用max_4函数,得到4个数的最大者,赋给变量max
printf("max=%d \n",max);
return 0;
}
int max_4(int a, int b, int c, int d) //定义max_4函数
{
int max(int x, int y); //max函数的声明
int m;
m = max(a,b); //调用max函数,找出a,b中的最大者
m= max(m, b); //调用max函数,找出a,b,c中的最大者
m= max(m, d); //调用max函数,找出a,b,c,d中的最大者
return m; //函数返回值是个数中的最大者
}
int max(int x, int y) //定义max函数
{
return(x > y ? x : y); //函数返回值是x,y中的最大者
}
- 在主函数(main函数)中要调用max_4函数,因此在主函数的开头要对max_4函数作声明;在max_4函数中三次调用max函数(嵌套调用),因此在max_4函数的开头要对max函数作声明。由于在主函数中没有直接调用max函数,因此在主函数中不必对max函数作声明。
- 从代码的4和13以及15和23行中可以看到,对函数的声明与函数的定义基本上是相同的,只差1个分号。因此可以简单地照写已定义的函数的首部,再加一个分号,就成为了对函数的“声明”。
- 由于函数声明与函数首部的一致,故把函数声明称为函数原型(function prototype)。
- 使用函数原型作声明是ANSI C的一个重要特点。用函数原型来声明函数,能减少编写程序时可能出现的错误。由于函数声明的位置与函数调用语句的位置距离比较近,因此在写程序时便于就近参照函数原型来书写函数调用,不易出错。
- 函数声明中的参数名可以省写,因为编译系统并不检查参数名,只检查参数类型。。 例:
int max(int , int )
- 如果被调用函数的定义出现在主调函数之前,可以不必加以声明(即把max函数与max_4函数放在main函数之前)。因为编译系统已经先知道了已定义的函数的有关情况,会根据函数首部提供的信息对函数的调用作正确性检查。
- 如果已在源文件模块的开头(在所有函数之前),已对本文件中所调用的所有函数进行了声明,它们的作用范围时全局性的,编译系统已由此可知各被调用函数的有关信息,因此不必在各函数中再对所调用的函数作声明。
函数的递归调用
- 嵌套调用和递归调用的区别:
- 在调用一个函数过程中调用另一个函数,称为函数的嵌套调用。
- 在调用一个函数过程中直接或间接调用本函数,称为函数的递归调用。
一个递归的问题可以分为“回溯”和“递推”两个阶段。
例:
f
a
c
(
n
)
=
{
1
,
(
n
=
0
,
1
)
n
⋅
(
n
−
1
)
!
,
(
n
>
1
)
fac(n)=\begin{cases} 1,(n=0,1) \\ n\cdot(n-1)!,(n>1) \\ \end{cases}
fac(n)={1,(n=0,1)n⋅(n−1)!,(n>1)
- 求解5!可分为两个阶段,“回溯(实线部分)”和“递推(虚线部分)”
- “回溯”:求解fac(5)要知道fac(4),求解fac(4)要知道fac(3), ⋯ \cdots ⋯,求解fac(2)要知道fac(1),而fac(1)已知是1,此时结束“回溯”,开始“倒推”。
- “倒推”:fac(2)=2·fac(1)=2,fac(3)=3·fac(2)=6, ⋯ \cdots ⋯,fac(5)=4·fac(4)=120,因此结束递归。
- 递推方法求
n
!
n!
n!
即从1开始,×2,再×3,…,一直到×n。这种方法容易理解,也容易实现。其特点是:从一个已知的事实出发(1!=1),按一定规律推出下一个事实,在从这个新的已知的事实出发,再向下推出一个新的事实…。
#include <stdio.h>
int main()
{
long fac(int n); //对fac函数进行声明
int n;
long fact; //变量fact用来存放n!的值
printf("Please enter a integer number: \n"); //输出一行信息,请用户输入n
scanf("%d", &n); //输入n
if (n < 0) //当输入为负值时,判为数据异常
printf("n<0,data error!");
else //当n为0或整数时,调用函数
{
fact = fac(n);
printf("%d!=%ld\n",n,fact); //输出n!
}
return 0;
}
long fac(int n) //定义fac函数
{
int i;
long fac = 1;
for (i = 1; i <= n; i++) //计算n!
fac *= i;
return fac; //将结果返回
}
- 递归方法求
n
!
n!
n!
递归的思想和递推是相反的,其特点是:直接从目标出发提出问题,从未知“回溯”到已知,再从已知“倒推”回未知。
#include <stdio.h>
int main()
{
long fac(int n);
int n;
long fact;
printf("Please enter a interger number: \n");
scanf("%d", &n);
if (n < 0)
printf("n<0,data error!");
else
{
fact = fac(n);
printf("%d!=%ld\n",n,fact);
}
return 0;
}
long fac(int n)
{
long fact;
if (n == 0 || n == 1) //0!和1!等于1
fact = 1;
else
fact = n*fac(n-1); //递归调用fac函数
return fact;
}
一个问题能否用递归方法处理,取决于以下三个条件:
- 所求解的问题能转化为同一方法解决的子问题,例如求n!可以转为(n-1)!×n。(n-1)!就是子问题,它的求解方法与n!是相同的。
- 子问题的规模比原问颗的规模小,如求(n-1)!比求n!的规模小,规模应是有规律地递减,表现在调用函数时,参数是递减的。如第一次调用fac(5),第二次调用fac(4),…
- 必须要有递归结束条件(边界条件),例如fac(1)=1,fac(0)=1。停止递归,否则形成无穷递归,系统无法实现。
数组作为函数参数
- 数组元素作函数参数
- 可以用变量作函数参数,显然,数组元素也可以作函数参数,其用法与变量相同。此外,数组名也可以作实参和形参,传递的是数组首元素的地址。
- 数组元素只能作函数的实参,而不能作为函数的形参。用数组元素作为函数实参,每次调用函数时,把数组元素的值传递给函数形参。其作用与用法与用变量作为函数实参是一样的。
- 数组名作函数参数
- 用数组名作为函数实参可以在函数中处理整个数组的元素
- !此时并不是将该数组中全部元素传递给所对应的形参。由于数组名代表数组的首地址,因此只是将数组的首个元素的地址传递给所对应的形参。
- 用数组名作函数实参时,不是把数组元素的值传递给形参,而是把实参数组的首元素的地址传递给形参数组,这样实参数组和形参数组就共占同一段内存单元。因此形参数组中各元素的值如果发生改变会使实参数组元素的值同时发生变化。
- 用数组名作为函数参数,在调用函数时并不另外开辟一个存放形参数组的空间。这一点和用变量作函数参数是不同的。
用函数实现对整数进行排序(冒泡法)
#include<stdio.h>
int main()
{
void ascend(int a[], int n);
void descend(int a[], int n);
int a[100] ; //任意个数冒泡排列(个数在100以内)
int i,n,flag;
printf("input a number :\n"); //输入需要进行冒泡排序的个数
scanf("%d", &n);
printf("\n");
printf("input %d numbers :\n", n); //输入需要进行冒泡排序的具体数值
for (i = 0; i < n; i++)
scanf("%d", &a[i]);
printf("\n");
printf("choose 1(ascending order) or 0(descending order) :\n",flag);
//选择升序还是降序,1代表升序,0代表降序
scanf("%d", &flag);
printf("\n");
if (flag == 1) //如果输入是1,则升序输出
ascend(a, n);
else //如果输入是0,则降序输出
descend(a, n);
printf("the sorted numbers :\n"); //输出排序后的结果
for (i = 0; i < n; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
void ascend(int a[], int n) //升序
{
int i, j, t;
for (j = 0; j < n-1; j++)
for (i = 0; i < n-1-j; i++)
if (a[i] > a[i + 1])
{ t = a[i]; a[i] = a[i + 1]; a[i + 1] = t; }
}
void descend(int a[], int n) //降序
{
int i, j, t;
for (j = 0; j < n - 1; j++)
for (i = 0; i < n - 1 - j; i++)
if (a[i] < a[i + 1])
{ t = a[i]; a[i] = a[i + 1]; a[i + 1] = t; }
}
用函数实现对整数进行排序(选择法)
顺序排序的思想:通过每一趟从待排序序列中选出值最小的元素,顺序放在已排列好序的有序子表的后面,直到全部序列满足排序要求为止。
比如有五个数:78, 21, 8, 14, 30,要求从小到大排序。
第一趟:
8, 21, 78, 14, 30(将五个数中最小的数与a[0]交换)
经过第一趟比较后, 5个数中最小的数已经在最前面了, 接下来只需比较后4个数, 依次类推
第二趟:
8, 14, 78, 21, 30(将后4个数中最小的数与a[1]交换)
经过第二趟比较后, 5个数中最小的2个数已经在最前面了, 接下来只需比较后3个数, 依次类推
第三趟:
8, 14, 21, 78, 30(将后3个数中最小的数与a[2]交换)
经过第二趟比较后, 5个数中最小的3个数已经在最前面了, 接下来只需比较后2个数, 依次类推
第四趟:
8, 14, 21, 30, 78(将后2个数中最小的数与a[3]交换)
排序完成
#include<stdio.h>
int main()
{
void ascend(int a[], int n);
void descend(int a[], int n);
int a[100]; //任意个数冒泡排列(个数在100以内)
int i, n, flag;
printf("input a number :\n"); //输入需要进行冒泡排序的个数
scanf("%d", &n);
printf("\n");
printf("input %d numbers :\n", n); //输入需要进行冒泡排序的具体数值
for (i = 0; i < n; i++)
scanf("%d", &a[i]);
printf("\n");
printf("choose 1(ascend) or 0(descend) :\n");
//选择升序还是降序,1代表升序,0代表降序
scanf("%d", &flag);
printf("\n");
if (flag == 1) //如果输入是1,则升序输出
ascend(a, n);
else //如果输入是0,则降序输出
descend(a, n);
printf("the sorted numbers :\n"); //输出排序后的结果
for (i = 0; i < n; i++)
printf("%d ", a[i]);
printf("\n");
return 0;
}
void ascend(int a[], int n) //升序
{
int i, j, m, t;
for (i = 0; i < n - 1; i++)
for (j = i + 1; j < n; j++)
{
m = i;
if (a[j] < a[m])
{
m = j; //记录每一趟中值最小的数组下标
t = a[i]; a[i] = a[j]; a[j] = t;
}
}
}
void descend(int a[], int n) //降序
{
int i, j, m, t;
for (i = 0; i < n - 1; i++)
for (j = i + 1; j < n; j++)
{
m = i;
if (a[j] > a[m])
{
m = j; //记录每一趟中值最大的数组下标
t = a[i]; a[i] = a[j]; a[j] = t;
}
}
}
- 调用函数时的虚实结合的方式有两类:
- 值传递方式(用变量名或数组元素名作函数参数)
- 地址传递方式(用数组名作函数参数)
- 值传递方式时,系统为形参另外开辟存储单元,实参与形参不是同一单元,因此形参的值改变不会导致实参值的改变,值传递是单向的,只能从实参传到形参,而不能由形参传到实参。
- 地址传递方式时,传递的是地址,系统不会为形参数组另外开辟一段内存单元来存放数组的值,而是使形参数组与实参数组共占同一段内存单元。由于这个特点,可以利用在函数中改变形参数组的值来改变实参数组的值。
- 从现象上看,好似传递是双向的,从实参传递到形参,又从形参传递到实参。但是从严格意义上说,传递仍然是单向的,仅仅传递的是地址而已。由于地址共享,才会出现改变形参数组的值也改变实参数组的值的现象。