《C Primer Plus》学习笔记—第9章

《C Primer Plus》学习笔记

第9章 函数

1.复习函数

1.引入

函数(function) 是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。虽然C中的函数和其他语言中的函数、子程序、过程作用相同,但是细节上略有不同。一些函数执行某些动作,如printf()把数据打印到屏幕上;一些函数找出一个供程序使用,如strlen()把指定字符串的长度返回给程序。一般而言,函数可以同时具备以上两种功能
使用函数的好处:可以去编写重复代码的苦差。如果程序要多次完成某项任务,那么只需编写一个合适的函数,就可以在需要时使用这个函数,或者在不同的程序中使用该函数,就像许多程序中使用putchar()一样;即使程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而提高了程序代码的可读性,更方便后期修改、完善。例如,假设要编写一个程序完成以下任务:
1.读入一系列数字;
2.分类这些数字;
3.找出这些数字的平均值;
4.打印一份柱状图。
可以使用下面的程序:

include<stdio.h>
define SIZE 50
int main(void)
{
   
    float 1ist[SIZE];
    readlist(list,SIZE);
    sort(list,SIZE);
    average(list,SIZE);
    bargraph(list,SIZE);
    return 0; 
}

还要编写4个函数readlist()、sort()、average()和bargraph()的实现细节。描述性的函数名能清楚地表达函数的用途和组织结构。然后,单独设计和测试每个函数,直到函数都能正常完成任务。如果这些函数够通用,还可以用于其他程序。
许多程序员喜欢把函数看作是根据传入信息(输入)及其生成的值或响应的动作(输出)来定义的“黑盒"。如果不是自己编写函数,根本不用关心黑盒的内部行为。例如,使用printf()时,只需知道给该函数传入格式字符串或一些参数以及printf()生成的输出,无需了解printf()的内部代码。以这种方式看待函数有助于把注意力集中在程序的整体设计,而不是函数的实现细节上。因此,在动手编写代码之前,仔细考虑一下函数应该完成什么任务,以及函数和程序整体的关系
如何了解函数?首先要知道如何正确地定义函数、如何调用函数和如何建立函数间的通信。从一个简单的程序示例开始。

2.创建并使用简单的函数:程序lethead1.c

第1个目标是创建一个在一行打印40个星号的函数,并在一个打印表头的程序中使用该函数。如程序lethead1.c所示,该程序由main()和starbar()组成。

#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
****************************************
3.分析程序

该程序要注意以下几点。
1.程序在3处使用了starbar标识符:函数原型(fiunction prototype)告诉编译器函数starbar()的类型;函数调用(function call) 表明在此处执行函数;函数定义(function definition)明确地指定了函数要做什么。
2.函数和变量一样,有多种类型。任何程序在使用函数之前都要声明该函数的类型。因此,在main()函数定义的前面出现了下面的ANSIC风格的函数原型:.
void starba(void) ;
圆括号表明starbar是一个函数名。第1个void是函数类型,void 类型表明函数没有返回值。第2个void(在圆括号中)表明该函数不带参数。分号表明这是在声明函数,不是定义函数。也就是说,这行声明了程序将使用一个名为starbar()、没有返回值、没有参数的函数,并告诉编译器在别处查找该函数的定义。对于不识别ANSI C风格原型的编译器,只需声明函数的类型,如下所示:void starbar();
3.一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。这些信息称为该函数的签名(signature)。对于starbar()函数而言,其签名是该函数没有返回值,没有参数。
4.程序把starbar()原型置于main()的前面。当然,也可以放在main()里面的声明变量处。放在哪个位置都可以。
5.在main()中,执行到下面的语句时调用了starbar()函数:
starbar();
这是调用void类型函数的一种形式。当计算机执行到starbar();语句时,会找到该函数的定义并执行其中的内容。执行完starbar()中的代码后,计算机返回主调函数(calling function)继续执行下一行(本例中,主调函数是main())。
6.程序中strarbar()和main()的定义形式相同。首先函数头包括函数类型、函数名和圆括号,接着是左花括号、变量声明、函数表达式语句,最后以右花括号结束。注意,函数头中的starbar()后面没有分号,告诉编译器这是定义starbar(),而不是调用函数或声明函数原型。
7.程序把starbar()和main()放在一个文件中。当然,也可以把它们分别放在两个文件中。把函数都放在一个文件中的单文件形式比较容易编译,而使用多个文件方便在不同的程序中使用同一个函数。如果把函数放在一个单独的文件中,要把#define和#include指令也放入该文件。现在,先把所有的函数都放在一个文件中。main()的右花括号告诉编译器该函数结束的位置,后面的starbar()函数头告诉编译器starbar()是一个函数。
8.starbar()函数中的变量count是局部变量(local variable),意思是该变量只属于starbar()函数。可以在程序中的其他地方(包括main()中)使用count,这不会引起名称冲突,它们是同名的不同变量

