函 数

来源:互联网 发布:卡西欧9860编程教程 编辑:程序博客网 时间:2024/04/27 22:06

第6章 函 数

函数是C中的基本模块单元。一个函数设计用于执行特定的功能,它的名称通常反映其功能。一个函数包含说明和语句。本章描述如何说明、定义和调用函数的。其它讨论的主题有:

* 函数概述

* 函数定义

* 函数原型

* 函数调用


函数概述

函数必须有一个定义和一个说明,如果说明出现在函数被调用之前,一个定义可以作为一个说明,函数定义包括函数体即在该函数被调用时执行的代码。

一个函数说明建立名称、返回值和在程序其它地方定义的函数的属性。一个函数说明必须位于该函数调用的前面。这是为什么在你的代码中调用一个运行函数之前包括包含该运行函数说明的头文件的原因。如果该说明具有类型和参数个数的信息,该说明是原型。

有关更多信息参见本章后面的“函数原型”。

编译器使用该原型将后面调用中的参量类型与具有该函数参数的函数进行比较,必要时把参量类型转换成参数的类型。

一个函数调用将执行控制从调用函数传递到被调用函数。如果有参量,参量通过被调用函数的值传递。在被调用函数中执行一个return语句返回控制和一个可能的值到调用函数。

已废除的函数说明和定义格式

旧式函数说明和定义使用在说明参数上有点不同于ANSI C标准推荐的语法。首先,旧式说明没有参数表;第二,在函数定义中列出参数,但在参数表中不说明它们的定义。类型说明位于组成该函数体的复合语句的前面。旧式语法已废除了,不在新代码中使用。但仍支持使用旧式语法的代码。如下例子给出了说明和定义的已废除的格式:

double old_style(); /*已废除的函数说明*/

double alt_style(a,real) /*已废除的函数说明*/

double *real;

int a;{    return (*real+a);}

返回一个整数或与一个int具有相同尺寸的指针的函数不需要有一个说明,虽然建议有该说明。

为了遵守ANSI C标准,旧式函数说明使用一种省略方式,现在使用/Za选项时会产生一个错误;当使用/Ze选项时产生一个第4层的警告。

例如:

void functl(a,...) /*在/Ze下产生一个警告*/

int a; /*或在/Za下产生一个错误*/

{

}

你可以作为原型重写这个说明:

void funct1(int a,...)

{

}

如果你在后面用一个省略或一个不同于其提升类型的类型参数来说明或定义相同函数,则旧式函数说明也产生警告。

下一节“函数定义”说明了函数定义的语法,包括旧式语法。在旧式语法中参数表的非终结符是标识符表。


函数定义

一个函数指出函数的名称、它接受的参数类型和个数以及它的返回值。一个函数定义还包括说明它的局部变量以及确定该函数所做事情的函数体。

语法

转换单元:

外部说明

转换单元 外部说明

外部说明: /* 仅允许在外部(文件)范围内 */

函数定义

说明

函数定义: /* 这里的说明符是函数说明符 */

说明指示符opt 属性序列opt 说明符 说明表opt 复合语句

/* 属性序列是Microsoft特殊处 */

原型参数是:

说明指示符:

存储类指示符 说明指示符opt 类型指示符opt 类型指示符 说明指示符opt

类型修饰符 说明指示符opt

说明表:

说明

说明表 说明

说明符:

指针opt 直接说明符

直接说明符: /* 一个函数说明符 */

直接说明符(参数类型表) /* 新式说明符 */

直接说明符(标识符表opt) /* 已废除的说明符 */

在一个定义中参数表使用以下语法:

参数类型: /* 参数表 */

参数表

参数表,

...参数表:

参数说明

参数表,参数说明

参数说明:

参数指示符 说明符

说明指示符 抽象说明符opt

在一个旧式函数定义中的参数表使用以下语法:

标识符表: /* 已废除的函数定义和说明 */

标识符

标识符表,标识符

函数体的语法:

复合语句: /* 函数体 */

{说明表opt 语句表opt}

可以用于修饰一个函数说明的存储类指示符只有extern和static。extern指示符指示该函数可以从其它文件引用,也就是,该函数名称被输出到链接器。static指示符指示该函数不能从其它文件引用,也就是,该函数名称不输出到链接器。如果一个函数定义中不出现存储类,则假设是extern。在任何情况下,该函数从定义点到文件结尾就是可见的。

