C函数

2015/09/17 C

C函数

函数的基本认识

软件开发是相当耗时、复杂的工作,当需求和功能越来越多时,程序代码就会越来越庞大。这时多人分工合作完成软件开发就势在必行。另外,每次修改一小部分程序代码就要将成千上万行程序代码重新编译,这样的做法明显效率低下。如果程序中有许多类似的部分,一旦日后要更新,必定会增加更新难度。

应该如何解决上述问题呢?C语言中提供了相当方便实用的函数功能,可以让程序更加结构化、模块化。函数是一段程序语句的集合,可以给予一个名称代表程序代码的集合。想象一下,如果在一个程序中有许多相似的程序代码,就要组织得更有条理,否则会造成相当紊乱、复杂且难懂的程序流程。

认识函数

函数就像是一部机器或一个黑盒子。数学中就有函数,数学上函数的形式如下:

y = f(x)

其中,x代表输入的参数,f(x)是x的函数,y是针对一个特定值(x)所得到的结果与返回参数。如果这样的例子太过抽象,可以使用生活中的例子来说明,假设有一个函数“空调”,此函数的抽象形式为:

凉风 = 空调(开机指令)

当用户对“空调”这个函数输入“开机”指令后,空调就会吹出凉风。假设有另一个函数“微波炉”,“微波炉”函数的抽象形式为:

热过的快餐 = 微波炉(冷的快餐, 开机指令, 结束时间)

模块化设计精神

“模块化”设计精神是采用结构化分析方式自上而下逐一分析程序,并将大问题逐步分解成各个小问题,然后将这些小问题分别交由不同的程序员进行程序代码的编写。

模块化与结构化的概念并不是只有程序设计中才有,在实际的企业组织与制造工业中,这种概念已经存在很多年了。根据功能将组织分化为各个部门,不但管理起来更加方便,而且更容易掌握企业或公司运营的情况。 政府机关就是根据功能来划分各个部委的,例如税务方面的问题一般会到国税局或地税局等部门咨询,寄信或取汇款会到邮局,处理户口的事务会到就近的派出所。如果没有根据功能来划分,民众办理这些事务时就会一头雾水,极为不便。 同样,在工业上也有类似的例子,许多机器会被区分为功能独立的各个部分,一旦某个部分出现故障,通过更换故障零件就可以继续使用。

函数的使用好处

模块化的概念也沿用到了程序设计中,就是函数的实现。C语言的主程序就包含最大的函数main(),不过如果C程序只使用一个main()函数,就会降低程序的可读性并增加结构规划上的困难。 例如,一个大型程序是根据功能来划分的,如果某个功能有问题,就可以针对该部分进行修改或更换,这种方法可以让大家分工合作、共同开发程序,最后统一编译。 使用函数的好处相当多,只要对程序善加规划,就能让程序更精简、容易维护,函数的好处有以下3点:

  • 避免造成相同程序代码的重复出现。
  • 让程序更加清晰明了,降低维护时间与成本。
  • 将较大的程序分割成多个不同的函数,可以独立开发、编译,最后连接在一起。

函数的使用

C语言的函数可分为系统提供的标准函数和用户自行定义的自定义函数两种。使用标准函数时,只要将相关函数的头文件包含(include)进来即可。自定义函数是用户根据需求设计的函数

函数原型声明简介

C程序在编译时采用自上而下的顺序,如果在函数调用前没有编译过这个函数的定义,C编译程序就会返回函数名称未定义的错误。这时必须在程序未调用函数时声明函数的原型(Prototype),告诉编译程序有函数的存在。

大家只要通过函数声明说明这个函数名称、传入参数与返回参数,让编译程序知道这个函数存在且有完整的定义,就可以供整个程序甚至整个软件项目使用。语法格式如下:

返回值类型 函数名称 (参数类型1 参数1,参数类型2, …,参数类型n 参数n );

用户可以自行定义参数个数与参数的数据类型,并指定返回值的类型。如果没有返回值,通常会使用以下形式:

