浅析C语言中的数组及字符数组
我们来编写一个程序,以统计各个数字、空白符(包括空格符、制表符及换行符)以及所有其它字符出现的次数。这个程序的实用意义并不大,但我们可以通过该程序讨论C语言多方面的问题。
所有的输入字符可以分成12类,因此可以用一个数组存放各个数字出现的次数,这样比使用10个独立的变量更方便。下面是该程序的一种版本:
#include<stdio.h> /*countdigits,whitespace,others*/ main() { intc,i,nwhite,nother; intndigit[10]; nwhite=nother=0; for(i=0;i<10;++i) ndigit[i]=0; while((c=getchar())!=EOF) if(c>='0'&&c<='9') ++ndigit[c-'0']; elseif(c==''||c=='\n'||c=='\t') ++nwhite; else ++nother; printf("digits="); for(i=0;i<10;++i) printf("%d",ndigit[i]); printf(",whitespace=%d,other=%d\n",nwhite,nother); }
当把这段程序本身作为输入时,输出结果为:digits=9300000001,whitespace=123,other=345
该程序中的声明语句intndigit[10]将变量ndigit声明为由10个整型数构成的数组。在C语言中,数组下标总是从0开始,因此该数组的10个元素分别为ndigit[0]、ndiglt[1]、…、ndigit[9],这可以通过初始化和打印数组的两个for循环语句反映出来。
数组下标可以是任何整型表达式,包括整型变量(如i)以及整型常量。
该程序的执行取决于数字的字符表示属性。例如,测试语句if(c>='0'&&c<='9')用于判断c中的字符是否为数字。如果它是数字,那么该数字对应的数值是c-'0'。只有当'0'、'1'、…、'9'具有连续递增的值时,这种做法才可行。幸运的是,所有的字符集都是这样的。
由定义可知,char类型的字符是小整型,因此char类型的变量和常量在算术表达式中等价于int类型的变量和常量。这样做既自然又方便,例如,c-'0'是一个整型表达式,如果存储在c中的字符是'0'~'9',其值将为0~9,因此可以充当数组ndigit的合法下标。
判断一个字符是数字、空白符还是其它字符的功能可以由下列语句序列完成:
if(c>='0'&&c<='9') ++ndigit[c-'0']; elseif(c==''||c=='\n'||c=='\t') ++nwhite; else ++nother;
程序中经常使用下列方式表示多路判定:
if(条件1)
语句1
elseif(条件1)
语句2
...
...
else
语句n
在这种方式中,各条件从前往后依次求值,直到满足某个条件,然后执行对应的语句部分。这部分语句执行完成后,整个语句体执行结束(其中的任何语句都可以是括在花括号中的若干条语句)。如果所有条件都不满足,则执行位于最后一个else之后的语句(如果有的话)。类似于前面的单词计数程序,如果没有最后一个else及对应的语句,该语句体将不执行任何动作。在第一个if与最后一个else之间可以有0个或多个下列形式的语句序列:
elseif(条件)
语句
就程序设计风格而言,我们建议读者采用上面所示的缩进格式以体现该结构的层次关系,否则,如果每个if都比前一个else向里缩进一些距离,那么较长的判定序列就可能超出页面的右边界。
字符数组
字符数组是C语言中最常用的数组类型。下面我们通过编写一个程序,来说明字符数组以及操作字符数组的函数的用法。该程序读入一组文本行,并把最长的文本行打印出来。该算法的基本框架非常简单:
while(还有未处理的行)
if(该行比已处理的最长行还要长)
保存该行为最长行
保存该行的长度
打印最长的行
从上面的框架中很容易看出,程序很自然地分成了若干片断,分别用于读入新行、测试读入的行、保存该行,其余部分则控制这一过程。
因为这种划分方式比较合理,所以可以按照这种方式编写程序。首先,我们编写一个独立的函数getline,它读取输入的下一行。我们尽量保持该函数在其它场台也有用。至少getline函数应该在读到文件末尾时返回一个信号;更为有用的设计是它能够在读入文本行时返回该行的长度,而在遇到文件结束符时返回0。由于0不是有效的行长度,因此可以作为标志文件结束的返回值。每一行至少包括一个字符,只包含换行符的行,其长度为1。
当发现某个新读入的行比以前读入的最长行还要长时,就需要把该行保存起来。也就是说,我们需要用另一个函数copy把新行复制到一个安全的位置。
最后,我们需要在主函数main中控制getline和copy这两个函数。以下便是我们编写的程序:
#include<stdio.h> #defineMAXLINE1000/*maximuminputlinelength*/ intgetline(charline[],intmaxline); voidcopy(charto[],charfrom[]); /*printthelongestinputline*/ main() { intlen; intmax; /*currentlinelength*/ /*maximumlengthseensofar*/ charline[MAXLINE];/*currentinputline*/ charlongest[MAXLINE];/*longestlinesavedhere*/ max=0; while((len=getline(line,MAXLINE))>0) if(len>max){ max=len; copy(longest,line); } if(max>0)/*therewasaline*/ printf("%s",longest); return0; } /*getline:readalineintos,returnlength*/ intgetline(chars[],intlim) { intc,i; for(i=0;i<lim-1&&(c=getchar())!=EOF&&c!='\n';++i) s[i]=c; if(c=='\n'){ s[i]=c; ++i; } s[i]='\0'; returni; } /*copy:copy'from'into'to';assumetoisbigenough*/ voidcopy(charto[],charfrom[]) { inti; i=0; while((to[i]=from[i])!='\0') ++i; } </stdio.h>
程序的开始对getline和copy这两个函数进行了声明,这里假定它们都存放在同一个文件中。
main与getline之间通过一对参数及一个返回值进行数据交换。在getline函数中,两个参数是通过程序行。
intgetline(chars[],intlim)
声明的,它把第一个参数s声明为数组,把第二个参数lim声明为整型,声明中提供数组大小的目的是留出存储空间。在getline函数中没有必要指明数组s的长度,这是因为该数组的大小是在main函数中设置的。如同power函数一样,getline函数使用了一个return语句将值返回给其调用者。上述程序行也声明了getline数的返回值类型为int。由于函数的默认返回值类型为int,因此这里的int可以省略。
有些函数返回有用的值,而有些函数(如copy)仅用于执行一些动作,并不返回值。copy函数的返回值类型为void,它显式说明该函数不返回任何值。
getline函数把字符'\0'(即空字符,其值为0)插入到它创建的数组的末尾,以标记字符串的结束。这一约定已被C语言采用:当在C语言程序中出现类似于
"hello\0"
的字符串常量时,它将以字符数组的形式存储,数组的各元素分别存储字符串的各个字符,并以'\0'标志字符串的结束。
printf函数中的格式规范%s规定,对应的参数必须是以这种形式表示的字符串。copy函数的实现正是依赖于输入参数由'\0'结束这一事实,它将'\0'拷贝到输出参数中。也就是说,空字符'\0'不是普通文本的一部分。
值得一提的是,即使是上述这样很小的程序,在传递参数时也会遇到一些麻烦的设计问题。例如,当读入的行长度大于允许的最大值时,main函数应该如何处理,getline函数的执行是安全的,无论是否到达换行符字符,当数组满时它将停止读字符。main函数可以通过测试行的长度以及检查返回的最后一个字符来判定当前行是否太长,然后再根据具体的情况处理。为了简化程序,我们在这里不考虑这个问题。
调用getline函数的程序无法预先知道输入行的长度,因此getline函数需要检查是否溢出。另一方面,调用copy函数的程序知道(也可以找出)字符串的长度,因此该函数不需要进行错误检查。