任选说明指示符和强制性说明符一起指出该函数的返回类型和名称。说明符是命名该函数的标识符和紧跟函数名的圆括号的组合。传送的属性序列非终结符是定义在“函数属性”中的Microsoft特殊特征。

直接说明符(在说明符语法中)指出定义的函数的名称和它的参数的标识符。如果直接说明符包括一个参数表,该表指出所有参数的类型。这样的一个说明符也作为后面调用该函数的一个函数原型。

在函数定义中的说明表中的一个说明不能包含register之外的一个存储类指示符。只有在为一个int类型的值指定register存储类时,其说明指示符语法中的类型指示符可以省略。

复合语句是包含局部变量说明、引用外部的说明项和语句的函数体。“函数属性”、“存储类”、“返回值”、“参数”和“函数体”等节详细地描述了函数定义的组成份。

函数属性

Microsoft特殊处

任选的属性序列非终结符允许你在每一个函数基础上选择一个调用约定。你也可以指定函数为__fastcall或_ _inline。

Microsoft特殊处结束

指定调用约定

Microsoft特殊处

有关调用约定的信息,参见联机“Microsoft Visual C++ 6.0程序员指南”中的“调用约定主题”。

Microsoft特殊处结束

联编函数

Microsoft特殊处

__inline关键字告诉编译器在一个函数调用的每个实例中用该函数的定义替换该代码。但替换仅出现在编译器的决定中。例如,如果一个函数的地址被占用或它太大,则编译器不联编该函数。

对一个认为是联编的候选者的函数,它必须使用新式函数定义。

使用这个格式指出一个联编函数:

__inline 类型opt 函数定义;

使用联编函数产生更快的代码,有时产生比相同函数调用产生更小的代码,其原因如下:

* 它节省需要执行函数调用的时间。

* 小的联编函数,可能三行或更少,因为编译器不产生处理参量和返回值的代码而生成比相同的函数调用更少的代码。

* 生成的联编函数是属于不能正常使用的优化代码,因为编译器不执行内部过程的优化。

使用__inline的函数不要与联编汇编器混淆,有关更多信息参见“联编汇编器”。

Microsoft特殊处结束

联编汇编器

Microsoft特殊处

联编汇编器让你在你的C源程序中直接嵌入汇编语言指令而不要额外的汇编和链接步骤。

联编汇编器构建在编译器中,你不需要单独的汇编器例如Microsoft宏汇编器(MASM)。

因为该联编汇编器不需要单独的汇编和链接步骤,它比一个单独的汇编器更方便。联编汇编代码可以使用任何C变量或函数名称,因此它容易与你的程序的C代码集成在一起。又因为汇编代码可以与C语句混合,因此它可以完成单独用C很麻烦的或不可能的功能。

__asm关键字调用联编汇编器,它可以出现在任何合法的C语句中,但不能出现自身的语句中。之后必须跟一个汇编指令、一个用花括号括起的指令组或者至少一个空花括号对。

术语“__asm块”在这里指的是任何指令或指令组,不论是否出现在花括号中。

下面的代码是一个花括号括起的简单__asm块(这个代码是一个空制函数):

__asm

{  push ebp   mov ebp,esp  sub esp, __LOCAL_SIZE}

另一种方式,你可以在每个汇编指令前面放置一个__asm:

__asm push ebp

__asm mov ebp,esp

__asm sub esp, __LOCAL_SIZE

由于__asm关键字是一个语句分隔符,你也可以把汇编指令放在同一行中:__asm push ebp __asm mov ebp,esp __asm sub esp, __LOCAL_SIZE

Microsoft特殊处结束

DLL输入和输出函数

Microsoft特殊处

dllimport和dllexport存储类修饰符是C语言的Microsoft特殊处扩充。这些修饰显式定义了DLL的客户界面(可执行的文件或另外的DLL)。说明为dllexport的函数消除了一个模块定义(.DLL)文件的需要。你可以为数据和对象使用dllimport和dllexport修饰符。

dllimport和dllexport存储类修饰符必须与扩充的属性语法关键字__declspec一起使用,下面是这样的例子:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllExport void func();

Dllexport int i = 10;

DllExport int j;

DllExport int n;

有关扩充的存储类修饰符的语法的指定信息,参见第3章“说明和类型”中的“扩充的存储类型属性”。

Microsoft特殊处结束

定义和说明

Microsoft特殊处