void 函数名称 (参数类型1 参数1,参数类型2, …,参数类型n 参数n );

如果没有任何需要传递的参数,同样以void关键字表示。有返回值但没有参数的形式如下:

返回值类型 函数名称 (void);

以下是没有返回值也没有参数的函数:

void 函数名称 (void);

没有函数原型声明也能使用吗?事实上是可以的。只要在使用前让编译程序知道有关函数的定义就可以使用;如果在使用前没有定义,就不能顺利通过编译。也就是说,如果调用函数的程序代码位于自定义函数的定义后,就可以不事先声明函数,代码如下:

void GetFact();  /* 函数原型声明与定义在调用前,可省略原型声明 */
{
  函数主体;
     :
}
int main()
{
  程序主体;
     :
  GetFact();  /*调用函数*/
}

一般会将函数原型声明放置于程序开头,通常位于#include与main()之间。函数原型声明的语法格式有以下两种:

返回数据类型 函数名称(数据类型 参数1, 数据类型 参数2, …);
或
返回数据类型 函数名称(数据类型, 数据类型,…);

例如,一个函数sum()可接收两个成绩参数,并返回最后计算总和的值,原型声明如下:

int sum(int score1,int score2);
或
int sum(int, int);

从开始学习C语言至今使用过许多内建的标准函数,例如printf()、scanf()等。如果要输出“Hello World”字符串,那么可以通过调用printf(“Hello World”);函数来完成; 如果要用户输入一个整数并存至整数变量value中,那么可以使用scanf(“%d”,&value);。其中,Hello World字符串与%d、&value等都算是一种参数。

大家或许思考过为何printf()与scanf()等函数在程序中没有事先声明与定义?事实上是有的。在程序的开端有一个#include,表示将头文件(stdio.h)引入。 如果查看include路径下的stdio.h文件,就可以发现printf()函数与scanf()函数都有声明:

_CRTIMP int __cdecl  printf (const char*, ...);
_CRTIMP int __cdecl  scanf (const char*, ...);

这些内建标准函数的定义已经分别编译为函数库文件(例如.lib文件、.dll文件),所以内建函数有声明、有定义才能被调用。

函数的定义

清楚了函数的原型声明后,接下来学习如何定义一个函数的主体架构。 函数定义是函数架构中最重要的部分,定义一个函数的内部流程包括接收什么参数、进行什么处理、在处理完成后返回什么数据等。

如果只有函数的声明没有函数的定义,这个函数就像一部空有外壳而没有实际运行功能的机器一样,根本无法使用。 自定义函数在C语言中的定义方式与main()函数类似,基本架构如下:

返回值类型 函数名称 (参数类型1 参数1, 参数类型2,  …, 参数类型n 参数n )
{
函数主体;
...
return返回值;
}

一般来说,使用函数大多都是处理计算的工作,因此需要把结果返回给函数的调用者,在定义返回值时不能使用void,一旦指定函数的返回值不为void,在函数中就要使用return返回一个数值,否则编译程序将汇报错误。如果函数没有返回值,就可以省略return语句。

函数将结果返回时必须指定一个数据类型给返回值,接收函数返回值时存储返回值的变量或数值的类型必须与函数定义的返回值类型一样。返回值的使用格式如下:

return返回值;

函数名称是定义函数的第一步,由设计者命名,命名规则与变量命名规则一样,最好具备可读性,要避免使用不具任何意义的字母组合作为函数的名称,例如bbb、aaa等。

在函数名称后面括号内的参数行不能像原型声明时一样只写各个参数的数据类型,务必同时填上每一个数据类型与参数名称。函数主体由C语言的语句组成,在程序代码编写的风格上,建议大家尽量使用注释说明函数的作用。

函数的调用

创建好函数后就可以在程序中直接调用该函数的名称执行了。在进行函数调用时,只要将需要处理的参数传给该函数,安排变量接收函数运算的结果,就可以正确无误地使用函数。

函数返回值不但可以代表函数的运行结果,还可以用来检测函数是否成功地执行完毕。函数调用的方式有两种,如果没有返回值,就直接使用函数名称调用函数。语法格式如下:

