C语言逆向学习基础课 第9课 文件操作的核心陷阱

C语言实战高频深度错误解析

文章目录

  • C语言实战高频深度错误解析
    • [一、第9课 文件操作的核心陷阱](#一、第9课 文件操作的核心陷阱)
      • [1.1 课程目标](#1.1 课程目标)
      • [1.2 核心知识点讲解](#1.2 核心知识点讲解)
        • [1.2.1 文件操作的核心函数与基础流程](#1.2.1 文件操作的核心函数与基础流程)
        • [1.2.2 文件操作的高频陷阱(重点规避)](#1.2.2 文件操作的高频陷阱(重点规避))
      • [1.3 实战示例(综合错误排查)](#1.3 实战示例(综合错误排查))
      • [1.4 课后作业(实战巩固)](#1.4 课后作业(实战巩固))
      • [1.5 课程总结](#1.5 课程总结)
    • [二、上一课作业答案 函数原型与可变参数使用误区](#二、上一课作业答案 函数原型与可变参数使用误区)
      • [2.1 实战作业代码](#2.1 实战作业代码)
      • [2.2 代码功能说明](#2.2 代码功能说明)
      • [2.3 注意事项](#2.3 注意事项)

一、第9课 文件操作的核心陷阱

1.1 课程目标

  1. 掌握C语言文件操作的核心函数(fopen、fread、fwrite、fclose等)的使用规范,理解各函数返回值的意义;

  2. 识别并规避文件操作中的高频陷阱(返回值未校验、文件未关闭、错误信息未获取等);

  3. 能独立编写规范的文件读写代码,排查并修正文件操作相关的错误,提升代码健壮性。

1.2 核心知识点讲解

1.2.1 文件操作的核心函数与基础流程

C语言文件操作依赖<stdio.h>头文件,核心是通过"文件指针"操作文件,基础流程为:打开文件(fopen)→ 读写操作(fread/fwrite等)→ 关闭文件(fclose),每一步都存在高频陷阱,需重点关注。

  1. 核心函数说明(重点掌握)
  • fopen:打开文件,返回文件指针(FILE *),打开失败返回NULL,是文件操作的第一步,也是最易出错的环节。

格式:FILE *fopen(const char *filename, const char *mode); (mode为打开模式,如"r"读、"w"写、"a"追加)

  • fread/fwrite:文件读写函数,返回实际读写的字节数,读写失败返回0或小于预期值。

  • fclose:关闭文件,释放文件资源,返回0表示成功,非0表示失败,必须在文件操作结束后调用。

  • strerror:获取错误信息,传入错误码(errno),返回错误描述字符串,用于排查文件操作失败原因。

  1. 基础流程示例(正确用法)
c 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
    // 1. 打开文件(只读模式,打开失败返回NULL)
    FILE *fp = fopen("test.txt", "r");
    if (fp == NULL) {
        // 规避陷阱:获取错误信息,便于排查
        printf("文件打开失败:%s\n", strerror(errno));
        return 1; // 打开失败,直接退出程序
    }
    
    // 2. 读写操作(示例:读取文件内容)
    char buf[1024] = {0};
    // fread返回实际读取的字节数,判断是否读取成功
    size_t read_len = fread(buf, 1, sizeof(buf)-1, fp);
    if (read_len == 0) {
        // 区分"读取到文件末尾"和"读取失败"
        if (feof(fp)) {
            printf("已读取到文件末尾\n");
        } else {
            printf("文件读取失败:%s\n", strerror(errno));
        }
    } else {
        printf("读取到的内容:%s\n", buf);
    }
    
    // 3. 关闭文件(必写,释放资源)
    if (fclose(fp) != 0) {
        printf("文件关闭失败:%s\n", strerror(errno));
        return 1;
    }
    fp = NULL; // 规避野指针
    
    return 0;
}
1.2.2 文件操作的高频陷阱(重点规避)

文件操作的陷阱主要集中在"打开失败未处理""读写未校验""文件未关闭"三大类,以下是具体陷阱、错误示例及规避方法。

陷阱1:fopen返回值未校验(最高频)

  • 错误表现:直接使用fopen返回的文件指针,未判断是否为NULL,若文件不存在、权限不足等导致打开失败,后续操作会触发空指针解引用,程序崩溃。

  • 错误示例+正确修正:

c 复制代码
#include <stdio.h>

int main() {
    // 错误:未校验fopen返回值,若test.txt不存在,fp为NULL
    FILE *fp = fopen("test.txt", "r");
    char buf[100];
    fread(buf, 1, 100, fp); // 空指针解引用,程序崩溃
    
    // 正确修正:校验返回值,获取错误信息
    FILE *fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("文件打开失败:%s\n", strerror(errno));
        return 1;
    }
    char buf[100] = {0};
    fread(buf, 1, 100, fp);
    
    fclose(fp);
    fp = NULL;
    return 0;
}

陷阱2:文件打开模式使用错误

  • 错误表现:混淆打开模式(如用"r"模式写文件、用"w"模式读文件),导致文件操作失败;用"w"模式打开已存在文件,会直接清空文件内容(易误操作)。

  • 常见模式对比(重点记忆):

  • "r":只读模式,文件必须存在,否则打开失败;

  • "w":只写模式,文件不存在则创建,存在则清空内容;

  • "a":追加模式,文件不存在则创建,写入内容追加到文件末尾;

  • "r+":读写模式,文件必须存在,可读写;

  • "w+":读写模式,文件不存在则创建,存在则清空。

  • 错误示例:用"r"模式写入文件,导致写入失败。

c 复制代码
#include <stdio.h>

int main() {
    // 错误:"r"模式仅支持读,无法写入
    FILE *fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("打开失败:%s\n", strerror(errno));
        return 1;
    }
    fputs("hello world", fp); // 写入失败,无报错但无效果
    fclose(fp);
    return 0;
}

陷阱3:文件操作后未关闭文件(资源泄漏)

  • 错误表现:文件读写完成后,未调用fclose关闭文件,会导致文件资源泄漏(系统可打开的文件句柄数量有限),长期运行会导致程序异常,甚至无法打开新文件。

  • 易错场景:程序异常退出(如return、break)时,跳过fclose操作。

  • 规避方法:确保每个fopen对应一个fclose,异常分支也需关闭文件(可借助goto统一清理)。

c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("打开失败:%s\n", strerror(errno));
        return 1;
    }
    
    // 模拟异常场景:写入失败后直接退出,未关闭文件
    if (fputs("hello", fp) == EOF) {
        printf("写入失败\n");
        // 错误:未关闭文件,导致资源泄漏
        return 1;
    }
    
    // 正确修正:异常分支也需关闭文件
    if (fputs("hello", fp) == EOF) {
        printf("写入失败\n");
        fclose(fp);
        fp = NULL;
        return 1;
    }
    
    fclose(fp);
    fp = NULL;
    return 0;
}

陷阱4:读写操作返回值未校验

  • 错误表现:认为fread/fwrite一定会成功,未校验返回值,若读写失败(如磁盘满、文件损坏),会导致数据丢失或程序逻辑错误。

  • 规避方法:校验读写返回值,区分"正常结束"和"异常失败"(借助feof判断是否到达文件末尾)。

陷阱5:忽略错误信息获取(难以排查问题)

  • 错误表现:文件操作失败后,仅提示"操作失败",未获取具体错误信息(如权限不足、文件不存在),导致无法快速定位问题。

  • 规避方法:使用strerror(errno)获取错误描述,errno是系统全局变量,存储最近一次系统调用的错误码。

1.3 实战示例(综合错误排查)

以下代码包含4个高频错误(fopen未校验、模式错误、未关闭文件、读写未校验),请排查并修正:

c 复制代码
#include <stdio.h>

int main() {
    // 错误1:fopen未校验返回值
    FILE *fp = fopen("test.txt", "r");
    // 错误2:打开模式错误("r"模式无法写入)
    fputs("hello world", fp);
    
    // 错误3:读写返回值未校验
    char buf[100];
    fread(buf, 1, 100, fp);
    printf("读取内容:%s\n", buf);
    
    // 错误4:未关闭文件,资源泄漏
    return 0;
}

修正后代码:

c 复制代码
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
    // 修正1:校验fopen返回值,修正打开模式为"w"(可写入)
    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("文件打开失败:%s\n", strerror(errno));
        return 1;
    }
    
    // 修正2:校验写入返回值
    if (fputs("hello world", fp) == EOF) {
        printf("文件写入失败:%s\n", strerror(errno));
        fclose(fp);
        fp = NULL;
        return 1;
    }
    
    // 重新打开文件(只读模式),读取内容
    fclose(fp);
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("文件打开失败:%s\n", strerror(errno));
        return 1;
    }
    
    // 修正3:校验读取返回值,区分文件末尾和失败
    char buf[100] = {0};
    size_t read_len = fread(buf, 1, sizeof(buf)-1, fp);
    if (read_len == 0) {
        if (feof(fp)) {
            printf("已读取到文件末尾\n");
        } else {
            printf("文件读取失败:%s\n", strerror(errno));
        }
    } else {
        printf("读取内容:%s\n", buf);
    }
    
    // 修正4:关闭文件,释放资源
    if (fclose(fp) != 0) {
        printf("文件关闭失败:%s\n", strerror(errno));
        return 1;
    }
    fp = NULL;
    
    return 0;
}

1.4 课后作业(实战巩固)

  1. 编写一个程序,实现"读取一个文本文件的内容,将内容复制到另一个新文件中",要求:校验fopen、fread、fwrite、fclose的返回值,获取错误信息,避免资源泄漏。

  2. 编写一个程序,使用"追加模式"向文件中写入3行文本(如"第一行""第二行""第三行"),要求:若文件不存在则创建,写入后读取文件内容并输出,验证写入是否成功。

  3. 排查以下代码的错误(至少3个),并修正:

c 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "r");
    char buf[50];
    fread(buf, 1, 50, fp);
    printf("内容:%s\n", buf);
    
    fputs("追加内容", fp);
    
    return 0;
}

1.5 课程总结

  1. 文件操作核心流程:打开(fopen)→ 读写(fread/fwrite)→ 关闭(fclose),每一步都需校验返回值,避免空指针、资源泄漏等问题。

  2. 高频陷阱规避:fopen必校验、模式不混淆、读写必校验、文件必关闭、错误必排查(strerror(errno))。

  3. 核心原则:文件操作的核心是"安全",既要避免程序崩溃,也要防止数据丢失和资源泄漏,养成"校验返回值、及时关文件"的习惯。

二、上一课作业答案 函数原型与可变参数使用误区

2.1 实战作业代码

c 复制代码
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// 作业1:函数原型与定义完全一致,计算两个字符串长度之和
int strLenSum(char *str1, char *str2); // 函数原型声明

// 函数定义(与原型完全一致)
int strLenSum(char *str1, char *str2) {
    // 规避陷阱:校验指针非空,避免空指针解引用
    if (str1 == NULL || str2 == NULL) {
        printf("错误:字符串指针为空!\n");
        return -1; // 返回错误标识
    }
    return strlen(str1) + strlen(str2);
}

// 作业2:可变参数函数,求n个float类型数据的平均值
float floatAvg(int n, ...); // 函数原型声明

float floatAvg(int n, ...) {
    // 规避陷阱:n为0时直接返回0,避免除零错误
    if (n <= 0) {
        printf("错误:参数个数不能小于等于0!\n");
        return 0.0;
    }
    
    va_list args;
    va_start(args, n); // 初始化可变参数列表
    float total = 0.0;
    
    // 规避陷阱:读取类型与实际参数一致(float)
    for (int i = 0; i < n; i++) {
        total += va_arg(args, float);
    }
    
    va_end(args); // 规避陷阱:及时清理可变参数列表
    return total / n;
}

int main() {
    // 测试作业1:计算两个字符串长度之和
    char str1[] = "hello";
    char str2[] = "world";
    int lenSum = strLenSum(str1, str2);
    if (lenSum != -1) {
        printf("两个字符串长度之和:%d\n", lenSum);
    }
    
    // 测试作业2:求3个float数据的平均值
    float avg = floatAvg(3, 1.5f, 2.5f, 3.5f);
    printf("3个float数据的平均值:%.2f\n", avg);
    
    // 测试错误场景:可变参数个数为0、字符串指针为空
    floatAvg(0);
    strLenSum(NULL, str2);
    
    return 0;
}

2.2 代码功能说明

本代码实现两个核心功能,规避函数原型与可变参数使用高频陷阱。功能1:声明与定义完全一致的函数,计算两个字符串长度之和,校验指针非空避免空指针解引用;功能2:可变参数函数,求n个float数据的平均值,遵循"初始化→读取→清理"流程,校验参数个数避免除零错误,确保读取类型与实际一致。代码包含正常测试与错误场景测试,逻辑规范,有效规避原型不一致、可变参数使用不当等陷阱。

2.3 注意事项

  1. 函数原型:必须保证原型声明与函数定义的返回值类型、参数个数、类型、顺序完全一致,避免隐式声明或参数不匹配导致的错误。

  2. 可变参数函数:必须包含至少一个固定参数,用于va_start绑定;va_arg读取参数的类型需与实际参数一致,避免数据截断;必须调用va_end清理,避免内存泄漏。

  3. 指针校验:函数参数为指针时,需先校验是否为NULL,避免空指针解引用导致程序崩溃,尤其字符串、文件指针等高频场景。

  4. 错误处理:函数返回值需包含错误标识(如-1、0.0),调用函数后需校验返回值,及时处理异常场景,提升代码健壮性。

  5. 代码规范:函数原型声明放在主函数前,注释清晰,变量初始化,避免因代码不规范隐藏错误,便于后续维护和排查。

上一课链接: C语言逆向学习基础课 第8课 函数原型与可变参数使用误区

第一课课程: C语言逆向学习基础课 第1课:数组越界与指针操作基础陷阱