K&R练习5-13:tail程序处理大文件时异常问题排查
问题根源分析
你的alloc函数试图实现内存复用逻辑,但存在多处逻辑错误,导致大文件处理时内存管理混乱:
allocn的无意义调整:你试图通过allocn -= no避免溢出,但allocn是累计分配次数,这个操作会破坏对已分配行数的跟踪,导致后续leftalloc和rightalloc的计算完全错误。- 内存边界计算错误:通过
while (*rightalloc++ != '\0')寻找旧行末尾不可靠——旧行内存被新数据覆盖后,字符串结束符'\0'可能已被破坏,无法正确定位内存边界。 - 循环缓冲区逻辑混乱:
leftalloc和rightalloc的初始化与更新逻辑不清晰,无法正确标记可复用内存区域,导致小n值(如-1到-4)时快速误判内存不足,n=5、6时出现内存覆盖导致行内容丢失。 - 换行符处理错误:
readlines中line[len-1] = '\0'会强制去掉换行符,若输入最后一行无换行符,会把该行最后一个字符替换为'\0',导致内容损坏。
修复方案
我们简化内存管理逻辑:用环形队列存储最后n行的指针和内存长度,新行到来时,若队列已满则复用最旧行的内存(空间足够的话),否则从缓冲区末尾分配,直到静态缓冲区耗尽。
修复后的完整代码:
#include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> int isint(char s[]); int toint(char s[]); int readlines(char *lineptr[], int no); void writelines(char *lineptr[], int no); #define MAXLINE 100 /* 最大允许打印的行数 */ int main(int argc, char *argv[]) { int n = 10; char *lineptr[MAXLINE]; while (--argc > 0) { if ((*++argv)[0] == '-' && isint(*argv)) { int val = toint(*argv); if (val > 0 && val <= MAXLINE) { n = val; break; } } printf("invalid argument %s\n", *argv); } printf("Printing %d line%s\n", n, n > 1 ? "s" : ""); if (readlines(lineptr, n) >= 0) { writelines(lineptr, n); return 0; } else { printf("input too big to handle\n"); return 1; } } int isint(char s[]) { if (*s == '-') s++; if (!isdigit(*s)) return 0; while (*s != '\0') { if (!isdigit(*s++)) return 0; } return 1; } int toint(char s[]) { return atoi(s + (*s == '-' ? 1 : 0)); } #define MAXLEN 1000 /* 单行长上限 */ #define ALLOCSIZE MAXLINE * MAXLEN /* 总内存缓冲区大小 */ static char allocbuf[ALLOCSIZE]; static char *allocp = allocbuf; /* 下一个可用内存的起始位置 */ /* 记录环形队列中每行的内存起始地址和长度 */ typedef struct { char *ptr; int len; } LineInfo; static LineInfo line_info[MAXLINE]; static int queue_head = 0; /* 环形队列头(最旧行的索引) */ static int queue_count = 0; /* 当前队列中的行数 */ char *alloc(int n) { /* 先尝试复用环形队列中最旧的行的内存 */ if (queue_count >= MAXLINE) { LineInfo oldest = line_info[queue_head]; if (oldest.len >= n) { /* 旧行内存足够,直接复用 */ return oldest.ptr; } } /* 没有可复用的内存,从缓冲区末尾分配 */ if (allocbuf + ALLOCSIZE - allocp >= n) { char *p = allocp; allocp += n; return p; } return NULL; } void update_queue(char *p, int len) { if (queue_count < MAXLINE) { /* 队列未满,直接添加新行信息 */ line_info[queue_count].ptr = p; line_info[queue_count].len = len; queue_count++; } else { /* 队列已满,更新最旧行的信息 */ line_info[queue_head].ptr = p; line_info[queue_head].len = len; queue_head = (queue_head + 1) % MAXLINE; } } int get_line(char *line, int lim) { int i, c; i = 0; while (--lim > 0 && (c = getchar()) != EOF && c != '\n') line[i++] = c; if (c == '\n') { line[i++] = c; } line[i] = '\0'; return i; } void push(char *lineptr[], char *p, int no) { int i; for (i = 0; i < no - 1; i++) { lineptr[i] = lineptr[i + 1]; } lineptr[no - 1] = p; } int readlines(char *lineptr[], int no) { int len, i; char *p, line[MAXLEN]; for (i = 0; i < no; i++) { lineptr[i] = NULL; } while ((len = get_line(line, MAXLEN)) > 0) { /* 分配内存时要包含'\0'的空间 */ int alloc_len = len + 1; if ((p = alloc(alloc_len)) == NULL) { return -1; } else { /* 保留换行符,不需要修改line[len-1] */ strcpy(p, line); push(lineptr, p, no); update_queue(p, alloc_len); } } return 1; } void writelines(char *lineptr[], int no) { int i; for (i = 0; i < no; i++) { if (lineptr[i] != NULL) { printf("%s", lineptr[i]); } } }
修复要点说明
- 简化内存分配逻辑:去掉原
alloc函数中复杂的leftalloc/rightalloc和allocn调整,改用LineInfo结构体跟踪每行的内存地址和长度,优先复用最旧行的内存(空间足够时)。 - 修正换行符处理:不再强制修改
line[len-1]为'\0',直接保留读取到的换行符,避免损坏无换行符的最后一行内容。 - 环形队列管理:用
queue_head和queue_count维护环形队列,准确跟踪当前存储的n行,确保内存复用的正确性。 - 参数校验优化:原
toint函数会跳过开头的'-'和'0',导致-011被解析为11,修复后直接用atoi处理完整字符串,同时校验参数范围。
修改后,无论小文件还是大文件,不同n值下都能正确输出最后n行,不会出现内存不足误判或行内容丢失的问题。
内容的提问来源于stack exchange,提问作者hansoko
相关产品推荐
相关产品推荐