DLL界面指的是系统中某个程序中输出的所有已知项(函数和数据);也就是所有说明为dllimport或dllexport的所有项。包括在DLL界面中的所有说明必须指定为dllimport或dllexport属性。但该定义只能指定dllexport属性。例如,如下函数定义生成一个编译器错误:

#define DLLImport __declspec(dllimport)

#define DLLExport __declspec(dllexport)

DLLImport int func()/*错误:在定义中禁止dllimport*/

{    return 1;}

下面代码也产生一个错误:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllImport int i=10; /*错误:这是一个定义*/

但如下是正确的语法:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllExport int i=10; /*正确:这是一个输出定义*/

dllexport的使用隐含一个定义,而dllimport隐含一个说明。你必须对dllexport使用extern关键字强制为一个说明;否则,隐含是一个定义。

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

extern DllImport int k; /*这是正确的并隐含一个说明*/

Dllimport int j;

Microsoft特殊处结束

用dllexport和dllimport定义联编函数

Microsoft特殊处

你可以用dllexport属性定义一个联编函数,在这种情况下,该函数总是被实例化和被输出,无论程序中的任何模块引用该函数。该函数假定是被另一程序输入。

你也可以用dllimport属性说明一个函数为联编函数,在这种情况下,该函数可以被伸展(从属于/Ob(联编)编译器选项规格)但不能被实例化。在特殊情况中,如果一个联编输入的函数的地址被占用,该函数的地址保留在返回的DLL中。这个行为和占用一个非联编输入的函数的地址相同。在联编函数中的静态局部数据和字符串在DLL和象在单个程序中似的客户(也就是,一个没有DLL界面的可执行文件)之间维护相同的标识符。

在进行提供输入的联编函数的练习时要小心,例如,如果你修改DLL,不要假设该客户使用该DLL的改变的版本。为了保证你加载适当的DLL版本,重新建立该DLL的客户。

Microsoft特殊处结束

dllimport/dllexport的规则和限制

Microsoft特殊处

* 如果你说明一个函数没有dllimport或dllexport属性,该函数不认为是DLL界面的部分。因此,该函数的定义必须出现在该模块中或相同程序的另一个模块中。为了使该函数成为DLL界面部分,必须在其它模块中以dllexport说明该函数的定义;否则,在建立客户时产生一个链接器错误。

* 如果你的程序的单个模块包含相同函数的dllimport和dllexport说明,那么dllexport属性的优先级比dllimport属性的优先级高。但编译器产生一个警告。例如:

#define DLLimport __declspec(dllimport)

#define DLLexport __declspec(dllexport)

DllImport void func1(void); DllExport void func1(void);/*警告:dllexport更优先*/

* 你不能用一个以dllimport属性说明的数据对象的地址初始化一个静态数据指针。

例如,如下代码产生一个错误:#define DllImport __declspec(dllimport)#define DllExport __declspec(dllexport) DllImport int i ; . . . int *pi=&i; /* 错误 */ void func2() { static int *pi=&i; /* 错误 */ }

* 用一个dllimport说明的函数的地址初始化一个静态函数指针,设置该指针为该DLL输入形实替换程序(一个转换控制到该函数的代码块)而不是该函数的地址。如下赋值不产生错误消息:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllImport void func1(void)

. . . static void (*pf)(void)=&func1;/* 没有错误 */

void func2()

{

static void (*pf)(void)=&func1;/* 没有错误 */

}

* 因为在一个对象的说明中包括dllexport属性的程序必须提供这个对象的定义,你可以用一个dllexport函数的地址初始化一个全局或局部静态函数指针。类似地,你可以用一个dllexport数据对象的地址初始化一个全局或局部静态数据指针。例如:

#define DllImport __declspec(dllimport)

#define DllExport __declspec(dllexport)

DllImport void func1(void);

DllImport int i;

DllExport void func1(void);

DllExport int i;

. . .

int *pi=&i; /* 正确 */

static void(*pf)(void) = &func1;

/* 正确 */

void func2() {      static int *pi=i; /* 正确 */      static void (*pf)(void) = &func1; /* 正确 */ }

Microsoft特殊处结束

naked函数

Microsoft特殊处

naked存储类属性是C语言的一个Microsoft特殊处扩充。对于用naked存储类属性说明的函数,编译器生成没有序言和结尾部分的代码。你可以利用这个特征,在使用联编写虚拟设备驱动程序中特别有用。

