K&R 的 readlines 删除无换行符行的末尾字符,这是 bug 吗?
是的,如果要求保留未以 \n 结尾的最后一行,题目中的实现就存在丢失数据的 bug。readlines 无条件覆盖读取结果的最后一个字符,依赖每次读取都以换行符结尾,而 mygetline 并不保证这一点。
仅凭代码,无法判断作者是否有意要求输入满足这一前提。
为什么会丢失最后一个字符?
题目中的 mygetline 返回实际存入缓冲区的字符数,不包含末尾的 \0。如果读到了 \n,换行符也计入长度。
输入为 abc\n 时:
line[0] = 'a'
line[1] = 'b'
line[2] = 'c'
line[3] = '\n'
line[4] = '\0'
len = 4
此时执行:
line[len - 1] = '\0';
会把换行符替换为字符串终止符,得到 abc,符合预期。
如果输入只有 abc,随后遇到 EOF,结果为:
line[0] = 'a'
line[1] = 'b'
line[2] = 'c'
line[3] = '\0'
len = 3
同一条语句会覆盖 line[2] 中的 'c',最终只保存 ab。EOF 是读取函数返回的状态,不会作为字符存入字符串,也不会自动补上换行符。
如何修复?
先检查最后一个字符是否为 \n,确认存在换行符后再删除。同时,还要调整内存分配大小,为最终字符串的 \0 留出空间。
沿用题目中的 alloc 和 mystrcpy,可以这样修改:
#include <stddef.h>
#include "mylib.h"
#define MAXLEN 1000
int readlines(char *lineptr[], int maxlines) {
int len, nlines;
char *p, line[MAXLEN];
nlines = 0;
while ((len = mygetline(line, MAXLEN)) > 0) {
if (line[len - 1] == '\n') {
line[--len] = '\0';
}
if (nlines >= maxlines ||
(p = alloc(len + 1)) == NULL) {
return -1;
}
mystrcpy(p, line);
lineptr[nlines++] = p;
}
return nlines;
}
这里假定 alloc(n) 分配可容纳 n 个字符的空间,mystrcpy 会复制末尾的 \0。
修改后,len 表示删除换行符(如果有)后的字符串长度,因此统一使用 alloc(len + 1):
- 读取到
abc\n:原长度为 4,删除换行符后长度为 3,分配 4 个字符的空间。 - 读取到
abc后遇到 EOF:长度仍为 3,同样分配 4 个字符的空间。 - 读取到空行
\n:删除换行符后长度为 0,分配 1 个字符的空间,用于保存\0。
如果只给删除语句加条件,却保留原来的 alloc(len),无换行符时就会少分配一个字符的空间,导致复制字符串终止符时越界。
缓冲区读满时也会遇到同样的问题
没有换行符不一定意味着遇到了 EOF。题目中的 mygetline 在缓冲区空间不足时也会停止读取。
当 MAXLEN 为 1000 时,它最多保存 999 个输入字符,再补上 \0。如果在此之前尚未读到换行符,原来的 readlines 也会错误地删除本次读取的最后一个字符。
上面的修改可以避免字符丢失,但仍会把过长的一行拆成多条记录。如果需要让每条记录对应完整的一行,还需要拼接后续读取的片段,或者改用能够动态扩容的读取实现。
要求输入的最后一行以换行符结束,只能避开部分触发条件。修复时应检查实际读取结果,并按最终字符串长度分配空间。
备注:内容仅供参考。
