函数可以把大的计算任务分解成若干个较小的任务,程序设计人员可以基于函数进一步构造函数,而不需要重新编写一些代码。一个设计得当的函数可以把程序中不需要了解的具体操作细节隐藏起来,从而使得整个程序结构更加清晰,并降低修改程序的难度。
C语言在设计中考虑了函数的高效性与易用性这两个因素。C语言程序一般都由许多小的函数组成,而不是由少量较大的函数组成。一个程序可以保存在一个或者多个源文件中。各个文件可以单独编译,并可以与库中已编译过的函数一起加载。
4.1函数的基本知识
尽管我们可以把所有的代码都放在主程序main中,但更好的做法是,利用其结构把每一部分设计成一个独立的函数。
分别处理3个小的部分要比处理一个大的整体更容易,因为这样可以把不相关的细节隐藏在函数中,从而减少了不必要的相互影响的机会,这些函数也可以在其他程序中使用。
函数定义形式如下:
返回值类型 函数名(参数声明表)
{
声明和语句
}
函数定义中的各构成部分都可以省略,最简单的函数如下所示: dummy(){} (注:在C99标准中,函数定义需要有返回值类型)该函数不执行任何操作也不返回任何值。这种不执行任何操作的函数有时很有用,它可以在程序开发期间用以保留位置留待以后填充代码。如果函数定义中省略了返回值类型,则默认为int类型。(c99不支持)
程序可以看成是变量定义和函数定义的集合。函数之间的通信可以通过参数、函数返回值以及外部变量进行。函数在源文件中出现的次序可以是任意的。只要保证每一个函数不被分离到多个文件中,源程序就可以分成多个文件。
被调用函数通过return语句向调用者返回值,return语句的后面可以跟任何表达式,在必要时,表达式将被转换为函数的返回值类型。表达式两边通常加一对圆括号,此处的括号是可选的。
调用函数可以忽略返回值,并且return语句的后面也不一定需要表达式,当return语句的后面没有表达式时,函数将不向调用者返回值。
当被调用函数执行到最后的右花括号而结束执行时,控制同样会返回给调用者(不返回值)。
4.2返回非整型值的函数
如果函数带有参数,则要声明它们;如果没有参数,则使用void进行声明。
4.3外部变量
C语言程序可以看成由一系列的外部对象构成,这些外部对象可能是变量或函数。internal用于描述定义在函数内部的函数参数及变量。外部变量定义在函数之外,因此可以在许多函数中使用。C语言不允许在一个函数中定义其他函数,因此函数本身是外部的。默认情况下,外部变量与函数具有下列性质:通过同一个名字对外部变量的所有引用实际上都是引用同一个对象。
外部变量可以在全局范围内访问,这就为函数之间的数据交换提供了一种可以代替函数参数与返回值的方式。任何函数都可以通过名字访问一个外部变量。
如果函数之间需要共享大量的变量,使用外部变量要比使用一个很长的参数表更方便、有效,但这样做必须非常谨慎,因为这种方式可能对程序结构产生不良的影响,而且可能会导致程序中各个函数之间具有太多的数据联系。
外部变量的用途还表现在它们与内部变量相比具有更大的作用域和更长的生存期。
自动变量只能在函数内部使用,从其所在的函数被调用时变量开始存在,在函数退出时变量也将消失。
外部变量是永久存在的,它们的值在一次函数调用到下一次函数调用之间保持不变。因此,如果两个函数必须共享某些数据,而这两个函数互不调用对方,这种情况下最方便的方式便是把这些共享数据定义为外部变量,而不是作为函数参数传递。
4.4作用域规则
构成C语言程序的函数与外部变量可以分开进行编译,一个程序可以存放在几个文件中,原先已编译过的函数可以从库中进行加载。
名字的作用域指的是程序中可以使用该名字的部分。
对于在函数开头声明的自动变量来说,其作用域是声明该变量名的函数。不同函数中声明的具有相同名字的各个局部变量之间没有任何关系。函数的参数也是这样,实际上可以把它看做是局部变量。
外部变量或函数的作用域从声明它的地方开始,到其所在的(待编译)文件的末尾结束。
如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字extern。
将外部变量的声明与定义严格区分开来很重要。变量声明用于说明变量的属性(主要是变量的类型),变量定义除此以外还将引起存储器的分配。
在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern声明)。
外部变量的定义中必须指定数组的长度,但extern声明则不一定要指定数组的长度。
外部变量的初始化只能出现在其定义中。
4.5头文件
我们尽可能把一个程序各个源文件共享的部分集中在一起,这样就只需要一个副本,改进程序时也容易保证程序的正确性。我们把公共的部分放在头文件中,在需要使用该头文件时通过#include指令将它包含进来。
一方面我们期望每个文件只能访问它完成任务所需的信息,另一方面是现实中维护较多的头文件比较困难。我们可以得出这样一个结论:对于某些中等规模的程序,最好只用一个头文件存放程序中各部分共享的对象。较大的程序需要使用更多的头文件,我们需要精心组织它们。
4.6静态变量
用static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。通过static限定外部对象,可以达到隐藏外部对象的目的。
要将对象制定为静态存储,可以在正常的对象声明之前加上关键字static作为前缀。
外部的static声明多用于变量,当然,它也可用于声明函数。通常情况下函数名字是全局可访问的,对整个程序的各个部分而言都可见,但是如果把函数声明为static类型,则该函数名除了对该函数声明所在的文件可见外,其他文件都无法访问。
static也可用于声明内部变量。static类型的内部变量同自动变量一样,是某个函数的局部变量,只能在该函数中使用,但它与自动变量不同的是,不管其所在函数是否被调用,它一直存在,而不像自动变量那样,随着所在函数的被调用和退出而存在和消失。static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。
4.7寄存器变量
register声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想是将register变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。但编译器可以忽略此选项。
register声明只适用于自动变量以及函数的形式参数。
实际使用时,底层硬件环境的实际情况对寄存器变量的使用会有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。但是,过量的寄存器声明并没有什么害处,这是因为编译器可以忽略过量的或不支持的寄存器变量声明。另外,无论寄存器变量实际是不是存放在寄存器中,它的地址都是不能访问的。
4.8程序块结构
C语言不允许在函数中定义函数,但是在函数中可以以程序块结构的形式定义变量。
变量的声明(包括初始化)除了可以紧跟在函数开始的花括号之后,还可以紧跟在任何其他标识复合语句开始的左花括号之后。以这种方式声明的变量可以隐藏程序块外与之同名的变量,它们之间没有任何关系,并在与左花括号匹配的右花括号出现之前一直存在。
每次进入程序块时,在程序块内声明以及初始化的自动变量都将被初始化。静态变量只在第一次进入程序块时被初始化一次。
自动变量(包括形式参数)也可以隐藏同名的外部变量与函数。
在一个好的程序设计风格中,应该避免出现变量名隐藏外部作用域中相同名字的情况,否则,很可能引起混乱和错误。
4.9初始化
在不进行显示初始化的情况下,外部变量和静态变量都将被初始化为0,而自动变量和寄存器变量的初值则没有定义(即初值为无用的信息)。
定义标量变量时,可以在变量名后紧跟一个等号和表达式来初始化变量。
对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次。对于自动变量与寄存器变量,则在每次进入函数或程序块时都被初始化。
对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式:表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用。
实际上,自动变量的初始化等效于简写的赋值语句。采用哪一种形式取决于个人习惯。考虑到变量声明中的初始化表达式容易被人忽略,且距使用位置较远,我们一般使用显式的赋值语句。
数组的初始化可以在声明的后面紧跟一个初始化表达式列表,初始化表达式列表用花括号括起来,各初始化表达式之间通过逗号分隔,当省略数组的长度时,编译器把花括号中初始化表达式的个数作为数组的长度。
如果初始化表达式的个数比数组元素数少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0。如果初始化表达式的个数比数组元素数多,则是错误的。不能一次将一个初始化表达式指定给多个数组元素,也不能跳过前面的数组元素而直接初始化后面的数组元素。
字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用逗号分隔的初始化表达式序列。
4.10递归
C语言函数可以递归调用,即函数可以直接或间接调用自身。
递归并不节省存储器的开销,因为递归调用过程中必须在某个地方维护一个存储处理值的栈。
递归的执行速度并不快,但递归代码比较紧凑,并且比相应的非递归代码更易于编写与理解。
4.11C预处理器
预处理器是编译过程中单独执行的第一个步骤。
两个常用的预处理器指令是:#include指令(用于在编译期间把指定文件的内容包含进当前文件中)和#define指令(用任意字符序列替代一个标记)
在源文件中,任何形如#include "文件名" 或#include <文件名> 的行都将被替换为由文件名指定的文件的内容。如果文件名用引号引起来,则在源文件所在位置查找该文件;如果在该位置没有找到文件,或者如果文件名是用尖括号括起来的,则根据相应的规则查找该文件。被包含的文件本身也课包含#include指令。
在大的程序中,#include指令是将所有声明捆绑在一起的较好的方法。它保证所有源文件都具有相同的定义与变量声明,可以避免一些不必要的错误。如果某个包含文件的内容发生了变化,那么所有依赖于该包含文件的源文件都必须重新编译。
宏定义的形式如下:#define 名字 替换文本 ,后续所有出现名字记号的地方都将被替换为替换文本。
#define指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串。
通常情况下#define指令占一行,替换文本是#define指令行尾部的所有剩余部分内容,但也可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠号\。
#define指令定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束。
宏定义中也可以使用前面出现的宏定义,替换只对记号进行,对括在引号中的字符串不起作用。
宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本。
如果对各种类型的参数的处理是一致的,则可以将同一个宏定义应用于任何数据类型而无需针对不同的数据类型需要定义不同的函数。
对于带参数的宏定义,要适当使用圆括号以保证计算次序的正确性。
getchar与putchar函数在实际中常常被定义为宏,这样可以避免处理字符时调用函数所需的运行时开销。
可以通过#undef指令取消名字的宏定义,这样可以保证后续的调用是函数调用而不是宏调用。
形式参数不能用带引号的字符串替换。
如果在替换文本中,参数名以#作为前缀则结果将被扩展为由实际参数替换该参数的带引号的字符串。例如,#define dprint(expr) printf(#expr" = %g\n",expr) ,使用语句dprint(x/y); 调用该宏时,将被扩展为printf("x/y" " = %g\n",x/y); 该宏调用的效果等价于printf("x/y = %g\n",x/y); 在实际参数中,每个双引号将被替换为\",反斜杠将被替换为\\,因此替换后的字符串是合法的字符串常量。
预处理器运算符##为宏扩展提供了一种连接实际参数的手段,如果替换文本中的参数与##相邻,则该参数将被实际参数替换,##与前后的空白符将被删除,并对替换后的结果重新扫描。例如#define paste(front,back) front##back ,宏调用paste(name,1)的结果将建立记号name1。
可以使用条件语句对预处理本身进行控制。
#if语句对其中的常量整型表达式(不能包含sizeof、类型转换运算符或enum常量)进行求值,若该表达式的值不等于0,则包含其后的各行,直到遇到#endif、#elif或#else语句为止,预处理器语句#elif类似于 else if。
在#if语句中可以使用表达式defined(名字),该表达式的值遵循下列规则:当名字已经定义时,其值为1;否则,其值为0。例如,为了保证hdr.h文件的内容只被包含一次,可以将该文件包含的内容包含在下列形式的条件语句中:
#if !defined(HDR)
#define HDR
/*文件内容*/
#endif
这样可以用来避免多次重复包含同一文件,如果多个头文件能够一致地使用这种方式,那么每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系。
C语言专门定义了两个预处理语句#ifdef和#ifndef,它们用来测试某个名字是否已经定义。上面一个例子可以改写为:
#ifndef HDR
#define HDR
/*文件内容*/
#endif
感想:通过本章的学习和习题的完成,对函数的使用、外部变量、静态变量、寄存器变量、头文件有了更深层次的认识,也学会了更好地组织多个源文件组成的代码。通过书本及习题,编写了一个计算器程序(使用逆波兰表示法,即后缀表达式),如下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <ctype.h>
#define MAXOP 100 //操作数或运算符的最大长度
#define NUMBER '0' //标识找到一个数
#define BUFSIZE 100
#define MAXVAL 100 //栈val的最大深度
#define NAME 'n' //标识找到一个名字
int sp = 0; //下一个空闲栈位置
double val[MAXVAL]; //值栈
char buf[BUFSIZE]; //用于ungetch函数的缓冲区
int bufp = 0; //buf中下一个空闲位置
int getch(void); //取一个字符(可能是压回的字符)
void ungetch(int); //把字符压回到输入中
int getop(char []); //获取下一个运算符或数值操作数
void push (double f); //把f压入到值栈中
double pop(void); //弹出并返回栈顶的值
void clear(void); //清空栈
void mathfnc(char s[]); //检查字符串s是否有支持的数学函数
main()
{
int i,type,var = 0;
double op1,op2,v; //用小写字母v存放打印的值
char s[MAXOP];
double variable[26]; //用大写字母A-Z表示变量,这些字母作为数组变量的索引
for(i=0;i < 26;i++)
variable[i] = 0.0;
while((type = getop(s)) != EOF)
{
switch(type){
case NUMBER:
push(atof(s));
break;
case NAME:
mathfnc(s);
break;
case '+':
push(pop() + pop());
break;
case '*':
push(pop() * pop());
break;
case '-':
op2 = pop();
push(pop() - op2);
break;
case '/':
op2 = pop();
if(op2 != 0.0)
push(pop() / op2);
else
printf("error: zero divisor\n");
break;
case '%':
op2 = pop();
if(op2 != 0.0)
push(fmod(pop(),op2));
else
printf("error: zero divisor\n");
break;
case '?': //打印栈顶元素
op2 = pop();
printf("\t%.8g\n",op2);
push(op2);
break;
case 'c': //清空栈
clear();
break;
case 'd':
op2 = pop(); //复制栈顶元素
push(op2);
push(op2);
break;
case 's': //交换栈顶两个元素
op1 = pop();
op2 = pop();
push(op1);
push(op2);
break;
case '=': //将栈中某个元素赋值给=前面的字母变量
pop();
if(var >='A' && var <= 'Z')
variable[var - 'A'] = pop();
else
printf("error: no variable name\n");
break;
case '\n':
v = pop();
printf("\t%.8g\n",v);
break;
default:
if(type >= 'A' && type <= 'Z') //遇到变量名,将该变量的值压入栈
push(variable[type - 'A']);
else if(type == 'v')
push(v);
else
printf("error: unknown command %s\n",s);
break;
}
var = type;
}
return 0;
}
void push(double f)
{
if (sp < MAXVAL)
val[sp++] = f;
else
printf("error: stack full, can't push %g\n",f);
}
double pop(void)
{
if(sp > 0)
return val[--sp];
else
{
printf("error: stack empty\n");
return 0.0;
}
}
int getop(char s[])
{
int i,c;
while((s[0] = c = getch()) == ' '|| c == '\t')
;
s[1] = '\0';
i=0;
if(islower(c)) //识别由小写字母组成的字符串
{
while(islower(s[++i] = c = getch()))
;
s[i] = '\0';
if(c != EOF)
ungetch(c);
if(strlen(s) > 1) // 大于1个char 说明是个name
return NAME;
else
return s[0]; //可能是个指令,比如 c——清空栈等。
}
if(!isdigit(c) && c!='.' && c!= '-')
return c; //不是数
if(c == '-')
if(isdigit(c = getch()) || c == '.')
s[++i] = c; //负数
else
{
if (c != EOF)
ungetch(c);
return '-'; //减号
}
if(isdigit(c)) //收集整数部分
while(isdigit(s[++i] = c =getch()))
;
if(c == '.') //收集小数部分
while(isdigit(s[++i] = c =getch()))
;
s[i] = '\0';
if(c != EOF)
ungetch(c);
return NUMBER;
}
int getch(void)
{
return (bufp > 0) ? buf[--bufp] : getchar();
}
void ungetch(int c)
{
if (bufp >=BUFSIZE)
printf("ungetch: too many characters\n");
else
buf[bufp++] = c;
}
void clear(void)
{
sp = 0;
}
void mathfnc(char s[])
{
double op2;
if(strcmp(s,"sin") == 0)
push(sin(pop()));
else if(strcmp(s,"cos") == 0)
push(cos(pop()));
else if(strcmp(s,"exp") == 0)
push(exp(pop()));
else if(strcmp(s,"pow") == 0)
{
op2 = pop();
push(pow(pop(),op2));
}
else
{
printf("error: %s not supported\n",s);
}
}
更深刻地认识了外部变量以及组织函数之间的关系。再将各个部分分别放置在不同的源文件中,更好地了解了头文件的用法,以及变量声明、定义的不同。
本章中还介绍了快速排序,让我更加了解递归。