因为naked属性仅与一个函数的定义有关,它不是一个类型修饰符,naked

函数使用第3章“说明和类型”中的“扩充的存储类属性”中描述的扩充的属性语法。

下面的例子用naked属性定义了一个函数:

__declspec(naked)int func(formal_parameters)

{

/* 函数体 */

}

或者,另一种方式是:

#define naked __declspec (naked)

naked int func(formal_parameters)

{

/* 函数体 */

}

naked属性只影响编译器生成该函数的序言和结尾部分序列的代码,它不影响调用这样函数生成的代码。因此,naked属性不认为是该函数的类型部分,函数指针不能有naked属性。而且naked属性不能应用于一个数据定义。例如,如下代码产生错误:

__declspec(naked) int i; /* 错误-naked属性不允许用在数据说明中 */naked属性只能与函数的定义相关,而不能在函数的原型中指出,下面的说明产生一个编译器错误:

__declspec(naked) int func(); /* 错误-naked属性不允许在函数说明中 */

Microsoft特殊处结束

使用naked函数的规则和限制

Microsoft特殊处

* 在一个naked函数中不允许使用return语句。但可以把该返回值移到RET指令之前的EAX寄存器。

* 在一个naked函数中不允许使用结构的异常处理指令,因为该指令必须在栈框架中执行取消内务操作。

* 在一个naked函数中不允许使用setjmp运行函数,因为它必须在栈框架中执行取消内务操作。但longjmp运行函数是允许的。

* 在一个naked函数中不允许使用_alloc函数。

* 为了确保在序言之前不出现局部变量的初始化代码,在函数范围不允许初始化的局部变量。

* 不推荐使用框架指针优化(/Oy编译器选项),但它为了naked函数而自动取消。Microsoft特殊处结束编写序言/结尾部分代码的考虑

Microsoft特殊处

在你编写自已的序言和结尾部分代码序列之前,重要的是了解栈框架的布局,它对于了解如何使用__LOCAL_SIZE预定义的常量也是重要的。

栈框架布局

这个例子说明了在一个32位函数中可以出现的标准序言代码:

push ebp ;存储ebp

mov ebp,esp ;设置栈框架指针

sub esp,localbytes ; 为局部变量分配空间

push ;存储registersloc

lbytes变量表示在栈上局部变量需要的字节数,registers变量是表示在栈上保存寄存器表的位置占用者。在下推寄存器后,你可以把任何其它适当的数据放在栈中。如下是对应的结尾部分代码:

pop ;恢复registers

mov esp ebp ;恢复栈指针

pop ebp ;恢复ebp

ret ;从函数返回

栈总是向下推(从高到低存储器地址)。基指针(ebp)指向ebp的推入的值,局部变量区域起始于ebp-2。为了访问该局部变量,通过ebp减适当的值计算从ebp的偏移量。

__LOCAL_SIZE常量

编译器为联编汇编器块中使用函数序言代码而提供了一个常量__LOCAL_SIZE。这个常量用在定制序言代码中用于在栈上为局部变量分配空间。

编译器确定__LOCAL_SIZE的值,该值是所有用户定义的局部变量和编译器生成的临时变量的总的字节个数。__LOCAL__SIZE仅用作一个立即操作数;它不能用在一个表达式中。

你不能改变或重新定义该常量的值。例如:

mov eax, __LOCAL_SIZE ; 立即操作数-正确

mov eax, [ebp-__LOCAL_SIZE] ; 错误

如下一个naked函数的例子包含定制序言和结尾部分序列:

__declspec( naked ) func(){   int i;   int j;    __asm   /* 序言 */   {        push ebp    mov ebp,esp    sub esp,__LOCAL_SIZE    }/* 函数体 */__asm  /* 结尾部分 */   {    mov esp,ebp    pop ebp     ret}}

Microsoft特殊处结束

存储类

在函数定义中的存储类给出该函数为extern或static存储类。

语法:

函数定义:

说明指示符opt 属性序列opt 说明符 说明表opt 复合语句

/* 属性序列是Microsoft特殊处 */

说明指示符:

存储类指示符 说明指示符opt

类型指示符 说明指示符opt

类型修饰符 说明指示符opt

存储类指示符: /* 对于函数定义 */

extern

static

如果一个不包括存储类指示符的函数定义,该存储类缺省为extern。你可以显式说明一个函数为extern,但它可以不需要。