函数名称(参数1, 参数2,…);

如果函数有返回值,就可以运用赋值运算符(=)将返回值赋值给变量。语法格式如下:

变量=函数名称(参数1, 参数2,…);

下面的范例程序将说明函数的基本定义与调用方法,包括函数的原型声明、函数调用及函数主体架构的定义,此函数要求用户输入两个数字,并比较哪一个数字较大。如果输入的两个数字一样大,输出任意一个数字即可。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int mymax(int,int); /*函数原型声明*/
05  
06  int main()
07  {     
08    int a,b;
09    printf("数字比大小\n请输入a:");
10    scanf("%d",&a);
11    printf("请输入b:");
12    scanf("%d",&b);
13    printf("较大者的值为:%d\n",mymax(a,b));/*函数调用*/
14    system("PAUSE");
15    return 0;
16  }
17  
18  int mymax(int x,int y)
19  { /*函数定义主体*/
20    if(x>y)
21      return x;
22    else
23      return y;
24  }  
  • 第4行:在main函数前,int mymax(int,int)是函数的原型声明。
  • 第13行:在main函数中,为了使用mymax函数,必须调用mymax(a,b)函数,并将a与b作为参数传递给mymax函数。
  • 第18~24行:是函数定义的主体。
  • 第21~23行:使用>符号判断究竟是x大还是y大,并输出较大的值。

参数传递方式

C语言提供了让程序员相当方便的自定义函数功能。自定义函数有许多好处,能提高程序的可读性与可维护性等,因此学会使用函数绝对是必要的。不过,许多C语言的初学者对于使用函数常有疑义,主要是因为对于C语言所提供的参数传递方式不太明白。事实上,传递参数的概念并不难理解,关键在于是否会改变参数本身的内容。

参数的意义

之前提到过,变量存储在系统内存的地址上,而地址上的数值和地址是独立分开的,所以更改变量的数值不会影响变量的地址。函数的参数传递功能主要是将主程序中调用函数的参数值传递给函数中的参数。 这种关系有点像棒球中投手与补手的关系,一个投球一个接球。其中的参数是“实际参数”(Actual Parameter),也就是实际调用函数时所提供的参数。我们通常所说的参数是“形式参数”(Formal Parameter),也就是在函数定义标头中所声明的参数。

一般来说,C语言中函数调用时参数传递的方式可以分为“传值调用”与“传址调用”两种。至于调用函数时所传入的参数本身是否会被更改,如何指定是否更改,都是通过地址与指针解决的。 如果希望传入的参数不被更改,将该变量的数值传给函数即可。另一方面,如果希望传入的参数被更改,只要将该变量的地址传给函数即可。

传值调用

传值调用方式并不会更改原先主程序中调用变量的内容(变量的值),也就是主程序调用函数的实际参数时,系统会将实际参数的数值传递并复制给函数中相对应的形式参数。 C语言默认的参数传递方式是传值调用,传值调用的函数原型声明如下:

返回数据类型 函数名称(数据类型 参数1, 数据类型 参数2,…);
或
返回数据类型 函数名称(数据类型, 数据类型,…);

传值调用的函数调用形式如下:

函数名称(参数1,参数2,…);

接下来使用以下范例说明传值调用的基本方式,目的在于将两个变量的内容传给自定义函数swap_test()以进行交换,不过不会对参数进行修改,所以不会实现变量内容交换的功能。

首先声明一个函数void swap_test(int,int),该函数仅接受以传值调用方式传入的参数。因此,调用swap_test时传入的a与b仅是将两个变量的数值复制一份副本。

