第七章 函数
函数复习
首先,什么是函数?
函数(unction)是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。虽然C中的函数和其他语言中的函数、子程序、过程作用相同,但是细节上略有不同。一些函数执行某些动作,如printf()把数据打印到屏幕上;一些函数找出一个值供程序使用,如 strlen()把指定字符串的长度返回给程序。一般而言,函数可以同时具备以上两种功能。
为什么要使用函数?
首先,使用函数可以省去编写重复代码的苦差。如果程序要多次完成某项任务,那么只需编写一个合适的函数,就可以在需要时使用这个函数,或者在不同的程序中使用该函数,就像许多程序中使用 putchar()一样。其次,即使程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而提高了程序代码的可读性,更方便后期修改、完善。
#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');
}
实验现象
函数分析:
程序在3处使用了starbar标识符:函数原型(function prototype)告诉编译器函数 starbar()的类型:函数调用表明在此处执行函数;函数定义明确地指定了函数要做什么。
函数和变量一样,有多种类型。任何程序在使用函数之前都要声明该函数的类型。因此,在main()函数定义的前面出现了下面的ANSIC风格的函数原型:
void starbar(void);
圆括号表明starbar是一个函数名。第1个 void 是函数类型,void 类型表明函数没有返回值第2个void(在圆括号中)表明该函数不带参数。分号表明这是在声明函数,不是定义函数。也就是说,这行声明了程序将使用一个名为starbar()、没有返回值、没有参数的函数,并告诉编译器在别处查找该函数的定义。对于不识别ANSIC风格原型的编译器,只需声明函数的类型,如下所示:
void starbar();
注意,一些老版本的编译器甚至连void 都识别不了。如果使用这种编译器,就要把没有返回值的函数声明为int类型。当然,最好还是换一个新的编译器。
一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。这些信息称为该函数的签名。对于starbar()函数而言,其签名是该函数没有返回值,没有参数。
程序把 starbar()原型置于 main()的前面。当然,也可以放在 main()里面的声明变量处。放在哪个位置都可以。
在 main()中,执行到下面的语句时调用了starbar()函数:
starbar();
这是调用 void 类型函数的一种形式。当计算机执行到starbar();语句时,会找到该函数的定义并执行其中的内容。执行完starbar()中的代码后,计算机返回主调函数继续执行下一行(本例中,主调函数是main()),见图(更确切地说,编译器把C程序翻译成执行以上操作的机器语言代码)。
程序中 strarbar()和 main()的定义形式相同。首先函数头包括函数类型、函数名和圆括号接着是左花括号、变量声明、函数表达式语句,最后以右花括号结束。注意,函数头中的 starbar()后面没有分号,告诉编译器这是定义starbar(),而不是调用函数或声明函数原型。
程序把 starbar()和 main()放在一个文件中。当然,也可以把它们分别放在两个文件中。把函数都放在一个文件中的单文件形式比较容易编译,而使用多个文件方便在不同的程序中使用同一个函数。如果把函数放在一个单独的文件中,要把#define 和#include 指令也放入该文件。我们稍后会讨论使用多个文件的情况。现在,先把所有的函数都放在一个文件中。main()的右花括号告诉编译器该函数结束的位置,后面的starbar()函数头告诉编译器starbar()是一个函数。
函数运行流程图:
starbar()函数中的变量 count是局部变量(local variable),意思是该变量只属于starbar()函数。可以在程序中的其他地方(包括main()中)使用count,这不会引起名称冲突,它们是同名的不同变量。
如果把 starbar()看作是一个黑盒,那么它的行为是打印一行星号。不用给该函数提供任何输入,因为调用它不需要其他信息。而且,它没有返回值,所以也不给main()提供(或返回)任何信息。简而言之,starbar()不需要与主调函数通信。
函数参数
实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
在调用函数时,它们都必须有确定的值,以便把这些值传送给形参。
实参可以是常量,变量,表达式,函数等等,但无论是何类型,在进行函数调用是,他们必须有确定的值,以便把这些值拷贝给形参。
形式参数(形参)
形式参数是指函数名后括号中的变量。
形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。因此形式参数只在函数中才有效。
下面是函数在处理数据时的处理思路:
形参变量是功能函数里的变量,只有在被调用的时候才分配内存单元,调用结束后立即释放。所以形参只在函数内部有效。
注意:
在函数运行时,形参和实参是不同的变量,他们在内存中处于不同的位置。形参将实参的内容拷贝一份,在该函数运行结束的时候释放,实参内容不变。
使用 return 从函数中返回值
#include <stdio.h>
int imin(int, int);
int main(void)
{
int evil1, evil2;
printf("Enter a pair of integers (q to quit):\n");
while (scanf("%d %d", &evil1, &evil2) == 2)
{
printf("The lesser of %d and %d is %d.\n",
evil1, evil2, imin(evil1,evil2));
printf("Enter a pair of integers (q to quit):\n");
}
printf("Bye.\n");
return 0;
}
int imin(int n,int m)
{
int min;
if (n < m)
min = n;
else
min = m;
return min;
}
实验结果:
关键字 return 后面的表达式的值就是函数的返回值。在该例中,该函数返回的值就是变量 min 的值。因为 min 是int 类型的变量,所以imin()函数的类型也是 int。
变量 min 属于 imin()函数私有,但是 return 语句把 min 的值传回了主调函数。
下面这条语句的作用是把imin中的min的值赋给lesser:
lesser = imin(n,m);
那是否能像写成下面这样
imin(n,m);
lesser =min;
不能。因为主调函数甚至不知道min的存在。记住,imin()中的变量是imin()的局部变量。函数调用imin(evil1,evi12)只是把两个变量的值拷贝了一份了到局部变量min中,然后通过return 将值返回了回去。
返回值不仅可以赋给变量,也可以被用作表达式的一部分。
例如,可以这样:
int z=10,zstar=20;
answer=2*imin(z,zstar)+25;
printf("%d",answer);
返回值不一定是变量的值,也可以是任意表达式的值。
例如,可以用以下的代码简化程序示例:
int imin(int n,int m)
{
return (n,m)? n : m;
}
条件表达式的值是n和m中的较小者,该值要被返回给主调函数。虽然这里不要求用圆括号把返回值括起来,但是如果想让程序条理更清楚或统一风格,可以把返回值放在圆括号内。
如果函数返回值的类型与函数声明的类型不匹配会怎样?
递归
函数递归的定义
C允许函数调用它自己,这种调用过程称为递归(recursion)。递归有时难以捉摸,有时却很方便实用。结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。
可以使用循环的地方通常都可以使用递归。有时用循环解决问题比较好,但有时用递归更好。递归方案更简洁,但效率却没有循环高。
函数递归的优缺点
优点:
函数递归只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小(这种思考方式十分重要)。
缺点:
①如果函数递归使用不恰当,会导致栈溢出,因为每一次函数调用都会在栈区上申请内存空间。
②每一次函数递归(函数调用)都会在函数栈帧上开辟一块空间,所谓的压栈。这样会大大降低我们代码的执行效率。
演示递归
#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
}
递归的基本原理
指针引入
除了以上这个,还可以用我们的 ^ 运算符
a = a ^ b;
b = b ^ a;
a = a ^ b;
基于以上这些,我们可以看下一下代码:
#include <stdio.h>
void interchange(int u, int v);
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x , y);
interchange(x, y);
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}
void interchange(int u, int v)
{
int temp;
temp = u;
u = v;
v = temp;
}
我们可以看到我们预想的结果并没有实现(通过这个函数来进行两个数的交换),我们将程序进一步的深入
#include <stdio.h>
void interchange(int u, int v);
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x , y);
interchange(x, y);
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}
void interchange(int u, int v)
{
int temp;
printf("Originally u = %d and v = %d.\n", u , v);
temp = u;
u = v;
v = temp;
printf("Now u = %d and v = %d.\n", u, v);
}
看来,interchange()没有问题,它交换了u和v的值。问题出在把结果传回main()时。interchange()使用的变量并不是máin()中的变量。
所以我们可以简单的了解到,通过这样传递参数的方法,在函数中编译的局部变量并不能影响到其他函数中的值,如果要通过函数来进行更改其他函数中的所传递过来的值,我们需要用到指针
指针简介
指针?什么是指针?从根本上看,指针是一个值为内存地址的变量(或数据对象)。
正如char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。在语言中,指针有许多用法。
运算符
查找地址:&运算符
指针是语言最重要的(有时也是最复杂的)概念之一,用于储存变量的地址。
前面使用的 scanf()函数中就使用地址作为参数。概括地说,如果主调函数不使用return 返回的值,则必须通过地址才能修改主调函数中的值。接下来,我们将介绍带地址参数的函数。首先介绍一元&运算符的用法。
取值运算符& 是用来取得其操作对象的地址。如果操作对象x的类型为T,则表达式&x的类型是T类型指针(指向T类型对象 x的指针)。
取值运算符的操作对象必须是在内存中可寻址到的地址。换句话说,该运算符只能用于函数或对象(左值),而不可用于位字段,以及哪些还未被存储类修饰符register声明的内容。
当需要初始化指针,以指向某些对象或函数时,需要获得这些对象或函数的地址:
float x, *ptr;
ptr = &x; // 合法,使得指针ptr指向x
ptr = &(x+1); // 错误,(x+1)不是一个左值
一元&运算符给出变量的存储地址。如果pooh是变量名,那么&pooh 是变量的地址。可以把地址看作是变量在内存中的位置。假设有下面的代码:
将输出如下内容(%p是输出地址的转换说明):
#include <stdio.h>
void mikado(int);
int main(void)
{
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;
}
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);
}
在C语言中,该例的输出说明了什么?首先,两个 pooh 的地址不同,两个 bah 的地址也不同。因此,和前面介绍的一样,计算机把它们看成4个独立的变量。其次,函数调用 mikado(pooh)把实际参数(main()中的pooh)的值(2)传递给形式参数(mikado()中的bah)。
注意,这种传递只传递了值。涉及的两个变量(main()中的pooh和mikado()中的bah)并未改变。
间接运算符 *
与取址运算符& 相反,间接运算符* 用于当已经具有一个指针,并希望获取它所引用的对象时,因此也被称为解引用运算符。
它的操作对象必须是指针类型。如果ptr是指针,那么 *ptr就是ptr所指向的对象或函数。如果ptr是一个对象指针,那么 *ptr就是一个左值,可以把它即( *ptr)当做赋值运算符左边的操作数:
float x, *ptr = &x;
*ptr = 1.7; // 将1.7赋值给变量x
++(*ptr); // 并将变量x的值加1
小结
声明指针
声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。
另外,程序必须知道储存在指定地址上的数据类型。long 和 float 可能占用相同的存储空间,但是它们储存数字却大相径庭。下面是一些指针的声明示例:
int * pi; //pi是指向 int 类型变量的指针
char * pc; //pc是指向 char 类型变量的指针
float * pd,* pf; //pd,pf是指向 float 类型变量的指针
类型说明符表明了指针所指向对象的类型,星号 ( * ) 表明声明的变量是一个指针
*int * pi;声明的意思是 pi是一个指针,pi是int类型。
( * ) 和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。
pc指向的值(*pc)是char 类型。pc本身是什么类型?
我们描述它的类型是“指向 char 类型的指针”。pc 的值是一个地址,在大部分系统内部,该地址由一个无符号整数表示。但是,不要把指针认为是整数类型。一些处理整数的操作不能用来处理指针,反之亦然。
例如,可以把两个整数相乘,但是不能把两个指针相乘。所以,指针实际上是一个新类型,不是整数类型。
使用指针在函数间通信
#include <stdio.h>
void interchange(int * u, int * v);
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x , y);
interchange(&x, &y);
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}
void interchange(int * u, int * v)
{
int temp;
printf("Originally u = %d and v = %d.\n", *u , *v);
temp = *u;
*u = *v;
*v = temp;
printf("Now u = %d and v = %d.\n", *u, *v);
}