基本的我觉得读者可以直接看菜鸟教程的讲解,这里的话还是细化和深化理解为主吧~
当执行流到达函数定义的末尾时,函数就将返回(return),也就是说,执行流返回到函数被调用的地方。return语句允许从函数体的任何位置返回,并不一定要在函数体的末尾。
批注:那为什么还要有函数类型呢?(这点我不是很明白,到底应该如何理解函数类型)
它的语法如下所示:
return expression;
表达式expression是可选的。如果函数无须向调用程序返回一个值,表达式就被省略。这类函数在绝大多数其他语言中被称为过程(procedure)。这些函数执行到函数体末尾时隐式地返回,没有返回值。这种没有返回值的函数在声明时应该把函数的类型声明为void。
真函数是从表达式内部调用的,它必须返回一个值,用于表达式的求值。这类函数的return语句必须包含一个表达式。通常,表达式的类型就是函数声明的返回类型。只有当编译器可以通过寻常算术转换把表达式的类型转换为正确的类型时,才允许返回类型与函数声明的返回类型不同的表达式。
批注:所以说,函数设置什么类型,就是看返回值是什么类型咯。
当编译器遇到一个函数调用时,它产生代码传递参数并调用这个函数,而且接收该函数返回的值(如果有的话)。但编译器是如何知道该函数期望接受的是什么类型和多少数量的参数呢?如何知道该函数的返回值(如果有的话)类型呢?
如果没有关于调用函数的特定信息,编译器便假定在这个函数调用时参数的类型和数量是正确的。它同时会假定函数将返回一个整型值。对于那些返回值并非整型的函数而言,这种隐式认定常常导致错误。
批注:我觉得这本书的优点就在于从“编译器”的角度来讲解代码吧,yysy,反正我上课的时候这方面是没怎么涉及的。
int read_column_numbers( int columns[], int max ); void rearrange( char *output, char const *input, int n_columns, int const columns[] );
这些声明被称为函数原型(function prototype)。它们告诉编译器这些以后将在源文件中定义的函数的特征。这样,当这些函数被调用时,编译器就能对它们进行准确性检查。每个原型以一个类型名开头,表示函数返回值的类型。跟在返回类型名后面的是函数的名字,再后面是函数期望接受的参数。所以,函数read_column_numbers返回一个整数,接受两个类型分别是整型数组和整型标量的参数。函数原型中参数的名字并非必需的,这里给出参数名的目的是提示它们的作用。
向编译器提供一些关于函数的特定信息显然更为安全,我们可以通过两种方法来实现。
首先,如果同一源文件的前面已经出现了该函数的定义,编译器就会记住它的参数数量和类型,以及函数的返回值类型。其次,编译器便可以检查该函数的所有后续调用(在同一个源文件中),确保它们是正确的。
第二种向编译器提供函数信息的方法是使用函数原型(function prototype),第1章已经见过它。原型总结了函数定义的起始部分的声明,向编译器提供有关该函数应该如何调用的完整信息。使用原型最方便(且最安全)的方法是把原型置于一个单独的文件,如果其他源文件需要这个函数的原型,就使用#include指令包含该文件。这个技巧避免了错误输入函数原型的可能性,又简化了程序的维护任务,因为这样只需要该原型的一份物理副本。如果原型需要修改,只需要修改它的一处副本即可。
举个例子,这里有一个find_int函数的原型,取自前面的例子:
int *find_int(int key, int array[], int len);
注意最后面的那个分号:它区分了函数原型和函数定义的起始部分。原型告诉编译器函数的参数数量和每个参数的类型以及返回值的类型。编译器见过原型之后,就可以检查该函数的调用,确保参数正确且返回值无误。当出现不匹配的情况时(例如,参数的类型错误),编译器会把不匹配的实参或返回值转换为正确的类型,当然前提是这样的转换必须是可行的。
下面的代码段说明了一种使用函数原型的危险方法。
Void a() { Int *func( int *value, int len); ... } void b() { Int func( int len, int *value ); ... }
批注:这个int没有问题?怎么写成Int了,,,
仔细观察一下这两个原型,就会发现它们是不一样的。参数的顺序倒了,返回类型也不同。问题在于这两个函数原型都写于函数体的内部,它们都具有代码块作用域,所以编译器在每个函数结束前会把它记住的原型信息丢弃,这样它就无法发现它们之间存在的不匹配情况。
标准表示,在同一个代码块中,函数原型必须与同一个函数的任何先前原型匹配,否则编译器应该生成一条错误信息。但是在这个例子里,第1个代码块的作用域并不与第2个代码块重叠,因此,原型的不匹配就无法被检测到。这两个原型至少有一个是错误的(也可能两个都错),但编译器看不到这种情况,所以不会生成任何错误信息。
下面的代码段说明了一种使用函数原型的更好方法:
#include "func.h"
文件func.h包含了下面的函数原型:
int *func(int *value, int len);
从几个方面看,这个技巧比前一种方法更好。
1.现在函数原型具有文件作用域,所以原型的一份副本可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型要容易得多。
2.现在函数原型只书写一次,这样就不会出现多份原型的副本之间不匹配的现象。
3.如果函数的定义进行了修改,我们只需要修改原型,并重新编译所有包含了该原型的源文件即可。
4.如果函数的原型同时也被#include指令包含到定义函数的文件中,编译器就可以确认函数原型与函数的定义匹配。
通过只书写函数原型一次,我们消除了多份原型的副本之间不一致的可能性。然而,函数原型必须与函数定义匹配。把函数原型包含在定义函数的文件中可以使编译器确认它们之间的匹配性。
当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。对于那些并不返回整型值的函数,这种认定可能会引起错误。
所有的函数都应该具有原型,尤其是那些返回值不是整型的函数。记住,值的类型并不是值的内在本质,而是取决于它被使用的方式。如果编译器认定函数返回一个整型值,它将产生整数指令操纵这个值。如果这个值实际上是个非整型值,比如说是个浮点值,其结果通常将是不正确的。
让我们看一个这种错误的例子。假设有一个函数xyz,它返回浮点值3.14。在Sun Sparc工作站中,用于表示这个浮点数的二进制位模式如下:
01000000010010001111010111000011
现在假定函数是这样被调用的:
float f; ... f = xyz();
如果在函数调用之前编译器无法看到它的原型,它便认定这个函数返回一个整型值,并产生指令将这个值转换为浮点值,然后再赋值给变量f。
函数返回的位如上所示。转换指令把它们解释为整型值1078523331,并把这个值转换为float类型,结果存储于变量f中。
为什么函数的返回值实际上已经是浮点值的形式时,还要执行类型转换呢?编译器并没有办法知道这个情况,因为没有原型或声明告诉它这些信息。这个例子说明了为什么返回值不是整型的函数具有原型是极为重要的。
C函数的所有参数均以“传值调用”方式进行传递,这意味着函数将获得参数值的一份拷贝。这样,函数可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。这个行为与Modula和Pascal中的值参数(不是var参数)相同。
C的规则很简单:所有参数都是传值调用。但是,如果被传递的参数是一个数组名,并且在函数中使用下标引用该数组的参数,那么在函数中对数组元素进行修改时,实际上修改的是调用程序中的数组元素。函数将访问调用程序的数组元素,数组并不会被复制。这个行为被称为“传址调用”,也就是许多其他语言所实现的var参数。
数组参数的这种行为似乎与传值调用规则相悖。但是,此处其实并无矛盾之处——数组名的值实际上是一个指针,传递给函数的就是这个指针的一份副本。下标引用实际上是间接访问的另一种形式,它可以对指针执行间接访问操作,访问指针指向的内存位置。参数(指针)实际上是一份拷贝,但在这份拷贝上执行间接访问操作所访问的是原先的数组。下一章将再讨论这一点,此处只要记住两个规则:
1.传递给函数的标量参数是传值调用的;
2.传递给函数的数组参数在行为上就像它们是通过传址调用的那样。
批注:然后是我比较欣赏的地方了,用下面的这个例子对上面的知识进行了说明。
/* ** 交换调用程序中的两个整数(没有效果!) */ void swap( int x, int y ) { int temp; temp = x; x = y; y = temp; }
/* ** 交换调用程序中的两个整数。 */ void swap( int *x, int *y ) { int temp; temp = *x; *x = *y; *y = temp; }
因为函数期望接受的参数是指针,所以应该按照下面的方式调用它:
swap (&a, &b);
C可以用于设计和实现抽象数据类型(ADT,Abstract Data Type),因为它可以限制函数和数据定义的作用域。这个技巧也被称为黑盒(black box)设计。抽象数据类型的基本想法是很简单的——模块具有功能说明和接口说明,前者说明模块所执行的任务,后者定义模块的使用。但是,模块的用户并不需要知道模块实现的任何细节,而且除了那些定义好的接口,用户不能以任何方式访问模块。
限制对模块的访问是通过合理使用static关键字来实现的,它可以限制对那些并非接口的函数和数据的访问。例如,考虑一个用于维护一个地址/电话号码列表的模块。模块必须提供函数,根据一个指定的名字查找地址和电话号码。但是,列表存储的方式是依赖于具体实现的,所以这个信息为模块所私有,用户并不知情。
下一个例子程序说明了这个模块的一种可能的实现方法。程序7.5a定义了一个头文件,它定义了一些由客户使用的接口。程序7.5b展示了这个模块的实现。
/* ** 地址列表模块的声明。 */ /* ** 数据特征 ** ** 各种数据的最大长度(包括结尾的NUL字节)和地址的最大数量。 */ #define NAME_LENGTH 30 /*允许出现的最长名字 */ #define ADDR_LENGTH 100 /* 允许出现的最长地址 */ #define PHONE_LENGTH 11 /* 允许出现的最长电话号码 */ #define MAX_ADDRESSES 1000 /* 允许出现的最多地址个数 */ /* ** 接口函数 ** ** 给出一个名字,查找对应的地址。 */ char const * lookup_address( char const *name ); /* ** 给出一个名字,查找对应的电话号码。 */ char const * lookup_phone( char const *name );
程序7.5a 地址列表模块:头文件 addrlist.h
/* ** 用于维护一个地址列表的抽象数据类型。 */ \# include "addrlist.h" \# include <stdio.h> /* ** 每个地址的3个部分,分别保存于3个数组的对应元素中。 */ static char name[MAX_ADDRESSES][NAME_LENGTH]; static char address[MAX_ADDRESSES][ADDR_LENGTH]; static char phone[MAX_ADDRESSES][PHONE_LENGTH]; /* ** 这个函数在数组中查找一个名字并返回查找到的位置的下标。 ** 如果这个名字在数组中并不存在,函数返回-1。 */ static int find_entry( char const *name_to_find ) { int entry; for( entry = 0; entry < MAX_ADDRESSES; entry += 1 ) if( strcmp( name_to_find, name[ entry ] ) == 0 ) return entry; return -1; } /* ** 给定一个名字,查找并返回对应的地址。 ** 如果名字没有找到,函数返回一个NULL指针。 */ char const * lookup_address( char const *name ) { int entry; entry = find_entry( name ); if( entry == -1 ) return NULL; else return address[ entry ]; } /* ** 给定一个名字,查找并返回对应的电话号码。 ** 如果名字没有找到,函数返回一个NULL指针。 */ char const * lookup_phone( char const *name ) { int entry; entry = find_entry( name ); if( entry == -1 ) return NULL; else return phone[ entry ]; }
程序7.5b 地址列表模块:实现 addrlist.c
程序7.5是一个黑盒的好例子。黑盒的功能通过规定的接口访问,在这个例子里,接口是函数lookup_address和lookup_phone。但是,用户不能直接访问和模块实现有关的数据,如数组或辅助函数find_entry,因为这些内容被声明为static。
批注:感觉和今天学的encapsulation很像~。