如果把starbar()看作是一个黑盒,那么它的行为是打印一行星号。不用给该函数提供任何输入,因为调用它不需要其他信息。而且,它没有返回值,所以也不给main()提供(或返回)任何信息。简而言之,starbar()不需要与主调函数通信
接下来介绍一个函数间需要通信的例子。

4.函数参数

在程序lethead1.c的输出中,如果文字能居中,信头会更加美观。可以通过在打印文字之前打印一定数量的空格来实现,这和打印一定数量的星号(starbar()函数)类似,只不过现在要打印的是一定数量的空格。虽然这是两个任务,但是任务非常相似,与其分别为它们编写一个函数,不如写一个更通用的函数,可以在两种情况下使用。设计一个新的函数show_n_char()(显示一个字符n次)。唯一要改变的是使用内置的值来显示字符和重复的次数,show_n_char()将使用函数参数来传递这些值。
具体分析。假设可用的空间是40个字符宽。调用show_n_char(’ ‘,40)应该正好打印一行40个星号,就像starbar()之前做的那样。第2行GIGATHINK, INT.是15个字符宽,所以第1个版本中,文字后面有25个空格。为了让文字居中,文字的左侧应该有12个空格,右侧有13个空格。因此,可以调用show_n_char(’ ', 12)。
show_n_char()与starbar()很相似,但是show_n_char()带有参数。从功能上看,前者不会添加换行符,而后者会,因为show_n_char()要把空格和文本打印成一行。程序lethead2.c是修改后的版本。为强调参数的工作原理,程序使用了不同的参数形式。

1.程序lethead2.c
#include <stdio.h>
#include <string.h>
#define NAME "GIGATHINK, INC."
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis, CA 94904"
#define WIDTH 40
#define SPACE ' '
void show_n_char(char ch, int num);//函数原型 
int main(void)
{
   
    int spaces;
    
    show_n_char('*', WIDTH);   //用符号常量作为参数 
    putchar('\n');
    show_n_char(SPACE, 12);    
    printf("%s\n", NAME);
    spaces = (WIDTH - strlen(ADDRESS)) / 2;
    
    show_n_char(SPACE, spaces);//用变量作参数 
    printf("%s\n", ADDRESS);
    show_n_char(SPACE, (WIDTH - strlen(PLACE)) / 2);//用表达式作参数 

    printf("%s\n", PLACE);
    show_n_char('*', WIDTH);
    putchar('\n');
    
    return 0;
}
void show_n_char(char ch, int num)//函数定义 
{
   
    int count;
    
    for (count = 1; count <= num; count++)
        putchar(ch);
}

输出如下:

****************************************
            GIGATHINK, INC.
           101 Megabuck Plaza
          Megapolis, CA 94904
****************************************
5.定义带形式参数的函数

函数定义从下面的ANSI C风格的函数头开始:
void show_n_char(char ch, int num)
该行告知编译器show_n_char()使用两个参数ch和num,ch是char类型,num是int类型。这两个变量被称为形式参数(formal argument,但是最近的标准推荐使用formal parameter),简称形参。和定义在函数中变量一样,形式参数也是局部变量,属该函数私有。这意味着在其他函数中使用同名变量不会引起名称冲突。每次调用函数,就会给这些变量赋值。

注意,ANSI C要求在每个变量前都声明其类型。也就是说,不能像普通变量声明那样使用同一类型的变量列表:
void dibs(int x, y, z)//无效的函数头
void dubs(int x, int y, int z) //有效的函数头
ANSI C也接受ANSI C之前的形式,但是将其视为废弃不用的形式:
void show_n_char(ch, num)
char ch;
int num;
这里,圆括号中只有参数名列表,而参数的类型在后面声明。注意,普通的局部变量在左花括号之后声明,而上面的变量在函数左花括号之前声明。如果变量是同一类型,这种形式可以用逗号分隔变量名列表,如下所示:
void dibs(x, y, z)
int x, y, z;//有效
当前的标准正逐渐淘汰ANSI之前的形式。自己编写程序时应使用现在的标准形式(C99和C11标准继续警告这些过时的用法即将被淘汰)。虽然show_n_char()接受来自main()的值,但是它没有返回值。因此,show_n_char()的类型是void。

6.声明带形式参数函数的原型

在使用函数之前,要用ANSI C形式声明函数原型:
void show_n_char(char ch,int num);
当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。根据个人喜好,也可以省略变量名:
void show_n_char(char, int);
在原型中使用变量名并没有实际创建变量,char仅代表了一个char类型的变量,以此类推。
ANSI C也接受过去的声明函数形式,即圆括号内没有参数列表:
void show_n_char();
这种形式最终会从标准中剔除现在函数原型的设计更有优势(稍后会介绍),了解这种形式的写法是为了以后读得懂以前写的代码。

7.调用带实际参数的函数

在函数调用中,实际参数(actual argument,简称实参)提供了ch和num的值。考虑程序lethead2.c中第1次调用show_n_char():
show_n_char(SPACE,12);
实际参数是空格字符和12。这两个值被赋给show_n_char()中相应的形式参数:变量ch和num。
简而言之,形式参数是被调函数(called function)中的变量,实际参数是主调函数(calling function)赋给被调函数的具体值。如上例所示,实际参数可以是常量、变量,或甚至是更复杂的表达式。无论实际参数是何种形式都要被求值,然后该值被拷贝给被调函数相应的形式参数。以程序清单9.2中最后一次调用show_n_char()为例:
show_n_char(SPACE,(WIDTH - strlen (PLACE))/2);
构成该函数第2个实际参数的是一个很长的表达式,对该表达式求值为10。然后,10被赋给变量num。
被调函数不知道也不关心传入的数值是来自常量、变量还是一般表达式。实际参数是具体的值,该值要被赋给作为形式参数的变量。因为被调函数使用的值是从主调函数中拷贝而来,所以无论被调函数对拷贝数据进行什么操作,都不会影响主调函数中的原始数据。

注意:实际参数和形式参数
实际参数是出现在函数调用圆括号中的表达式。形式参数是函数定义的函数头中声明的变量。调用函数时,创建了声明为形式参数的变量并初始化为实际参数的求值结果。程序lethead2.c中,'*'和WIDTH都是第1次调用show_n_char()时的实际参数,而SPACE和12是第2次调用show_n_char()时的实际参数。在函数定义中,ch和num都是该函数的形式参数。

8.黑盒视角

从黑盒的视角看show_n_char(),待显示的字符和显示的次数是输入。执行后的结果是打印指定数量的字符。输入以参数的形式被传递给函数。这些信息清楚地表明了如何在main)中使用该函数。而且,这也可以作为编写该函数的设计说明。
黑盒方法的核心部分是: ch、num和count都是show_n_char()私有的局部变量。如果在main()中使用同名变量,那么它们相互独立,互不影响。也就是说,如果main()有一个count变量,那么改变它的值不会改变show_n_char()中的count,反之亦然。黑盒里发生了什么对主调函数是不可见的。