如果一个函数说明包含存储类指示符extern,该标识符和任何文件范围的标识符的可见说明具有相同的连接。如果没有文件范围的可见说明,该标识符具有外部的连接。如果一个标识符有文件范围且没有存储类指示符,那么该标识符具有外部的连接。外部的连接意味着该标识符的每个实例指示相同的对象或函数。有关连接和文件范围的更多信息参见第2章“程序结构”中的“生存期、范围、可见性和连接”。

用一非extern的存储类指示符说明一个块范围的函数会产生错误。具有static存储类的一个函数仅在定义它的源文件中是可见的。所有其它函数,不论它们是显式还是隐含地给出extern存储类,它们在程序中的所有源文件中都是可见的。如果期待是static存储类,必须在该函数说明(如果有)的第一次出现或者该函数的定义上说明它为该存储类。

Microsoft特殊处

当允许Microsoft扩充时,最初说明没有一个存储类(或有extern存储类)的函数,如果该函数的定义在同一源文件中,且该定义显式指出static存储类,则该函数给出为static存储类。

当用/Ze编译器选项编译时,在一个块中用extern关键字说明的函数具有全局可见性。

当用/Za编译时不是这样的。如果考虑源代码的移植性,这个特征是不可靠的。

Microsoft特殊处结束

返回类型

一个函数的返回值建立该函数返回值的尺寸和类型,对应于下面语法中的类型指示符:

语法

函数定义:

说明指示符opt 属性序列opt 说明符 说明表opt 复合语句

/* 属性序列是Microsoft特殊处 */

说明指示符:

存储类指示符 说明指示符opt

类型指示符 说明指示符opt

类型修饰符 说明指示符opt

类型指示符:

void

char

short

int

long

float

double

signed

unsigned

结构或联合指示符

枚举指示符

typedef名称

类型指示符可以指出任何基本的、结构或联合类型。如果你不包括类型指示符,返回的类型假设是int。

在函数定义中给出的返回值必须与在程序中其它地方该函数说明的返回值相匹配。当执行包含一个表达式的return语句时一个函数返回一个值。对该表达式求值时,如果需要,要转换该返回值的类型,并返回到该函数被调用的地方。如果一个函数被说明为返回类型是void,则一个包含表达式的返回语句会产生一个警告,并不对其中的表达式进行求值。

如下例子说明了函数的返回值:

typedef struct{   char name[20];   int id;   long class;}

STUDENT;

/* 返回的类型是STUDENT */

STUDENT sortstu(STUDENT a,STUDENT b){   return((a.id < b.id) ? a : b);}

这个例子用一个typedef说明定义了STUDENT类型,并定义函数sortstu具有STUDENT返回类型。该函数选择并返回它的两个结构参量之一。在该函数的后续调用中,编译器进行检测以确保参量类型为STUDENT。注意:通过传送该结构的指针而不是整个结构可以提高效率。

char *smallstr(char s1[],char s2[]){   int i;   i=0;   while (s1[i]!=′/0′ && s2[i]!= ′/0′)      i++;   if (s1[i]== ′/0′)      return(s1);   else      return(s2);}

这个例子定义了一个这样的函数,该函数返回一个字符数组的指针。该函数有两个字符数组的(字符串)参量并返回两个字符串中较短的指针,一个数组的指针指向第一个数组元素并具有它的类型;因此,该函数的返回类型是char类型的指针。

对于返回int类型的函数,你不需要在调用它们之前说明它的int返回类型,虽然建议使用原型以便对参量和返回值进行正确的类型检测。

参 数

参量是通过一个函数调用传给该函数的值的名称,参数是该函数期待接受的值。在一个函数原型中,紧跟函数名称后面的圆括号包含该函数的参数及其类型的完整表。

参数说明指出存储在参数中的值的类型、尺寸和标识符。

语法

函数定义:

说明指示符opt 属性序列opt 说明符 说明表opt 复合语句

/* 属性序列是Microsoft特殊处 */

说明符:

指针opt 直接说明符直接

说明符:

/* 一个函数说明符 */

直接说明符(参数类型表)/*新式说明符*/

参数类型表: /* 一个参数表 */

参数表

参数表,...

参数表:

参数说明

参数表,参数说明

参数说明:

说明指示符,说明符

说明指示符,抽象说明符opt

参数类型表是一个由逗号分隔的参数说明序列。在参数表中每个参数的格式如下:

