题目
编写一个程序,删除每个输入行末尾的空格及制表符,并删除完全是空格的行。
自我解答:
编程思想:
首先把这里的“完全是空格的行”理解为 包含一个换行符和0~n个空格或制表符的行。编程思路是对于输入的每一行纪录下最后一个非空格或制表符到行末换行符之间的长度,在接下来拷贝这一行时不对这些字符进行拷贝。下面程序中用getline函数获取文本中的一行,用copy函数把获取的一行去除行尾空格及制表符,MAXLINE用于标记获取行的最大长度(由于有换行符合终止符,实际获取输入行的最大长度是998,见下面解析)。
#include#define MAXLINE 1000 int getline(char line[]) { int c; int len = 0; while(len < MAXLINE - 1 && (c = getchar()) != EOF && c != 'n') { line[len] = c; len++; } if(c == 'n') { line[len] = c; len++; } line[len] = ' '; return len; } void copyLine(char to[], char from[]) { int i = 0; int lenSpaceTab = 0; while((to[i] = from[i]) != ' ') { if(from[i] == ' ' || from[i] == 't') { lenSpaceTab++; } else if(from[i] != 'n') { lenSpaceTab = 0; } i++; } if(lenSpaceTab > 0) { to[i - lenSpaceTab - 1] = 'n'; to[i - lenSpaceTab] = ' '; } if(to[0] == 'n') to[0] = ' '; } int main() { int lenLine; char line[MAXLINE] = {' '}; char newline[MAXLINE] = {' '}; while((lenLine = getline(line)) > 0) { copyLine(newline, line); printf("%s", newline); } return 0; }
解析:getline函数用于获取输入中的一行,当输入行的字符总长度小于MAXLINE时,getline能完全获取这一行的所有字符。考虑临界情况,例如当MAXLINE设置为1000时,当输入行的字符个数为999时,这些字符存储在line[0] ~ line[998],line[999]设置为' '。当输入行字符长度大于等于MAXLINE时,getline只能获取前999个字符。getline的返回值len为获取行的字符长度。
copy函数中会首先把from[]完全拷贝到to[]中,在copy的过程中会对行尾的空格和Tab进行计数,得到计数之后,会修改to[]数组,即把'n'和' '字符向前移动,最后判断to[]的第一个字符是否为换行符,若为换行符说明输入行为完全为空格的行(这里完全为空格的行包含空格和tab的任意数量的组合)。
针对上面程序考虑一些特殊情况:
1. 输入长度大于等于MAXLINE的情况,getline依然会先获得输入行的前MAXLINE-1个字符,然后打印出经过copyLine的处理的行,此时结果将不再准确,因为此时输入行的最后仍然可能出现多个空格和Tab的,而这些空格和Tab并不是希望被处理的。接下来的循环依然会取这一行中剩余的第MAXLINE-1个字符并处理,直至取到此行结束或EOF。针对这种情况稍作改进如下:
getline中改动
line[len] = ' ';
if(len == MAXLINE - 1)
len = MAXLINE;
return len;
main中改动:
while((lenLine = getline(line)) > 0)
{
if(lenLine == MAXLINE)
printf("%s", line);
else
{
copyLine(newline, line);
printf("%s", newline);
}
}
但这种情况依然会对超出limit行的最后lim-1个字符进行处理,且处理时当成新行处理(此时若为空格行,则不会换行)。
2. 输入为ab_ _时,注意ab后时两个空格,接着就到了文件尾。此时输出为
a
可见此时并不能输出正确的结果,原因是当调用getline后,line[] = {a,b,_,_},调用copyLine后,空格数为2,在移动换行时有to[4 - 2 - 1] = ‘n’,所以此时把to[1]的值换成了回车符,导致错误。本质原因是输入行没有遇到n,而是直接到了EOF。所以为了适配这种情况,应该在移动换行之前先判断to[]的倒数第二个字符是不是n(用于判定输入最后是不是没有换行直接到了EOF的情况)
修正如下:
if(lenSpaceTab > 0)
{
if(to[i - 1] == 'n')
to[i - lenSpaceTab - 1] = 'n';
to[i - lenSpaceTab] = ' ';
}
如果没有上面修改带来的另一个问题时,可能会出现数组越界的情况,例如,当输入有_ _ _EOF时,即一串空格后结束的情况,这是空格数和to[]的长度相同,这样导致 i - lenSpaceTab - 1 = -1,这也许会导致很严重的问题。
3. 程序中使用了copyLine函数用于获得行的重新拷贝动作,这一步也可直接在getline函数中操作line[]数组,这样可以提高程序运行效率。优化如下
#include#define MAXLINE 1000 int getline(char line[]) { int c, lenSpaceTab; int len = 0; lenSpaceTab = 0; while(len < MAXLINE - 1 && (c = getchar()) != EOF && c != 'n') { line[len] = c; if(c == ' ' || c == 't') lenSpaceTab++; else lenSpaceTab = 0; len++; } if(c == 'n') { line[len] = c; len++; } line[len] = ' '; if(len == MAXLINE - 1) return MAXLINE; if(lenSpaceTab > 0) { if(line[len - 1] == 'n') line[len - lenSpaceTab - 1] = 'n'; line[len - lenSpaceTab] = ' '; } if(line[0] == 'n') line[0] = ' '; return len; } int main() { int lenLine; char line[MAXLINE] = {' '}; int endOfLine; while((lenLine = getline(line)) > 0) { if(lenLine == MAXLINE) { endOfLine = 1; } else { if(endOfLine == 1) if(line[0] == ' ') line[0] = 'n'; endOfLine = 0; } printf("%s", line); } return 0; }
上述程序考虑了当行的长度超过limit的情况。即getline函数中的while循环是由于len < MAXLINE - 1而终止时,即判定此时行的长度已超限,此种情况下返回MAXLINE,并不对line做任何处理。在main函数中调用getline时会判断行的长度是否已超限,若超限则不作任何处理打印,另外定义了一个bool变量endOfLine用于标识,在行的长度超过MAXLINE时,什么时候退出该行。由于退出该行时调用getline返回长度一定小于MAXLINE,所以在getline中依然会有移动n的操作,但是获得的行有可能是全部空格行,而这时要求打印换行(因为这只是超限行的一部分,不能纯粹当成空格航看待),所以在main中对这种情况进行了判断处理。
即使这样对于超限行的处理仍然不是完备的,即当超限行的最后是很长的空格时(超过了MAXLINE),此时并不能完全去除行尾的这些空格,只能去除最后MAXLINE-1个空格。
参考答案:
#include#define MAXLINE 1000 int getline(char line[], int maxline); int remove(char s[]); int main() { char line[MAXLINE]; while(getline(line, MAXLINE) > 0) if(remove(line) > 0) printf("%s", line); return 0; } int remove(char s[]) { int i; i = 0; while(s[i] != 'n') ++i; --i; while(i >= 0 && (s[i] == ' ' || s[i] == 't')) --i; if(i >= 0) { ++i; s[i] = 'n'; ++i; s[i] = ' '; } return i; }
remove函数负责删除字符串line末尾的空格和制表符并返回它的新长度。如果这个长度大于零,就说明line中有不是空格和制表符的其他字符,程序就会把这一行打印出来;否则,就说明line完全是由空格和制表符构成的,程序就将忽略这个输入行。这就保证了完全是空格的行不会打印出来。
remove函数首先找到换行符,然后倒退一个位置。随后,这个函数将从后向前检查空格或制表符,直到它找到一个不是空格或制表符的字符或者没有字符可以让它倒退(即i<0)为止。如果i>=0,则说明至少还有一个字符。此后,函数remove将换行符和字符串结束符写回输入行,再返回变量i.
函数getline与练习1-16中的同名函数相同。
补充:
1. 1-16中的getline函数为
int getline(char s[], int lim)
{
int c, i ,j;
j = 0;
for(i = 0; (c = getchar()) != EOF && c != 'n'; ++i)
{
if(i < lim - 2)
{
s[j] = c;
j++;
}
}
if(c == 'n')
{
s[j] = c;
++j;
++i;
}
s[j] = ' ';
return i;
}
2. 纵观参考答案整个代码,当行的长度大于等于MAXLINE-1时,getline取到的最大长度是行最前面的MAXLINE-2个字符,其后的字符被忽略。其只能显示这行的前面一部分,并且这部分的末尾仍然会有去除空格和Tab的操作。这并不是所期望的
当把MAXLINE改成5时,输入是 sdjhkkjn (n代表换行符),此时输出为:sdj
当输入为sd jhkkjn时,输出为sd(程序会去除sd后面的这个空格)
当输入为sdjhkkj时(最后没有换行符,直接到输入结束,此时MAXLINE为多少不影响),getline得到的数组中并没有n,此时再调用remove函数,并不能在数组长度内找到n,此时下面语句仍会循环,直至找到n,那此时返回的i是个不确定的值,会以这个值进行后续的处理,这会导致数组越界并且修改了数组以外的值,这也许会导致严重的后果。
while(s[i] != 'n')
++i;
下面是这种情况下多次运行找到n的位置
i is 130 i is 114 i is 98 i is 82



