3.2 字符数组

文本处理在计算机应用中占有重要地位。本书到现在为止还没有正式讨论过字符串(尽管曾经使用过),因为在C语言中,字符串其实就是字符数组——可以像处理普通数组一样处理字符串,只需要注意输入输出和字符串函数的使用。

竖式问题。找出所有形如abc*de(三位数乘以两位数)的算式,使得在完整的竖式中,所有数字都属于一个特定的数字集合。输入数字集合(相邻数字之间没有空格),输出所有竖式。每个竖式前应有编号,之后应有一个空行。最后输出解的总数。具体格式见样例输出(为了便于观察,竖式中的空格改用小数点显示,但所写程序中应该输出空格,而非小数点)。

样例输入:

2357

样例输出:

<1>

..775

X..33

-----

.2325

2325.

-----

25575

The number of solutions = 1

【分析】

本题的思路应该是很清晰的:尝试所有的abc和de,判断是否满足条件。我们可以写出整个程序的伪代码:


char s[20];
int count = 0;   
scanf("%s", s);
for(int abc = 111; abc <= 999; abc++)
  for(int de = 11; de <= 99; de++)
    if("abc*de"是个合法的竖式)
    {
     printf("<%d>\n", count);
     打印abc*de的竖式和其后的空行
     count++;
    }
printf("The number of solutions = %d\n", count);

第一个新内容是char s[20]定义。char是“字符型”的意思,而字符是一种特殊的整数。为什么字符会是特殊的整数?请参见图3-1所示的ASCII编码表。

图3-1 ASCII编码表

从图3-1中可见,每一个字符都有一个整数编码,称为ASCII码。为了方便书写,C语言允许用直接的方法表示字符,例如,“a”代表的就是a的ASCII码。不过,有一些字符直接表示出来并不方便,例如,回车符是“\n”,而空字符是“\0”,它也是C语言中字符串的结束标志。其他例子包括“\\”(注意必须有两个反斜线)、“\'”(这个是单引号),甚至还有的字符有两种写法:“\"”和“"”都表示双引号。像这种以反斜线开头的字符称为转义序列(Escape Sequence)。如果认真完成了第1章中的实验,相信对这些字符不会陌生。

提示3-8:C语言中的字符型用关键字char表示,它实际存储的是字符的ASCII码。字符常量可以用单引号法表示。在语法上可以把字符当作int型使用。

另一个新内容是“scanf("%s", s)”。和“scanf("%d", &n)”类似,它会读入一个不含空格、TAB和回车符的字符串,存入字符数组s。注意,不是“scanf("%s", &s)”,s前面没有“&”符号。

提示3-9:在“scanf("%s", s)”中,不要在s前面加上“&”符号。如果是字符串数组char s[maxn] [maxl],可以用“scanf("%s", s[i])”读取第i个字符串。注意,“scanf("%s", s)”遇到空白字符会停下来。

接下来有两个问题:判断和输出。根据我们的一贯作风,先考虑输出,因为它比较简单。每个竖式需要打印7行,但不一定要用7条printf语句,1条足矣。首先计算第一行乘积x=abc*e,然后是第二行y=abc*d,最后是总乘积z=abc*de,然后一次性打印出来:


printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z);

注意这里的%5d,它表示按照5位数打印,不足5位在前面补空格(还记得%03d吗?)。

完整程序如下:

程序3-4 竖式问题


#include<stdio.h>
#include<string.h>
int main()
{
  int count = 0;
  char s[20], buf[99];
  scanf("%s", s);
  for(int abc = 111; abc <= 999; abc++)
    for(int de = 11; de <= 99; de++)
    {
     int x = abc*(de%10), y = abc*(de/10), z = abc*de;
     sprintf(buf, "%d%d%d%d%d", abc, de, x, y, z);
     int ok = 1;
     for(int i = 0; i < strlen(buf); i++)
       if(strchr(s, buf[i]) == NULL) ok = 0;
     if(ok)
     {
       printf("<%d>\n", ++count);
       printf("%5d\nX%4d\n-----\n%5d\n%4d\n-----\n%5d\n\n", abc, de, x, y, z);
     }
    }
  printf("The number of solutions = %d\n", count);
  return 0;
}

还有两个函数是以前没有遇到的:sprintf和strchr。strchr的作用是在一个字符串中查找单个字符,而这个sprintf似曾相识:之前用过printf和fprintf。没错!这3个函数是“亲兄弟”,printf输出到屏幕,fprintf输出到文件,而sprintf输出到字符串。多数情况下,屏幕总是可以输出的,文件一般也能写(除非磁盘满或者硬件损坏),但字符串就不一定了:应该保证写入的字符串有足够的空间。

提示3-10:可以用sprintf把信息输出到字符串,用法和printf、fprintf类似。但应当保证字符串足够大,可以容纳输出信息。

多大才算足够大呢?答案是字符个数加1,因为C语言的字符串是以空字符“\0”结尾的。后面还会提到这个问题,但是基本原则仍然是以前说过的:如果算不清楚就把数组空间设置得大一点,空间够用的情况下浪费一点没关系。例如,此处声明的缓冲字符串buf的长度为99(可以保存长度为98的字符串),保存abc, de, x, y, z的所有数字绰绰有余。

函数strlen(s)的作用是获取字符串s的实际长度。什么叫实际长度呢?字符数组s的大小是20,但并不是所有空间都用上了。如果输入是“2357”,那么实际上s只保存了5个字符(不要忘记了还有一个结束标记“\0”),后面15个字符是不确定的(还记得吗?变量在赋值之前是不确定的)。strlen(s)返回的就是结束标记之前的字符个数。因此这个字符串中的各个字符依次是s[0], s[1],…, s[strlen(s)-1],而s[strlen(s)]正是结束标记“\0”。

提示3-11:C语言中的字符串是以“\0”结尾的字符数组,可以用strlen(s)返回字符串s中结束标记之前的字符个数。字符串中的各个字符是s[0], s[1],…,s[strlen(s)-1]。

提示3-12:由于字符串的本质是数组,它也不是“一等公民”,只能用strcpy(a, b), strcmp(a, b), strcat(a, b)来执行“赋值”、“比较”和“连接”操作,而不能用“=”、“==”、“<=”、“+”等运算符。上述函数都在string.h中声明。

此处再次看到了++count这样的用法,有必要对它进行进一步说明。猜猜看:count=0时,“printf("%d %d %d\n", count++, count++, count++)”会输出什么(然后做个实验)。怎么样,是不是和你想的不同呢?

另一个例子是“count = count++”。这里对count++的解释是:count++在表达式中的值是加1之前的值(即原来的值),但计算count++之后count会增加1。问题出现了:这个“稍后再加1”到底是何时进行的呢?如果是计算完赋值的右边(即count++)之后就立刻执行,最后count的值不会变(别忘了最后执行的是赋值);但如果是整个赋值完成之后才加1,最后count的值会比原来多1。如果在理解刚才这段话时感到吃力,最好的方法是避开它。

提示3-13:滥用“++”、“—”、“+=”等可以修改变量值的运算符很容易带来隐蔽的错误。建议每条语句最多只用一次这种运算符,并且所修改的变量在整条语句中只出现一次。

事实上,就算充分理解了这条规则,在实际编程时也可能临时忘记。好在可以利用编译器减少这种错误。用-Wall命令编译刚才的两个例子,编译器都会给出警告:对count的运算可能是没有定义的。