[register] 类型指示符 [说明符]

用auto属性说明函数参数会产生错误,该参数的标识符用在函数体中指的是送给该函数的值。你不能在原型中命名参数,除非这些名称超出该说明结尾的范围。因此参数名称可以与函数定义中相同方式或不同方式赋值。这些标识符在函数体的最外层的块中不能重新定义,但它们可以在里层、嵌套的块中好像参数表是一个括起来的块重新定义。

在参数类型表中的每个标识符之前必须有适当的类型指示符,正如下面的例子指示的:

void new (double x, double y, double z)

{

/* 这里是函数体 */

}

如果在参数表中至少出现一个参数,该表可以以一个逗号跟三个句点(,...)结尾。这个构造称为“省略用法”,指出该函数可变的参量个数(有关更多信息,参见本章后面的“用可变个数参量调用”)。但调用该函数的参量个数必须至少有逗号之前的参数个数多。

如果没有参量传送给该函数,该参数表由关键字void代替,这个void的使用不同于作为一个类型指示符的使用。

参数的次序和类型包括任何省略用法,在所有函数说明(如果有)和函数定义中必须都是相同的。在常用算术转换之后参量的类型必须与对应参数的类型进行兼容赋值(对于算术转换的信息参见第4章“表达式和赋值”中的“常用的算术转换”),省略号后的参量不进行检测。一个参数可以有任何基本的、结构、联合、指针或数组类型。

如果必要,编译器在每个参数和每个参量上执行独立的常用算术转换。在转换之后,没有短于int的参数,没有参数具有float类型,除非该参数类型在原型中显式指定为float。例如,这意味着说明一个参数为char与说明它为int具有相同的作用。

函数体

一个“函数体”是包含指定该函数执行的语句的复合语句。

语法

函数定义:

说明指示符opt 属性序列opt 说明符 说明表opt 复合语句

/* 属性序列是Microsoft特殊处 */

复合语句:

/* 函数体 */

{

说明表opt 语句表opt

}

在一个函数体中说明的变量、“局部变量”除非指定其它的存储类型,否则具有auto存储类。当一个函数被调用时,为局部变量建立存储并执行局部初始化。执行控制传给该复合语句中的第一个语句并继续执行直到遇到一个return语句或遇到该函数体结束。然后控制返回到函数被调用的地方。

如果该函数有一个返回值,必须执行包含一个表达式的return语句。如果没有return语句执行或者return语句不包含表达式,该函数的返回值是不确定的。


函数原型

一个函数说明位于函数定义之前,指出一个函数的名称、返回类型、存储类型和其它属性。为了作为一个原型使用,该函数说明也必须建立该函数参量的类型和标识符。

语法

说明:

说明指示符 属性序列opt 初始说明符表opt

/* 属性序列opt是Microsoft特殊处 *

/说明指示符:

存储类指示符 说明指示符opt

类型指示符 说明指示符opt

类型修饰符 说明指示符opt

初始说明符表:

初始说明符

初始说明符表,初始说明符初始

说明符:

说明符

说明符=初始化器

说明符:

指针opt 直接说明符

直接说明符: /* 一个函数说明符 */

直接说明符(参数类型表) /*

新式说明符 */ 直接说明符(标识符表opt) /* 已废除的说明符 */

该原型和函数定义具有相同的格式,除了一个分号直接跟在圆括号之后终止原型外,因此原型没有语句体。在每种情况下,返回值必须与函数定义中指定的返回值一致。

函数原型具有如下重要用途:

* 它们建立函数返回非int类型的返回类型,虽然返回int值的函数不需要原型,但建议有原型。

* 没有完成的原型,要进行标准转换,但不要试图用参数个数检测参量的类型和个数。

* 原型在函数定义之前用于初始化该函数的指针。

* 参数表用于对函数定义中的参数与函数调用中对应的参量进行检测。

每个参数转换的类型确定了栈中放置的函数调用参量的解释。参量和参数之间的一个类型不匹配导致栈中的参量被误解释。例如,在一个16位计算机中,如果一个16位指针作为一个参量传送,那么说明作为一个long参数,栈中的开头32位解释为一个long参数。

这个错误不仅对该long参数产生问题,而且对于随后的任何参数也产生问题。你可以通过所有函数说明完整的函数原型来检测这种错误。

