肯尼斯·里科《C和指针》第7章 函数(1)

7.1 函数定义

基本的我觉得读者可以直接看菜鸟教程的讲解,这里的话还是细化和深化理解为主吧~

return语句

当执行流到达函数定义的末尾时,函数就将返回(return),也就是说,执行流返回到函数被调用的地方。return语句允许从函数体的任何位置返回,并不一定要在函数体的末尾。

批注:那为什么还要有函数类型呢?(这点我不是很明白,到底应该如何理解函数类型)

它的语法如下所示:

return expression;

表达式expression是可选的。如果函数无须向调用程序返回一个值,表达式就被省略。这类函数在绝大多数其他语言中被称为过程(procedure)。这些函数执行到函数体末尾时隐式地返回,没有返回值。这种没有返回值的函数在声明时应该把函数的类型声明为void。

真函数是从表达式内部调用的,它必须返回一个值,用于表达式的求值。这类函数的return语句必须包含一个表达式。通常,表达式的类型就是函数声明的返回类型只有当编译器可以通过寻常算术转换把表达式的类型转换为正确的类型时,才允许返回类型与函数声明的返回类型不同的表达式

批注:所以说,函数设置什么类型,就是看返回值是什么类型咯。

7.2 函数声明

当编译器遇到一个函数调用时,它产生代码传递参数并调用这个函数,而且接收该函数返回的值(如果有的话)。但编译器是如何知道该函数期望接受的是什么类型和多少数量的参数呢?如何知道该函数的返回值(如果有的话)类型呢?

如果没有关于调用函数的特定信息,编译器便假定在这个函数调用时参数的类型和数量是正确的。它同时会假定函数将返回一个整型值。对于那些返回值并非整型的函数而言,这种隐式认定常常导致错误。

批注:我觉得这本书的优点就在于从“编译器”的角度来讲解代码吧,yysy,反正我上课的时候这方面是没怎么涉及的。

7.2.1 原型
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指令包含到定义函数的文件中,编译器就可以确认函数原型与函数的定义匹配。

通过只书写函数原型一次,我们消除了多份原型的副本之间不一致的可能性。然而,函数原型必须与函数定义匹配。把函数原型包含在定义函数的文件中可以使编译器确认它们之间的匹配性。

7.2.2 函数的缺省认定

当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。对于那些并不返回整型值的函数,这种认定可能会引起错误。

所有的函数都应该具有原型,尤其是那些返回值不是整型的函数。记住,值的类型并不是值的内在本质,而是取决于它被使用的方式。如果编译器认定函数返回一个整型值,它将产生整数指令操纵这个值。如果这个值实际上是个非整型值,比如说是个浮点值,其结果通常将是不正确的。

让我们看一个这种错误的例子。假设有一个函数xyz,它返回浮点值3.14。在Sun Sparc工作站中,用于表示这个浮点数的二进制位模式如下:

01000000010010001111010111000011

现在假定函数是这样被调用的:

float  f;
...
f = xyz();

如果在函数调用之前编译器无法看到它的原型,它便认定这个函数返回一个整型值,并产生指令将这个值转换为浮点值,然后再赋值给变量f。

函数返回的位如上所示。转换指令把它们解释为整型值1078523331,并把这个值转换为float类型,结果存储于变量f中。

为什么函数的返回值实际上已经是浮点值的形式时,还要执行类型转换呢?编译器并没有办法知道这个情况,因为没有原型或声明告诉它这些信息。这个例子说明了为什么返回值不是整型的函数具有原型是极为重要的。

7.3 函数的参数

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);

7.4 ADT和黑盒

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很像~。

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: C 是一种编程语言,由肯尼斯·莫瑞·比尔(Dennis Ritchie)在1972年开发。C 语言是一种通用的、高级的编程语言,常用于系统软件和应用软件的开发。C 语言为程序员提供了一种快速编写程序的方式,并且运行效率高。它也是许多其他编程语言的基础,如 C++、Java 和 Python 等。 ### 回答2: C是一种高级的程序设计语言,由美国计算机科学家丹尼斯·里奇在1972年至1973年间开发出来。它是一种通用且强大的编程语言,被广泛应用于系统开发、嵌入式系统、操作系统、游戏开发等领域。C语言具有简洁、高效、易于理解的特点,被称为“高级汇编语言”。 C语言为程序员提供了丰富的编程工具和灵活的编程方法,包括结构体、指针函数指针、动态内存管理等。它支持底层操作,并且提供了对硬件的直接访问能力,因此可以写出高效的低级别代码。与其他高级语言相比,C语言的执行效率更高,可移植性更好。 C语言具有易学易用的特点,其语法简洁并且规范,对初学者来说较为友好。很多计算机专业大学生都会在课程中学习C语言作为入门编程语言。C语言学习也有助于培养程序员的逻辑思维能力和问题解决能力。 C语言的标准是由美国国家标准协会(ANSI)和国际标准化组织(ISO)制定的,并且得到了广泛的应用和认可。在C语言的基础上,还有C++、Objective-C等衍生语言,它们在C语言的基础上增加了一些新的特性和功能,满足了不同开发需求。 总之,C语言是一种重要的程序设计语言,具有广泛的应用和重要的地位。对于想要从事软件开发和计算机编程的人来说,学习和掌握C语言是非常重要的基础。 ### 回答3: C是计算机科学和编程中的一种编程语言。它是由美国贝尔实验室的丹尼斯·里奇在20世纪70年代初开发的,并在1972年首次在PDP-11计算机上使用。C语言被广泛用于系统软件开发和应用程序开发,是一种高效且灵活的编程语言。 C语言有许多特点使其成为流行的选择。首先,它具有高度的可移植性,这意味着在不同的平台上编写的C代码可以很容易地移植到其他平台上运行。其次,C语言具有简洁的语法和结构,使得编写和阅读代码变得相对容易。此外,C语言提供了底层访问计算机资源的能力,例如内存管理和指针,使开发人员能够更好地控制程序的执行。 C语言广泛应用于操作系统的开发,包括UNIX和Windows操作系统。许多著名的系统软件,如Linux内核和数据库管理系统MySQL,也是用C语言编写的。此外,C语言在应用程序开发中也非常受欢迎。许多流行的编程语言,如C++和Objective-C,都是以C语言为基础发展而来的。 总的来说,C语言是一种广泛使用的高级编程语言,具有高度的可移植性、简洁的语法和强大的底层访问能力。它在系统软件和应用程序开发中发挥着重要的作用,并对计算机科学和编程产生了深远的影响。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值