9.使用return从函数中返回值

前面介绍了如何把信息从主调函数传递给被调函数。反过来,函数的返回值可以把信息从被调函数传回主调函数。创建一个返回两个参数中较小值的函数。由于函数被设计用来处理int类型的值,所以被命名为imin()。另外,还要创建一个简单的main(),用于检查imin()是否正常工作。这种被设计用于测试函数的程序有时被称为驱动程序(driver),该驱动程序调用一个函数。如果函数成功通过了测试,就可以安装在一个更重要的程序中使用。程序lesser.c演示了这个驱动程序和返回最小值的函数。

1.程序lesser.c
#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的值传回了主调函数。下面这条语句的作用是把min的值赋给lesser:
lesser=imin(n,m) ;
是否能像写成下面这样:
imin(n,m);
lesser=min;
不能。因为主调函数不知道min的存在。记住,imin()中的变量是imin()的局部变量。函数调用imin(evi11, evi12)只是把两个变量的值拷贝了一份。
返回值不仅可以赋给变量,也可以被用作表达式的一部分。例如,可以这样:
answer=2*imin(z, zstar)+25;
printf(“&d\n”,imin(-32+answer,LIMIT );
返回值不一定是变量的值,也可以是任意表达式的值。例如,可以用以下的代码简化程序示例:

imin(int n,int m)//返回最小值的函数,第2个版本
{
	return (n<m)?n:m;
}

条件表达式的值是n和m中的较小者,该值要被返回给主调函数。这里不要求用圆括号把返回值括起来,但是如果想让程序条理更清楚或统一风格,可以把返回值放在圆括号内。

如果函数返回值的类型与函数声明的类型不匹配会怎样?
int what_if(int n)
{
double z=100.0/(double)n;
return z;//会发生什么?
}
实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量所得到的值。因此在本例中,相当于把z的值赋给int类型的变量,然后返回int类型变量的值。例如,假设有下面的函数调用:
return=what_if(64);
虽然在what_if()函数中赋给z的值是1.5625,但是return语句返回确实int类型的值1.
使用return语句的另一个作用是,终止函数并把控制返回给主调函数的下一条语句。因此,可以这样编写imin():

//返回最小值的函數,第3个版本
imin(int n,int m)
{
    if(n<m)
    	return n;
    else
    	return m;
}

许多C程序员都认为只在函数末尾使用一次return语句比较好,因为这样做更方便浏览程序的人理解函数的控制流。但是,在函数中使用多个return语句也没有错。无论如何,对用户而言, 这3个版本的函数用起来都一样,因为所有的输入和输出都完全相同,不同的是函数内部的实现细节。下面的版本也没问题:

//返回最小值的函數,第4个版本
imin(int n, int m)
{
    if(n<m)
    	return n;
    else
    	return m;
    printf("professor Fleppard ig like totally a fopdoodle. \n");
}

return语句导致printf()语句永远不会被执行。
另外,还可以这样使用return:
return;
这条语句会导致终止函数,并把控制返回给主调函数。只有在void函数中才会用到这种形式。

10.函数类型

声明函数时必须声明函数的类型。带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为void类型。如果没有声明函数的类型,旧版本的C编译器会假定函数的类型是inC99标准不再支持int类型函数的这种假定设置。
类型声明是函数定义的一部分。要记住,函数类型指的是返回值的类型,不是函数参数的类型。例如,下面的函数头定义了一个带两个int类型参数的函数,但是其返回值是double类型。
double klink(int a,int b)
要正确地使用函数,程序在第1次使用函数之前必须知道函数的类型。方法之一是,把完整的函数定义放在第1次调用函数的前面。然而,这种方法增加了程序的阅读难度。而且,要使用的函数可能在C库或其他文件中。因此,通常的做法是提前声明函数,把函数的信息告知编译器。例如,程序lesser.c中的
main()函数包含以下几行代码:
#include<stdio.h>
int imin(int,int);
int main(void)
{
int evil1,evil2,lesser;
第2行代码说明imin是一个函数名,有两个int类型的形参,且返回int类型的值。编译器在程序中调用imin()函数时就知道应该如何处理。
在程序lesser.c中,我们把函数的前置声明放在主调函数外面。当然,也可以放在主调函数里面。例如,重写lesser.c的开头部分:
#include<stdio.h>
int main(void)
{
int imin(int, int); //声明imin()函数的原型
int evill,evi12,lesser;
注意在这两种情况中,函数原型都声明在使用函数之前
ANSI C标准库中,函数被分成多个系列,每一系列都有各自的头文件。这些头文件中除了其他内容,还包含了本系列所有函数的声明。例如,stdio.h头文件包含了标准I0库函数(如,printf()和scanf())的声明。math.h头文件包含了各种数学函数的声明。例如,下面的声明:
double sqrt (double);
告知编译器sqrt()函数有一个double类型的形参,而且返回double类型的值。函数声明告知编译器函数的类型,而函数定义则提供实际的代码。在程序中包含math.h头文件告知编译器: sqrt()返回double类型,但是sqrt()函数的代码在另一个库函数的文件中。

2.ANIS C函数原型

在ANSIC标准之前,声明函数的方案有缺陷,因为只需要声明函数的类型,不用声明任何参数。下面看一下使用旧式的函数声明会导致什么问题。
int imin() ;
然而,以上函数声明并未给出imin()函数的参数个数和类型。因此,如果调用imin()时使用的参数个数不对或类型不匹配,编译器根本不会察觉出来。

1.问题所在:程序misuse.c

我们看看与imax()函数相关的一些示例,该函数与imin()函数关系密切。程序misuse.c演示了一个程序,用过去声明函数的方式声明了imax()函数,然后错误地使用该函数。

#include <stdio.h>
int imax();      //旧式函数声明 
int main(void)
{
   
    printf("The maximum of %d and %d is %d.\n",
           3, 5, imax(3));
    printf("The maximum of %d and %d is %d.\n",
           3, 5, imax(3.0, 5.0));
    return 0;
}
int imax(n, m)
int n, m;
{
   
    return (n > m ? n : m);
}

第1次调用printf()时省略了imax()的一个参数,第2次调用printf()时用两个浮点参数而不是整数参数。尽管有些问题,但程序可以编译和运行。输出如下:

The maximum of 3 and 5 is 37813996.
The maximum of 3 and 5 is 1074266112.

编译器运行正常,之所以输出错误的结果,是因为运行的程序没有使用函数原型。
由于不同系统的内部机制不同,所以出现问题的具体情况也不同。下面介绍的是使用PC和VAX的情况。主调函数把它的参数储存在被称为(stack)的临时存储区,被调函数从栈中读取这些参数。对于该例,这两个过程并未相互协调。主调函数根据函数调用中的实际参数决定传递的类型,而被调函数根据它的形式参数读取值。因此,函数调用imax(3)把一个整数放在栈中。当imax()函数开始执行时,它从栈中读取两个整数。而实际上栈中只存放了一个待读取的整数,所以读取的第2个值是当时恰好在栈中的其他值。
第2次使用imax()函数时,它传递的是float类型的值。这次把两个double类型的值放在栈中(回忆一下,当float类型被作为参数传递时会被升级为double类型)。在本系统中,两个double类型的值就是两个64位的值,所以128位的数据被放在栈中。当imax()从栈中读取两个int类型的值时,它从栈中读取前64位。每个int类型的变量占用32位。这导致读取出现错误。

2.ANSI的解决方案

针对参数不匹配的问题,ANSI C标准要求在函数声明时还要声明变量的类型,即使用函数原型(function prototype)来声明函数的返回类型、参数的数量和每个参数的类型。未标明imax()函数有两个int类型的参数,可以使用下面两种函数原型来声明:
int imax(int,int);
int imax(int a,int b);
第1种形式使用以逗号分隔的类型列表,第2种形式在类型后面添加了变量名。注意,这里的变量名是假名,不必与函数定义的形式参数名一致。
有了这些信息,编译器可以检查函数调用是否与函数原型匹配。参数的数量是否正确?参数的类型是否匹配?以imax()为例,如果两个参数都是数字,但是类型不匹配,编译器会把实际参数的类型转换成形式参数的类型。例如,imax(3.0,5.0) 会被转换成imax(3,5)。我们用函数原型替换程序misuse.c中的函数声明,如程序proto.c所示。

1.程序proto.c
#include <stdio.h>
int imax(int, int);        
int main(void)
{
   
    printf("The maximum of %d and %d is %d.\n",
           3, 5, imax(3));//这里会报错,因为只传递了一个参数 
    printf("The maximum of %d and %d is %d.\n",
           3, 5, imax(3.0, 5.0));
    return 0;
}
int imax(int 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值