一个原型建立一个函数的属性,以便在其定义(或出现在其它源文件中)之前的函数调用检测参数类型和返回类型的不匹配。例如,如果你在一个原型中指出static存储表指示符,你必须在函数定义中指定static存储类。完整参数说明(int a)可以在同一说明中与抽象说明符(int)混合使用。例如,如下说明是合法的:

int add (int a,int);

该原型可以包括两种形式,一种是类型,一个是标识符,每个表达式作为一个参量传送。

但这样的标识符的范围只有到该说明结束为止。原型也可以反映参量个数是可变的事实,或者没有传送的参量。没有这样的一个表,不会反映出不匹配,因此编译器不能生成有关它们的诊断消息,有关类型检测的更多信息,参见本章后面的“参量”。

现在Microsoft C编译器的原型范围在使用/Za编译器选项编译时是与ANSI一致的。这意味着如果你在一个原型中说明一个struct或union标志,该标志进入这个范围而不是一个全局范围。例如,当用/Za与ANSI一致性的方式编译时,你不调用这个函数不会得到一个类型不匹配的错误:void func1(struct s*);

为了修正你的代码,在该函数原型之前以全局范围定义或说明为struct或union:struct s;

void func1(struct s*);

在/Ze下,该标志仍进入全局范围中。


函数调用

一个函数调用是一个传送控制和参量(如果有)给一个函数的表达式,它有格式:

表达式(表达式表opt)

这里的表达式是一个函数名称或一个函数地址的求值,表达式表是一个表达式(用逗号分隔)的表。这些表达式的最后值是传送给该函数的参量。如果该函数不返回一个值,那么你说明它为一个返回void的函数。如果一个说明在函数调用之前存在,但没有给出有关参数的信息,任何未说明的参量简单地经历常用的算术转换。

注意:函数参量表中的表达式可以以任何次序求值。因此其值被其它参量的副作用所改变的参量具有不确定的值。函数调用运算符所定义的顺序点仅保证参量中的所有副作用在控制传送给被调用函数之前被求值(注意,参量推入栈的次序是另一个事情)。有关更多信息参见第4章“表达式和赋值”中的“顺序点”。

在任何函数调用中的唯一要求是圆括号之前的表达式必须求值为一个函数地址。这意味着一个函数可以通过任何函数指针表达式调用。

例子

这个例子说明函数调用,它从一个switch语句调用函数:

void main(){    /* 函数原型 */    long lift(int),step(int),drop(int);    void work(int number,long (*function)(int i));    int select,count;    .select =1;    switch(select)   {       case 1:work(count,lift);             break;       case 2:work(count,step);            break;       case 3:work(count,drop);            /* 进入到下一个case */       default:            break;   }}

/* 函数定义 */

void work(int number,long (*function)(int i)){   int i;   long j;   for (i=j=0;i<number;i++)      j += (*function)(i);}

在这个例子中,函数在main中调用:

work (count, lift),

它传送一个整数变量count和函数lift的地址给函数work。注意该函数的地址通过给出的函数标识符简单地传送,由于一个函数标识符求值为一个指针表达式。为了以这种方式使用一个函数标识符,函数必须在该标识符被使用之前说明或定义;否则,不认识该标识符。在这种情况下,work的一个原型在main函数的开头给出。

work中的参数function说明为一个这样的函数的一个指针,该函数有一个int参量和返回一个long值。括起参数名称的圆括号是需要的;没有它们,该说明指出一个返回long值指针的函数。

函数work从for循环中通过作用如下函数调用方式调用选择的函数:(*function)(i);

一个参量i传送给被调用的函数。

参 量

在一个函数调用中的参量具有这种格式:

表达式(表达式表opt) /* 函数调用 */

在一个函数调用中,表达式表是一个表达式的表(由逗号分隔)。这些表达式的最终值传送给函数的参量。如果该函数没有参量,表达式包含关键字void。

一个参量可以是任何具有基本的、结构、联合或指针类型的值。所有参量都通过值传送。这意味着把该参量的拷贝赋给对应的参数。该函数不知道传送的参量的实际存储器位置。该函数使用这个拷贝不影响最初导出的变量。

虽然你不能作为参量传送数组或函数,你可以传送它们的指针。指针提供了一个函数通过引用访问一个值的方式。由于一个变量的指针保存变量的地址,函数使用这个地址访问该变量的值。指针参量允许一个函数访问数组和函数,尽管数组和函数不能作为参量传送。