原本a与b的数值是10与20,在调用swap_test函数后,仅对函数中的x与y进行交换,即x与y的数值原本是10与20,交换后x为20、y为10,不过这个函数并不会对参数进行修改,所以不会实现变量值交换的功能,请大家仔细观察输出的结果。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  void swap_test(int,int);/*传值调用函数*/ 
05  
06  int main()
07  {
08    int a,b;
09    a=10;
10    b=20;/*设置a,b的初值*/ 
11    printf("函数外交换前:a=%d, b=%d\n",a,b);
12    swap_test(a,b);/*函数调用 */ 
13    printf("函数外交换后:a=%d, b=%d\n",a,b);
14    
15      system("PAUSE");
16       return 0;
17  }
18  
19  void swap_test(int x,int y)/* 无返回值 */ 
20  {
21    int t;
22    printf("函数内交换前:x=%d, y=%d\n",x,y);
23    t=x;
24    x=y;
25    y=t;/* 交换过程 */ 
26    printf("函数内交换后:x=%d, y=%d\n",x,y);
27  }  
  • 第4行:传值调用函数的原型声明。
  • 第9、10行:设置a、b的初值。
  • 第12行:函数调用语句。
  • 第19行:无返回值的函数。
  • 第23~25行:x与y数值的交换过程。

下面的范例程序说明全局变量与函数中局部变量的关系,首先声明全局变量与局部变量并设置其数值,当全局变量与函数中局部变量具有相同的名称时,在函数中会优先局部变量。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int a=20; /* 全局变量 */
05  int b=50; /* 全局变量 */
06  void fun1();
07  
08  int main()
09  {
10      int a=10; /* 局部变量 */
11      printf("主程序中,a=%d,b=%d\n",a,b);
12      fun1();
13      
14      system("PAUSE");
15      return 0;
16  }  
17  
18  void fun1()
19  {
20      int a=30; /* 局部变量 */
21      printf("函数 fun1 中,a=%d,b=%d\n",a,b);
22  } 
  • 第4、5行:声明a与b为全局变量。
  • 第10行:再次声明a为局部变量。
  • 第11行:整数b为全局变量,且在main()函数中没有同样名称的变量,因而显示b变量为50。
  • 第10、20行:在main()函数与fun1()函数中都有自定义a变量的值,故只能显示局部变量的值。

传址调用

C函数的传址调用表示在调用函数时,系统并没有另外分配实际的地址给函数的形式参数,而是将实际参数的地址直接传递给所对应的形式参数。

在C语言中要进行传址调用必须声明指针变量作为函数的参数,因为指针变量用来存储变量的内存地址,调用的函数在调用参数前必须加上&运算符。传址方式的函数声明形式如下:

返回数据类型 函数名称(数据类型 *参数1, 数据类型 *参数2,…);
或
返回数据类型 函数名称(数据类型 *, 数据类型 *,…);

传址调用的函数调用形式如下:

函数名称(&参数1,&参数2,…);

如何修改才能让主程序中的a与b通过swap_test()函数进行数值的交换呢?很简单,只要将函数修改为传址调用的形式就能解决该问题,让两个数值确实交换。

我们可以将函数的声明修改为void swap_test(int,int),指定传入的参数必须是两个整数地址,并以两个整数指针x与y接收参数,这样就可以真正更改两个变量的内容(值)了

以下程序是传址调用的基本范例,其他传址调用的函数结构也都大同小异。可以通过自定义函数void swap_test(int,int)指定传入的参数必须是两个整数地址,并以两个整数指针x与y接收参数,从而更改两个变量的值或内容。

01  include <stdio.h>
02  #include <stdlib.h>
03  #include <string.h>
04  
05  void swap_test(int *,int *);/*函数的传址调用 */ 
06  
07  int main()
08  {
09    int a,b;
10    a=10;
11    b=20;
12    printf("函数外交换前:a=%d, b=%d\n",a,b);
13    swap_test(&a,&b);/* 传址调用 */ 
14    printf("函数外交换后:a=%d, b=%d\n",a,b);
15    
16      system("PAUSE");
17       return 0;
18  }
19  
20  void swap_test(int *x,int *y)
21  {
22    int t;
23    printf("函数内交换前:x=%d, y=%d\n",*x,*y);
24    t=*x;
25    *x=*y;
26    *y=t;/* 交换过程 */ 
27    printf("函数内交换后:x=%d, y=%d\n",*x,*y);
28  
29  }  
  • 第5行:函数的传址调用,指定传入的参数必须是两个整数的地址,并以两个整数指针x与y接收参数。
  • 第13行:必须加上&运算符来调用参数。
  • 第24~26行:如果要交换数据就必须使用运算符,因为x与y是整数指针,必须通过运算符存取其值或内容。

