C语言存储系统命令输出到变长字符串数组时段错误排查
问题根因
不管是第一版还是修改后的代码,都存在几个致命的指针和内存逻辑错误,逐个说明:
第一版代码的核心bug
- 指针步进逻辑完全错误:
out是const char**类型,本质是存储char指针的数组,每个元素的大小是一个指针长度(64位系统下为8字节)。你写head += strlen(buffer),相当于每次步进几十上百个数组元素,直接越界访问非法内存。另外把指针强转成size_t做边界判断本身就是未定义行为,同类型指针直接比较就行,没必要转整数。 strcat用法完全错误:首先fgets读取内容时会自动在末尾补'\0'终止符,根本不需要手动追加。其次strcat要求第二个参数是合法的、以'\0'结尾的字符串,你传的&terminator是单个字符的地址,这个地址后续的内存值是随机的,strcat会一直向后遍历找0值,直接触发越界读。- 栈内存指针逃逸:你把
*head指向了函数内的局部栈数组buffer,这个数组的生命周期只在exec函数内部,函数返回后对应的栈空间会被系统回收复用,外层访问这些指针全是野指针。
修改后第二版代码的遗留bug
你修正了索引步进的问题,但两个致命问题没解决:
- 还是把所有
out[i]都指向了同一个局部栈数组buffer:一方面函数返回后这些指针全是野指针,另一方面每次fgets读新内容都会覆盖buffer的内容,就算不考虑生命周期问题,最后所有指针指向的内容全是最后读取的那一行。 - 打印循环硬遍历128次数组元素:你根本没记录实际读取到了多少行,那些没被赋值的
buffer[i]都是未初始化的野指针,printf访问这些地址直接触发段错误。
修复方案
要独立存储每一行内容,必须给每一行单独分配堆内存(不能用函数内的局部栈数组),同时函数要返回实际读取到的行数,外层遍历的时候只访问有效元素,不要碰未初始化的指针。
可直接运行的修复后代码:
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_LINE_LENGTH 512 /* * 返回值说明: * 正整数:实际读取到的输出行数 * -1:内存分配失败 * 1:popen调用失败 * 2:pclose调用失败 */ int exec(const char* in, char** out, const size_t n) { char buffer[MAX_LINE_LENGTH]; FILE *file; size_t line_cnt = 0; if ((file = popen(in, "r")) == NULL) { return 1; } while (line_cnt < n && fgets(buffer, MAX_LINE_LENGTH, file) != NULL) { // 给当前行单独分配堆内存,长度是字符串长度+1(预留终止符位置) out[line_cnt] = malloc(strlen(buffer) + 1); if (out[line_cnt] == NULL) { pclose(file); // 出错时释放之前已分配的内存,避免泄漏 for (size_t i = 0; i < line_cnt; i++) { free(out[i]); } return -1; } strcpy(out[line_cnt], buffer); // 将行内容拷贝到独立的堆内存 line_cnt++; } if (pclose(file) == -1) { // 出错时统一清理内存 for (size_t i = 0; i < line_cnt; i++) { free(out[i]); } return 2; } return (int)line_cnt; } #define N 128 int main(void) { char* buffer[N] = {0}; // 指针数组全初始化为空,避免野指针 const char cmd[] = "ls -l"; // 替换成你需要执行的系统命令 const int code = exec(cmd, buffer, N); // 错误分支处理 if (code < 0) { perror("malloc failed"); exit(EXIT_FAILURE); } if (code == 1) { perror("popen failed"); exit(code); } if (code == 2) { perror("pclose failed"); exit(code); } // 只遍历实际读取到的有效行,不访问未初始化的指针 for (int i = 0; i < code; i++) { printf("%s", buffer[i]); free(buffer[i]); // 用完释放堆内存,避免泄漏 } exit(EXIT_SUCCESS); }
关键注意事项
- 栈上局部变量的生命周期仅限所属函数,函数返回后对应内存会被回收,绝对不能把局部变量的地址传出函数给外部使用。
- 指针步进的步长是由指针指向的类型决定的:
char**类型指针+1,实际移动的字节数是sizeof(char*),不是1字节,更和读取到的字符串长度无关。 fgets会自动在读取内容末尾补'\0'终止符,不需要手动追加;如果读入的行包含换行符'\n',也会被一起存入缓冲区。- 遍历指针数组时必须明确有效元素的个数,禁止访问未初始化的野指针。
malloc分配的堆内存,用完必须对应调用free释放,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Oh Fiveight
相关产品推荐
相关产品推荐