参量求值的次序在不同编译器下和不同的优化层中可能发生变化。但在进入函数之前参量和任何副作用都必须完整地求值。有关副作用的信息参见第4章“表达式和赋值”中的“副作用”。

在一个函数调用中的表达式被求值并在函数调用中每个参量上执行常用的算术转换。如果一个原型是可用的,结果参量类型与原型对应的参数进行比较。如果它们不匹配,执行一次转换或发生一个诊断消息。参数也经历常用的算术转换。

在表达式表中的表达式个数必须与参数个数相匹配。除非该函数的原型或定义显式指出参量个数是可变的,在这种情况下,编译器检测参数表中类型名称那样多的参量,如果必要,对它进行转换。有关更多信息,参见下一节的“用可变个数参量调用”。

如果该原型的参数表仅包含关键字void,编译器在函数调用中期待0个参量,在定义中期待0个参数。如果发现了任何参量,编译器发出一个诊断消息。

例子

这个例子使用指针作为参量:

void main(){   /* 函数原型 */       void swap(int *num1,int *num2);   int x,y;   .   .   .   swap(&x,&y);  /* 函数调用 */}

/* 函数定义 */

void swap(int *num1,int *num2){     int t;   t=*num1;   *num1=*num2;   *num2=t;}

在这个例子中,swap函数在main中说明,它有两个参量,分别用标识符num1和num2表示,两个都是int值的指针。在原型定义中参量num1和num2也都说明为int类型值的指针。

在函数调用中:

swap (&x,&y);

x的地址存储在num1中,y的地址存储在num2中,现在在同样的位置存在两个名称或“别名”。swap中*num1和*num2的引用有效地引用main中的x和y。swap中的赋值实际上交换x和y的内容。因此,不必要使用return语句。

编译器在swap的参量上执行类型检测,swap的原型包括每个参数的参量类型。在该原型和定义中圆括号中的标识符可以是相同或不相同的。重要的是在原型和定义中参量的类型必须与参数其中的类型相匹配。

用可变个数参量调用

部分参数表可以通过省略标记终止。一个逗号后跟三个句点(,...)指出可以有更多的参量传送给函数,但它们没有给出更多信息。类型检测不是在每个参量上执行。在省略标识的前面必须至少有一个参数,省略标记必须是参数表中最后一个语言符号。没有省略标记,如果它接受除了参数表中说明的参数外的参数时,该函数的行为是不确定的。

为了用一个可变个数的参量调用一个函数,在函数调用中简单地指出任何个数的参量。

一个例子是C运行库的printf函数,该函数调用对于参数表或参量类型中说明的每个类型名称必须包含一个参量。

在函数调用中指定的所有参量都放在栈中,除非指定__fastcall调用约定。函数说明的参数个数确定从栈中取多少参量并赋给这些参数。你要负责从栈中接受任何另外的参量并确定出现多少参量。STDARGS.H文件包含访问具有可变参量个数的函数的参量的ANSI方式宏。

同样,也支持包含在VARARGS.H中的XENIX方式宏。

这个样本说明是一个调用可变参量个数的函数:

int averagc(int first,...)

Microsoft特殊处

为了维护与Microsoft C以前版本的兼容性,ANSI C标准的一个Microsoft扩充是允许参数表的末尾有一个没有句点的逗号(,)指出可变的参量个数。但建议把这个代码改变为加上省略标记。

Microsoft特殊处结束

递归函数

在C程序中的任何函数可以递归调用,也就是,它可以调用本身。递归调用的次数受限于栈的尺寸。有关设置栈尺寸的链接器选项的信息,参见联机“Microsoft Visual C++ 6.0程序员指南”中的“栈分配”(/STACK)链接器选项。每次调用该函数时,为其参数和auto及register变量分配新的存储以便它们前面的未完成的调用不被覆盖。参数只能直接访问建立它们的函数的实例。以前的参数不能直接访问后面的函数实例。

注意有static存储说明的变量在每次递归调用时不需要新的存储。它们的存储存在于程序的生存期中,这样的变量的每次引用都访问相同的存储区域。

例子

这个例子说明了递归调用:

int factorial(int num);/* 函数原型 */void main(){  int result,number;  result=factorial(number);}
int factorial(int num)   /* 函数定义 */{if ((num>0)  ||  (num<=10))     return (num*factorial(num-1));}
原创粉丝点击