数组参数的传递

当函数中要传递的对象不止一个时(例如数组数据),可以通过地址与指针的方式进行处理并得到结果。由于数组名存储的值就是数组第一个元素的内存地址,因此可以直接使用传址调用的方式将数组指定给另一个函数,这时如果在函数中改变了数组内容,所调用的主程序中的数组内容也会随之改变。

由于数组大小必须根据所拥有的元素个数决定,因此在数组参数传递中最好可以加上传送数组长度的参数。一维数组参数传递的函数声明如下:

(返回数据类型or void)  函数名称 (数据类型 数组名[ ], 数据类型 数组长度…);
或
(返回数据类型 or void) 函数名称(数据类型 *数组名, 数据类型 数组长度…);

一维数组参数传递的函数调用方式如下:

函数名称 (数据类型 数组名, 数据类型 数组长度…);

下面的范例程序是将一组维数array以传址调用的方式传递给Multiple()函数,在函数中将每个一维arr数组中的元素值都乘以10,同时改变主程序中array数组的元素值。

01  #include <stdio.h>
02  #include <stdlib.h> 
03  
04  void Multiple(int arr[],int);    /* 函数Multiple()的原型 */
05  
06  int main()
07  {
08     int i,array[6]={ 1,2,3,4,5,6 };
09     int n=6;
10     
11     printf("调用Multiple()前,数组的内容为: ");   
12     for(i=0;i<n;i++)  /* 打印出数组的内容 */
13        printf("%d ",array[i]);
14     printf("\n");
15     Multiple(array,n);       /* 调用函数Multiple2() */
16     printf("调用Multiple()后,数组的内容为: "); 
17     for(i=0;i<n;i++)  /* 打印出数组的内容 */
18        printf("%d ",array[i]);
19     printf("\n");
20         
21     system("pause");
22     return 0;
23  }
24  
25  void Multiple(int arr[],int n1)
26  {
27     int i;
28     for(i=0;i<n1;i++)  
29        arr[i]*=10;
30  }
  • 第4行:函数的原型声明,传递一维数组arr[]与一个整数,在[]中的长度可写也可不写。
  • 第12、13行:输出array数组中所有元素。
  • 第15行:直接用数组名,也就是传递数组地址调用函数Multiple()。
  • 第25~30行:定义Multiple()函数主体。

多维数组参数传递的精神和一维数组大致相同,例如传递二维数组,只要加上一个维数大小的参数即可。还有一点要特别提醒大家,所传递数组的第一维可以不用填入元素的个数,不过其他维数都要填上元素的个数,否则编译时会产生错误。二维数组参数传递的函数声明形式如下:

(返回数据类型or void) 函数名称(数据类型 数组名[ ][列数], 数据类型 行数,数据类型 列数…);

二维数组参数传递的函数调用如下:

函数名称(数据类型 数组名, 数据类型 行数, 数据类型 列数…);

下面的范例程序是将二维数组score以传址调用的方式传递给print_arr()函数,并在函数中输出数组中的每个元素,请注意函数声明与调用时二维数组的表示方法。

