- 首先,什么是函数?函数(function)是用于完成特定任务的程序代码的自包含单元。尽管 C 中的函数和其他语言中的函数、子程序或子过程等扮演着相同的角色,但是在细节上会有所不同。某些函数会导致执行某些动作,比如 printf()可使数据呈现在屏幕上;还有一些函数能返回一个值以供程序使用,如strlen()将指定字符串的长度传递给程序。一般来讲,一个函数可同时具备以上两种功能。
为什么使用函数?第一,函数的使用可以省去重复代码的编写。如果程序中需要多次使用某种特定的功能,那么只需编写一个合适的函数即可。程序可以在任何需要的地方调用该函数,并且同一个函数可以在不同的程序中调用,就像在许多程序中需要使用 putchar()函数一样。第二,即使某种功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数使得程序更加模块化,从而有利于程序的阅读、修改和完善。例如,假设您想编写一个实现以下功能的程序:
读入一行数字。
对数字进行排序。
找出它们的平均值。
打印一个柱状图。
可以编写下一个程序 1:#include<stdio,h> #define SIZE 50 int main(void) { float list[SIZE]; readlist(list,SIZE); sort(list,SIZE); average(list,SIZE); bargraph(list,SIZE); return 0; }
当然,readlist()、sort()、average()、bargraph() 的实现细节需要自己编写。接下来,我会正式地跟您讲解函数的编写和使用
-
1. C 语言函数的定义、原型和调用
-
1.1 函数的定义
- 函数定义(function definition):
<函数类型> 函数名(<参数类型> 参数名,...) { 执行语句 }
其中函数类型如之前所讲的变量类型基本一致,例如:int、char、int*... ... 但值得一提的是,函数类型可以是 void 类型,即不需要返回任何值的,而其他的均需要用 return 语句返回值,否则可能会出现报错。
函数名和参数名的取名规则与前面所讲的变量名一致(回忆入口)。有细心朋友可能发现,在使用有些函数时()中是没有添加任何值的,比如 getchar()函数。而有些函数的()中有不定个参数,比如 printf()函数。这说明函数的()中的参数的个数可以是任意的。当然,这指的是在你还没定义前。
执行语句是您要实现函数的具体细节。 -
1.2 函数的原型
- 函数原型(function prototype):
<函数类型> 函数名(<参数类型> 参数名,...);
函数原型的作用主要是提醒编译器存在这个函数,因为,编译器的编译器往往是按照从上往下开始编译的,如果您在运用某个函数时,若没有声明函数原型,则可能会出现编译错误。当然,声明函数原型时,必须在使用该函数前的位置,否则会出现相同错误。
并且,在声明函数类型时,参数名可以省略。 -
1.3 函数的调用
- 函数调用(function call):
函数名(参数,...);
在调用函数时,不可与上面所讲的函数原型搞混了,不能自己添加函数类型和参数类型。注意,要保证参数个数与函数原型中的参数个数相同,且对应参数类型一致。
-
1.4 函数的返回值
- 函数的返回值的类型需要和函数类型相同。若不一致,则会将返回值类型强制转换成函数类型,而这往往容易出现报错。
- 程序 2:
#include <stdio.h> #define NAME "GIGATHINK, INC. " #define ADDRESS "101 Megabuck Plaza" #define PLACE "Megapolis, CA 94904" #define WIDTH 40 void starbar (void);/* 声明函数原型*/ int main (void) { starbar (); printf ("%s\n", NAME); printf ("%s\n", ADDRESS); printf ("%s\n",PLACE); starbar (); /*使用函数*/ return 0; } void starbar (void) /*定义函数*/ { int count; for (count = 1; count <= WIDTH; count ++) putchar ('*'); putchar ('\n'); }
结果:
****************************************
GIGATHINK, INC.
101 Megabuck Plaza
Megapolis, CA 94904
****************************************
-
-
2. 函数间的数据传递
-
在讲函数的数据传递之前,我们需要了解一下什么是形式参数和实际参数。
形式参数(formal argument)或形式变量(formal parameter):就如同函数内部定义的变量一样,形式参数是局部变量,它们是函数所私有的。
实际参数(actual argument):在函数调用中给形式参数赋值。
总的来说,实际参数是函数调用时出现在圆括号中的表达式。而形式参数则是函数定义中在函数头部声明的变量。void show_n_char(char ch,int num); //ch 和 num 均是形式参数 void show_n_char(SPACE,12); //SPACE 和 12 均是实际参数
-
2.1 值传递
- 形式参数的类型不是指针类型是值传递。在函数的调用时,往往是自己创造新的栈区来储存变量。而在您作为新手时调用函数的结果往往会出乎您的意料。就如下程序:
- 程序 3:
#include <stdio.h> void swap(int ,int); int main(void) { int a = 10, b = 20; swap(a,b); printf("%d %d\n", a, b); return 0; } void swap(int a,int b) { int c; c = a; a = b; b = c; }
结果:
10 20
显然,我们编写的交换函数 swap()并没有起到作用。这是为什么呢?原来,在 main()函数内部的实际参数 a、b 并不等同于 swap()函数内部的形式参数 a、b 。在调用函数 swap()函数时,相当于在系统内部重新建了两个小盒子 a、b,这两个小盒子尽管与对应的在 main()函数内部的两个名为 a、b 长得一模一样,但是它们依然是不等同的,因为它们之间存在一点不一样——位置(地址)。所以它们是不等同的。若要学习怎么用函数进行两个数的交换,可以用下面所讲的址传递。 -
2.2 址传递
- 形式参数的类型是指针类型是址传递。(本章 4.指针简介 会简单介绍指针)
- 程序 4:
#include <stdio.h> void swap(int*, int*); int main(void) { int a = 10, b = 20; swap(&a, &b); printf("%d %d\n", a, b); return 0; } void swap(int* a, int* b) { int c,*p=&c; *p = *a; *a = *b; *b = *p; }
结果:
20 10
在 C 语言中,如果没有其他的指针操作,地址一般是不会改变的。而 '*' 是对地址的解引用操作,相当于根据地址追踪原本的变量。当然,两个数的交换还可以用其他方法,但这里就不会在去讲解了。
-
-
3. 递归函数
-
3.1 递归的基本原理和使用
- C 允许一个函数调用其本身。这种调用被称作递归(recursion)。递归有时很难处理,而有时却很方便实用。当一个函数调用自己时,如果编程中没有设定可以终止递归的条件检测,它会无限制地进行递归调用,所以需要进行谨慎处理。
递归一般可以代替循环语句使用。有些情况下使用循环语句比较好,而有些时候使用递归更有效。递归方法虽然使程序结构优美,但其执行效率却没有循环语句高。 -
使用
- 为了具体说明,请看下面的例子。程序 5 中函数 main()调用了函数 up_and_down()。我们把这次调用称为“第 1 级递归”。然后 up_and_down ()调用其本身,这次调用叫做“第 2 级递归”。第 2 级递归调用第 3 级递归,依此类推。本例中共有 4 级递归。为了深入其中看看究竟发生了什么,程序不仅显示出了变量 n 的值,还显示出了存储 n 的内存的地址 &n(本章稍后部分将更全面讨论 & 运算符。printf()函数使用 %p 说明符来指示地址)。
- 程序 5:
#include<stdio.h> void up_and_down(int); int main(void) { up_and_down(1); return 0; } void up_and_down(int n) { printf("Level %d : n location %p\n",n,&n);/*1*/ if(n<4) up_and_down(n+1); printf("LEVEL %d : n location %p\n",n,&n);/*2*/ }
结果:
Level 1 : n location 000000000061FE00
Level 2 : n location 000000000061FDD0
Level 3 : n location 000000000061FDA0
Level 4 : n location 000000000061FD70
LEVEL 4 : n location 000000000061FD70
LEVEL 3 : n location 000000000061FDA0
LEVEL 2 : n location 000000000061FDD0
LEVEL 1 : n location 000000000061FE00
显然,在函数 up_and_down() 内部调用函数 up_and_down() 之前的语句是按顺序输出的,而在其之后的语句是按逆序输出的。而在同一级中,n 的地址没有发生改变,而在不同级中,n 的地址却发生改变。这说明,在不同级中,n 是不一样的。(参考上面所说的两个小盒子) -
基本原理
- 根据程序 5 所使用的递归,我们可以总结出递归的基本原理:
第一,每一级的函数调用都有自己的变量。
第二,每一次函数调用都会有一次返回。
第三,递归函数中,位于递归调用前的语句和各级被调函数具有相同的执行顺序。
第四,递归函数中,位于递归调用后的语句的执行顺序和各个被调函数的顺序相反。
第五,虽然每一级递归都有自己的变量,但是函数代码并不会得到复制。函数代码是一系列的计算机指令,而函数调用就是从头执行这个指令集的一条命令。一个递归调用会使程序从头执行相应函数的指令集。除了为每次调用创建变量,递归调用非常类似于一个循环语句。实际上,递归有时可被用来代替循环,反之亦然。
最后,递归函数中必须包含可以终止递归调用的语句。通常情况下,递归函数会使用一个 if 条件语句或其他类似的语句以便当函数参数达到某个特定值时结東递归调用。 -
3.2 尾递归
- 最简单的递归形式是把递归调用语句放在函数结尾即恰在 return 语句之前。这种形式被称作尾递归(lail recursion)或结尾递归(end recursion),因为递归调用出现在函数尾部。由于尾递归的作用相当于一条循环语句,所以它是最简单的递归形式。
下面我们讲述分别使用循环和尾递归完成阶乘计算的例子。一个整数的阶乘就是从1到该数的乘积。例如,3 的阶乘(写作 3!)是 1X2X3 。0!等于 1,而且负数没有阶乘。程序 6 中,第一个函数使用for 循环计算阶乘,而第二个函数用的是递归方法。 - 程序 6:
#include<stdio.h> long fact(int n); long rfact(int n); int main(void) { int num; printf("This program calculates factorials\n"); printf("Enter a value in the range 0-12 (q to quit):\n"); while(scanf("%d",&num)==1) { if(num<0) printf("No negative numbers,please.\n"); else if(num>12) printf("Keep input under 13.\n"); else { printf("loop: %d factorial = %ld\n",num,fact(num)); printf("recursion: %d factorial = %ld\n",num,rfact(num)); } printf("Enter a value in the range 0-12 (q to quit):\n"); } printf("Bye.\n"); return 0; } long fact(int n) { long ans; for (ans =1;n>1;n--) ans*=n; return ans; } long rfact(int n) { long ans; if(n>0) ans=n*rfact(n-1); else ans=1; return ans; }
结果:
This program calculates factorials
Enter a value in the range 0-12 (q to quit):
4
loop: 4 factorial = 24
recursion: 4 factorial = 24
Enter a value in the range 0-12 (q to quit):
2
loop: 2 factorial = 2
recursion: 2 factorial = 2
Enter a value in the range 0-12 (q to quit):
q
Bye. -
3.3 递归和反向运算
- 下面我们考虑一个使用递归处理反序问题(在这类问题中使用递归比使用循环更简单)。比如,进制之间的转换。学过数学的我们都知道,如果将十进制转换成二进制的话,需要将数不断除以 2 并且最后将余数反向书写就得到了该数的二进制。这个计算过程就是需要先算出结果后输出。而递归恰恰能够满足。
- 程序 7:
#include<stdio.h> void to_binary(unsigned long n); int main(void) { unsigned long number; printf("Enter an integer(q to quit):\n"); while(scanf("%ul",&number)==1) { printf("Binary equivalent:"); to_binary(number); putchar('\n'); printf("Enter an integer(q to quit):\n"); } printf("Done.\n"); return 0; } void to_binary(unsigned long n)/*递归函数*/ { int r; r=n%2; if(n>=2) to_binary(n/2); putchar('0'+r); return; }
结果:
Enter an integer(q to quit):
38
Binary equivalent:100110
Enter an integer(q to quit):
255
Binary equivalent:11111111
Enter an integer(q to quit):
q
Done. -
3.4 递归的优缺点
-
优点
- 其优点在于为某些编程问题提供了最简单的解决方法。尤其对于有些数列问题,递归有着惊人的优势。
-
缺点
- 缺点是一些递归算法会很快耗尽计算机的内存资源。同时,使用递归的程序难于阅读和维护。
- 下面,我会根据斐波那契数列
用递归的形式写出来。 -
long Fibonacci(int n) { if(n>2) return Fibonacci(n-1)+Fibonacci(n-2); else return 1; }
这个 C 递归函数只是重述了递归的数学定义(为使问题简化,函数不处理小于1的数值)。同时本函数使用了双重递归(double recursion)也就是说,函数对本身进行了两次调用。这就会导致一个弱点。
为了具体说明这个弱点,先假设调用函数 Fibonacci(40)。第1级递归会创建变量 n 。接着它两次调用 Fibonacci (),在第 2 级递归中又会创建两个变量n。上述的两次调用中的每一次又进行了两次调用,因而在第 3 级调用中需要 4 个变量n,这时变量总数为7。因为每级调用需要的变量数是上一级变量数的2倍,所以变量的个数是以指数规律增长的!学过数学的都知道,哪怕是国家粮厂都没有这么多的小麦粒。这种情况下,指数增长的变量数会占用大量内存,这就可能导致程序瘫痪。
当然,以上是一个比较极端的例子,但这也表明了必须小心使用递归,尤其当效率处在第一位的时候。 -
所有C函数地位同等
-
一个程序中的每个 C 函数和其他函数之间是平等关系。每一个函数都可以调用其他任何函数或被其他任何函数调用。这就使得 C 函数和 Pascal 以及 Modula-2 中的过程略有不同,因为这些过程可以嵌入在其他过程之中。而且嵌入在不同处的过程之间不能相互调用。
main()函数是否与其他的函数不同?是的,函数 main()是一个有点特殊的函数。因为在程序中当几个函数放在一起时,计算机将从 main()中的第一个语句开始执行,但这也是其局限之处。同时 main()也可以被其本身递归调用或被其他函数调用——尽管很少这么做。
-
-
4.指针简介
-
下一章我会开始正式讲解指针,这里我先向您简单介绍一下指针的基础概念。
究竟什么叫做指针?一般来讲,指针是一个其数值为地址的变量(或更一般地说是一个数据对象)。正如 char 类型的变量用字符作为其数值,而 int 类型变量的数值是整数,指针变量的数值表示的是地址。 -
4.1 地址运算符:&
- C 中最重要的(有时也是最复杂的)概念之一就是指针(pointer),也就是用来存储地址的变量。在上文中函数 scanf()就是使用地址作为参数。更一般地,当需要改变调用函数中的某个数值时,任何被调用的无返回值的 C 函数都需要使用地址参数来完成该任务。接下来我们将讨论使用地址参数的函数,首先介绍一元运算符 & 的使用方法。
一元运算符 & 可以取得变量的存储地址。假设 pooh 是一个变量的名字,那么&pooh 就是该变量的地址。一个变量的地址可以被看作是该变量在内存中的位置。假定使用了以下语句:
pooh = 24;
并且假定 pooh 的存储地址是 0B76(PC 的地址一般以4位十六进制数的形式表示)。那么语句:
printf (*&d &p\n", pooh, &pooh);
将输出如下数值(%p 是输出地址的说明符——通常是用十六进制表示):
24 0B76 -
4.2 间接运算符:*
- 假定 ptr 指向 bah,如下所示:
ptr = &bah;
这时就可以使用回接(indirection)运算符*(也称作取值(dereferencing)运算符)来获取 bah 中存放的数值(不要把这种一元运算符和表示乘法的二元运算符*相混淆)。
val = * ptr;/*得到 ptr 指向的值*/
语句 ptr=&bah;以及语句 val=*ptr;放在一起等同于下面的语句:
val = bah;
由此看出,使用地址运算符和间接运算符可以间接完成上述语句的功能,这也正是“间接运算符” 名称的由来。 -
4.3 指针声明
- 我们已讲述了 int 类型变量以及其他基本数据类型变量的声明方法。那么应该如何声明指针变量呢?您也许会猜想其声明形式如下:
pointer ptr; /* 不能这样声明一个指针*/
为什么不能这样声明?因为这对于声明一个变量为指针是不够的,还需要说明指针所指向变量的类型。原因是不同的变量类型占用的存储空间大小不同,而有些指针操作需要知道变量类型所占用的存储空间。同时,程序也需要了解地址中存储的是何种数据。例如,long 和 float 两种类型的数值可能使用相同大小的存储空间,但是它们的数据存储方式完全不同。指针的声明形式如下:
int * pi; /* pi 是指向一个整数变量的指针*/
char * pc; /* pc 是指向一个字符变量的指针*/
float * pf, * pg; /* pf 和pg 是指向浮点变量的指针*/
类型标识符表明了被指向变量的类型,而星号(*)表示该变量为一指针。声明 int*pi; 的意思是 pi 是一个指针,而且 *pi 是int 类型的。 - 变量:名称、地址以及数值
- 在上文有关指针的讨论中,变量名称、地址以及数值之间的关系是其关键所在。下面我们将对此进行深入讲解。
编写程序时,一个变量一般有两种属性:变量名和数值(当然还有其他属性,如数据类型等,但它们与这个主题无关)。程序被编译和加载后,同一个变量在计算机中的两个属性是地址和数值。变量的地址可以被看作是在计算机中变量的名称。
在许多编程语言中,变量地址只由计算机处理,对于编程人员来讲完全不可见。但是在 C 中,可以使用运算符 & 对变量的地址进行操作。
&barn 就表示变量 barn 的地址。
可以通过使用变量名获得变量的数值。
例如 printf ("%d/n", barn)输出的是barn的数值。
当然,也可以通过使用运算符 * 从地址中获取相应的数值。
对于语句 pbarn=&bar;,*pbarn 是存储在地址 &barn 中的数值。
总之,普通的变量把它的数值作为基本数值量,而通过使用运算符 & 将它的地址作为间接数值量。但是对于指针来讲,地址是它的基本数值量,使用运算符 * 后,该地址中存储的数值是它的间接数值量。
某些读者也许会将地址打印出来以满足好奇心,但这并不是 & 运算符的主要用途。更重要的是,使用 & 、* 和指针可以方便地操作地址以及地址中的内容,如程序 4 中的swap() 函数所示。
-
参考书籍:《C Primer Plus》【美】 Stephen Prata 著
《程序设计教程 用 C/C++ 语言编程》 周纯杰 何顶新 周凯波 彭刚 张惕远 编著