01  #include<stdio.h>
02  #include<stdlib.h>
03  
04  /*函数原型声明,第一维可省略,其他维数的下标都必须清楚定义长度*/
05  void print_arr(int arr[][5],int,int);
06  
07  int main()
08  {  
09    /*声明并初始化二维成绩数组*/
10    int score_arr[][5]={ {59,69,73,90,45},{81,42,53,64,55} };
11    print_arr(score_arr,2,5);/*传址调用并传递二维数组*/ 
12      
13      system("pause");  
14     return 0;  
15  }
16  
17  
18  void print_arr(int arr[][5],int r,int c)
19  {  
20    int i,j;
21    for(i=0; i<r; i++)
22    {
23      for(j=0; j<c;j++)
24           printf("%d  ",arr[i][j]);/*输出二维数组各元素的函数*/
25           printf("\n");
26    }
27  }
  • 第5行:第一维元素的个数可以不用定义,其他维数的下标都必须清楚定义长度。
  • 第10行:声明并初始化二维成绩数组。
  • 第11行:传址调用并传递二维数组。
  • 第18~27行:定义print_arr()函数的主体。
  • 第24行:输出二维数组各元素的值。

递归的作用

递归是一种很特殊的算法。对程序员而言,“函数”不只是能够被其他函数调用(引用),在某些程序设计语言中还提供了自身调用(引用)功能,也就是所谓的“递归”。递归在早期人工智能所用的程序设计语言中(如Lisp、Prolog)几乎是整个语言运行的核心,当然在C语言中也提供了这项功能,因为绑定时间(Binding Time)可以延迟至执行时才动态决定。

什么时候才是使用递归的最好时机呢?递归只能解决少数问题吗?事实上,任何可以用选择结构和循环结构编写的程序都可以用递归表示和编写,而且更加具有可读性。

定义递归函数

递归(Recursive)函数的精神是在函数中调用自己。假如一个函数是由自身所定义或调用的,就称为递归(Recursion)。递归至少要定义两种条件,包括一个可以反复执行的递归过程和一个跳出执行过程的出口。在C语言中建立递归函数的3大条件是起始状态、终止条件以及执行流程。递归函数必须指明终止条件,如果没有清楚指明终止条件,程序将会无穷无尽地运行下去,造成无限循环。

例如,阶乘函数是数学上很有名的函数,可以看成是递归的典型应用。数据结构中二叉树(Binary Tree)的遍历问题也可以使用递归,因为二叉树的子节点个数以2的次幂为基数,难以用单纯的循环结构完成遍历。

提示 “尾递归”(Tail Recursion)就是程序的最后一条语句为递归调用,因为每次调用后回到前一次调用的第一行语句就是return,所以不需要进行任何计算工作。

以下范例程序将使用一个求n阶乘(n!)的结果来说明递归的用法。在这个程序中会同时使用循环与递归的方式,借此比较两种方式的差异。这个程序要求用户输入n的大小,求得1×2×3×…×n的结果。例如n=4,则1×2×3×4=24。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int ndegree_rec(int);/*递归函数*/ 
05  int ndegree_loop(int);/*循环函数*/ 
06  
07  int main()
08  {   
09      int n;
10      printf("请输入n值:");
11      scanf("%d",&n);/*输入所求n!的n值*/ 
12      printf("%d!的循环版为%d,递归版为%d\n",n,ndegree_loop(n),ndegree_rec(n));
13      
14      system("PAUSE");
15      return 0;
16  }
17  
18  int ndegree_loop(int n)
19  {
20    int result=1;
21    do{
22        result*=n;
23        n--;
24      }while(n>0);/*使用do while控制*/ 
25      
26      return result;/*返回结果值*/ 
27  }  
28  
29  int ndegree_rec(int n)
30  {
31      if(n==1)
32       return 1;/* 跳出反复执行过程中的出口 */  
33      else  
34       return n*ndegree_rec(n-1);/* 反复执行的过程 */
35  }
  • 第4行:递归函数的原型声明。
  • 第5行:循环函数的原型声明。
  • 第11行:用于输入要计算的阶乘数。
  • 第21~24行:使用do while控制与计算。
  • 第26行:返回结果值。
  • 第29~35行:定义递归函数的程序代码。
  • 第32行:跳出反复执行过程中的出口。
  • 第34行:如果用户输入的数值大于1,就继续计算这个n值乘上(n-1)!的结果,ndegree_rec(n-1)部分会将n-1的值当成参数继续调用ndegree()函数。

Search

    微信好友

    博士的沙漏

    Table of Contents