【C语言】(23)文件操作

48 阅读17分钟

基础文件操作

文件操作是通过一系列的标准输入输出函数完成的,这些函数定义在stdio.h头文件中。文件操作通常包括打开文件、读写文件、定位文件内的数据、以及关闭文件等操作。

1. 打开文件 - fopen()

FILE *fopen(const char *filename, const char *mode);
  • filename:要操作的文件名。
  • mode:打开文件的模式,常用的模式包括:
    • "r":只读模式,文件必须存在。
    • "w":写模式,如果文件存在则覆盖,不存在则创建。
    • "a":追加模式,写入数据会被追加到文件末尾,文件不存在则创建。
    • "r+":读写模式,文件必须存在。
    • "w+":读写模式,文件存在则覆盖,不存在则创建。
    • "a+":读写模式,写入数据会被追加到文件末尾,文件不存在则创建。
  • 返回值:成功时返回FILE指针,失败时返回NULL

2. 关闭文件 - fclose()

int fclose(FILE *stream);
  • stream:由fopen()返回的文件指针。
  • 返回值:成功时返回0,失败时返回EOF。

3. 写入文件

格式化输出 - fprintf()

int fprintf(FILE *stream, const char *format, ...);

用于向文件写入格式化数据。它是 printf() 函数的文件操作版本,允许将格式化的输出写入指定的文件流中,而不是标准输出。

函数原型

int fprintf(FILE *stream, const char *format, ...);
  • stream:指向 FILE 对象的指针,该对象标识了将要进行写操作的流。
  • format:一个字符串,包含文本将被写入文件流的格式。它可以包含嵌入的格式标签,这些标签会被后续的附加参数替换,其行为类似于 printf()
  • ...:跟随 format 字符串的是可变数量的附加参数,每个参数对应一个格式标签。

返回值

  • 成功时,fprintf() 返回写入的字符数(不包括结尾的 null 字符)。
  • 出错时,返回一个负数。

格式标签

格式标签定义了每个附加参数的类型和如何呈现它们。常见的格式标签包括:

  • %d:以十进制形式输出整数。
  • %f:以十进制形式输出浮点数。
  • %s:输出字符串。
  • %c:输出单个字符。
  • 更多格式标签请参考标准 C 语言文档。

示例

#include <stdio.h>

int main() {
    FILE *fp = fopen("output.txt", "w");
    if (fp == NULL) {
        perror("Failed to open file");
        return 1;
    }

    int age = 30;
    float salary = 12345.67;
    char name[] = "John Doe";

    // 写入格式化的数据到文件
    fprintf(fp, "Name: %s\n", name);
    fprintf(fp, "Age: %d\n", age);
    fprintf(fp, "Salary: %.2f\n", salary);

    fclose(fp);

    return 0;
}

字符输出 - fputc()

int fputc(int char, FILE *stream);
  • 将一个字符写入到指定的文件中。

字符串输出 - fputs()

int fputs(const char *str, FILE *stream);
  • str:指向一个以空字符终止的字符串的指针,该字符串将被写入stream指定的文件流中。
  • stream:指向FILE对象的指针,该FILE对象标识了fputs函数将写入数据的流。

fputs函数会将str指向的字符串(不包括空字符)写入到stream指定的输出流中。成功时,fputs函数返回一个非负值;如果发生错误,则返回EOF

宽字符输出-fputws()

类似于 fputs 函数,但专门用于处理宽字符(通常是 Unicode 字符)。这使得 fputws 成为向文件中写入包含多字节字符集(如 UTF-16 或 UTF-32)字符串的理想选择。

int fputws(const wchar_t *ws, FILE *stream);
  • ws:指向宽字符数组的指针,该数组包含了要写入文件的宽字符字符串。
  • stream:指向 FILE 对象的指针,该 FILE 对象标识了 fputws 函数将向其中写入数据的流。

注意事项

  • 使用 fputws 之前,确保已通过调用 setlocale 函数设置了正确的区域设置,以便正确处理宽字符和多字节字符。
  • fputws 不会自动在写入的字符串后添加换行符。如果需要换行,应该在字符串中显式包含 \n(对于宽字符字符串是 L'\n')。

返回值

  • 成功:返回一个非负值。
  • 失败:返回 EOF,通常定义为 -1

示例:使用 fputws 向文件写入数据

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main() {
    // 设置当前 C 本地环境为用户的本地环境
    setlocale(LC_ALL, "");
    
    FILE *file = fopen("example.txt", "w");
    if (file == NULL) {
        wprintf(L"Failed to open file for writing\n");
        return 1;
    }

    // 使用 fputws 向文件中写入宽字符字符串
    const wchar_t *message = L"Hello, world! 你好,世界!\n";
    if (fputws(message, file) == EOF) {
        wprintf(L"Failed to write to file\n");
        // 关闭文件并退出程序
        fclose(file);
        return 1;
    }

    wprintf(L"Successfully wrote to file\n");

    fclose(file);
    return 0;
}

freadfwrite 函数是 C 语言标准库中用于二进制输入和输出的函数。这两个函数允许程序以二进制形式从文件读取数据或向文件写入数据,适用于处理不同类型的数据,如图像、音频文件或任何其他二进制数据。

二进制输出-fwrite()

fwrite 函数用于向文件写入二进制数据。

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:指向包含要写入文件的数据的缓冲区的指针。
  • size:要写入的每个数据项的大小,以字节为单位。
  • nmemb:要写入的数据项的数量。
  • stream:指向 FILE 对象的指针,该对象标识了 fwrite 函数向其中写入数据的流。

注意事项

  • 在使用 freadfwrite 之前,确保已以适当的模式打开了文件(例如,二进制读取或写入模式)。
  • 处理 freadfwrite 的返回值是非常重要的,它可以帮助识别和处理读写过程中可能发生的错误。
  • freadfwrite 都不会自动处理字节序问题,在跨平台读写二进制数据时需要特别注意。

返回值

  • 成功:返回实际写入的数据项数量。如果这个值小于 nmemb,表示发生了错误。
  • 失败:返回 0。

示例:使用 fread 和 fwrite

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *infile, *outfile;
    infile = fopen("input.bin", "rb");
    outfile = fopen("output.bin", "wb");

    if (!infile || !outfile) {
        perror("Error opening file");
        return 1;
    }

    // 假设我们知道数据项的大小和数量
    size_t size = sizeof(int);
    size_t nmemb = 10;
    int buffer[10];

    // 从文件中读取数据
    size_t read = fread(buffer, size, nmemb, infile);
    if (read != nmemb) {
        perror("Error reading file");
        // 处理错误
    }

    // 向文件写入数据
    size_t written = fwrite(buffer, size, nmemb, outfile);
    if (written != nmemb) {
        perror("Error writing file");
        // 处理错误
    }

    fclose(infile);
    fclose(outfile);

    return 0;
}

4. 读取文件

格式化输入 - fscanf()

int fscanf(FILE *stream, const char *format, ...);
  • 类似scanf(),但是fscanf()从指定的文件中读取数据。

字符输入 - fgetc()

int fgetc(FILE *stream);
  • 从文件中读取下一个字符。

字符串输入 - fgets()

char *fgets(char *str, int num, FILE *stream);
  • str:指向一个字符数组的指针,用于存储读取的字符串。
  • num:指定最多读取的字符数(包括最后的空字符)。因此,fgets实际上会读取num-1个字符,最后一个字符位置用于存储空字符\0
  • stream:指向FILE对象的指针,该FILE对象标识了fgets函数将从中读取数据的流。

fgets函数从stream指定的输入流中读取最多num-1个字符,直到遇到换行符\n或文件结束符EOF为止。读取的字符串包括换行符(如果存在),并且总是以空字符\0结尾。

如果读取成功,fgets函数返回str;如果遇到文件结束或发生错误,且没有读取到任何字符,则返回NULL

  • 如果fgets读取到换行符\n,它会将换行符存储在str指向的数组中,然后添加空字符\0
  • fgets函数对于避免缓冲区溢出是安全的,因为它允许指定最大读取字符数。
  • 如果读取的行比缓冲区小,则整行内容(包括换行符)都会被读取到缓冲区中。如果读取的行内容超过了缓冲区大小,则fgets会读取缓冲区大小减一的字符数,剩余的字符会留在输入流中,等待下次读取。

示例:使用fgets读取文件

下面的代码示例展示了如何使用fgets函数从文件中读取字符串。

#include <stdio.h>

int main() {
    char buffer[100];
    FILE *fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Failed to open file");
        return 1;
    }

    // 从文件中读取一行
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    // 关闭文件
    fclose(fp);

    return 0;
}

宽字符输入-fgetws()

它与 fgets 函数类似,但专门用于处理宽字符(通常是 Unicode 字符)。这使得 fgetws 成为处理包含多字节字符集(如 UTF-16 或 UTF-32)文件的理想选择。

wchar_t *fgetws(wchar_t *ws, int num, FILE *stream);
  • ws:指向宽字符数组的指针,用于存储从文件中读取的字符串。
  • num:要读取的最大字符数(包括结尾的空字符),因此实际上最多读取 num-1 个宽字符。
  • stream:指向 FILE 对象的指针,该 FILE 对象标识了 fgetws 函数将从中读取数据的流。

注意事项

  • 使用 fgetws 之前,确保已通过调用 setlocale 函数设置了正确的区域设置,以便正确处理宽字符和多字节字符。
  • fgets 一样,fgetws 也会将换行符(如果存在)读入到缓冲区中,并以空字符结尾。
  • 如果读取的行内容超过了缓冲区大小,fgetws 会读取缓冲区大小减一的宽字符数,剩余的字符会留在输入流中,等待下次读取。

返回值

  • 成功:返回指向宽字符字符串 ws 的指针。
  • 失败或文件结束:如果遇到文件结束或发生错误,并且没有读取到任何字符,则返回 NULL

示例:使用 fgetws 读取文件

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main() {
    // 设置当前 C 本地环境为用户的本地环境
    setlocale(LC_ALL, "");

    wchar_t buffer[256];
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        wprintf(L"Failed to open file\n");
        return 1;
    }

    // 使用 fgetws 从文件中读取宽字符字符串
    while (fgetws(buffer, 256, file) != NULL) {
        wprintf(L"%ls", buffer);
    }

    fclose(file);
    return 0;
}

二进制输入-fread()

fread 函数用于从文件中读取二进制数据。

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:指向一个缓冲区的指针,用于接收从文件中读取的数据。
  • size:要读取的每个数据项的大小,以字节为单位。
  • nmemb:要读取的数据项的数量。
  • stream:指向 FILE 对象的指针,该对象标识了 fread 函数从中读取数据的流。

返回值

  • 成功:返回实际读取的数据项数量。如果这个值小于 nmemb,可能是遇到了文件末尾或发生了错误。
  • 失败:返回 0。

5. 文件定位

定位文件位置 - fseek()

fseek() 函数是 C 语言标准库中的一个函数,用于设置文件流的读写位置。使用 fseek() 可以实现文件的随机访问,通过改变文件内的位置指针,fseek() 允许你随机访问文件中的任意位置,这在处理大文件或需要跳过文件中的某些部分时非常有用。

  • fseek() 在二进制模式下工作得更好,因为文本模式下可能会因平台而异(如换行符在不同系统上的表示不同)。
  • 如果你需要确定文件的当前位置,可以使用 ftell() 函数。

函数原型

int fseek(FILE *stream, long offset, int whence);
  • stream:指向 FILE 对象的指针,该 FILE 对象标识了要操作的流。
  • offset:相对于 whence 参数所指定位置的偏移量,以字节为单位。偏移量可以是正数也可以是负数。
  • whence:这是一个起始位置,它决定了 offset 参数如何解释。whence 的值可以是:
    • SEEK_SET:将文件的开始位置设置为参考点。
    • SEEK_CUR:以文件的当前位置作为参考点。
    • SEEK_END:将文件的末尾位置设置为参考点。

返回值

  • 成功时,返回 0
  • 失败时,返回非零值,并且全局变量 errno 被设置为错误代码。

示例

假设我们有一个名为 example.txt 的文件,并且想要从文件的开始位置向前移动 10 个字节的位置并从那里开始读取数据。

#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[100];

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return -1;
    }

    // 将位置指针向前移动 10 个字节
    if (fseek(fp, 10, SEEK_SET) != 0) {
        perror("Error seeking in file");
        fclose(fp);
        return -1;
    }

    // 从当前位置读取数据
    if (fgets(buffer, 100, fp) != NULL) {
        printf("Data from file: %s\n", buffer);
    } else {
        perror("Error reading from file");
    }

    // 关闭文件
    fclose(fp);

    return 0;
}

获取文件位置 - ftell()

ftell() 函数是用于获取当前文件位置指示器的当前值的函数。这个值是从文件开头到当前位置指示器的字节偏移量。ftell() 配合 fseek() 函数可以用来实现文件的随机访问。

  • ftell() 函数返回的位置是基于字节的,而不是基于字符或任何其他单位的。这意味着即使从文件中读取了文本行,返回的偏移量也是字节计数,可能与字符数不同,尤其是在处理多字节字符集时。
  • 当以文本模式打开文件时,在某些平台上(例如 Windows),文件中的某些字符(如换行符 \n)可能会被转换成多个字符(如 \r\n),这可能会影响 ftell() 返回的位置值。
  • 如果需要在文件中重新定位文件位置指示器,可以使用 fseek() 函数,并将 ftell() 返回的位置值作为参数之一传递给 fseek()

函数原型

long ftell(FILE *stream);
  • stream:指向 FILE 对象的指针,该 FILE 对象标识了要操作的流。

返回值

  • 成功时,返回当前文件位置指示器的位置,即从文件开头到当前位置的字节偏移量。
  • 失败时,返回 -1L,并且可能设置全局变量 errno 来指示错误。

示例

假设我们想要打开一个文件,读取一些数据,并获取当前文件位置指示器的位置。

#include <stdio.h>

int main() {
    FILE *fp;
    char buffer[100];

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return -1;
    }

    // 从文件中读取数据
    if (fgets(buffer, 100, fp) != NULL) {
        printf("Read data: %s\n", buffer);

        // 获取并打印当前文件位置
        long int position = ftell(fp);
        if (position != -1L) {
            printf("Current file position: %ld\n", position);
        } else {
            perror("ftell failed");
        }
    } else {
        perror("Error reading from file");
    }

    // 关闭文件
    fclose(fp);

    return 0;
}

6. 检查文件结束和错误 - feof()ferror()

文件结束-feof

feof 函数用于检查文件流上的EOF指示器是否被设置。当你读取文件并到达文件末尾时,feof 可以帮助你确定是否已经读取了所有的数据。这是文件读取循环中常用的一个函数,以确保正确处理文件的读取过程,避免进入无限循环。

  • feof 只有在尝试过读取操作之后,才能准确地返回是否到达文件末尾的信息。即,它通常在读取操作如 fscanffgetsfgetc 返回失败后使用。
  • 使用 feof 判断文件末尾时要小心,因为如果读取发生错误,feof 也可能返回非零值,所以最好同时使用 ferror 函数来检查是否存在读取错误。
  • 直接在循环条件中使用 feof 可能会导致最后一次迭代中的数据被处理两次,因为在达到EOF之前,feof 不会返回真值。正确的做法是检查读取函数的返回值。

函数原型

int feof(FILE *stream);

参数

  • stream:指向 FILE 对象的指针,该 FILE 对象标识了一个已打开的文件流。

返回值

  • 如果文件流上的EOF指示器被设置,则函数返回一个非零值(真)。
    • 如果EOF指示器没有被设置,函数返回0(假)。

使用场景

feof 函数通常在读取文件时使用,以确定是否已经到达文件的末尾。这对于处理不确定文件大小的情况特别有用。

示例代码

下面是一个使用 feof 函数读取文件内容的示例:

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp;
    char ch;

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error in opening file");
        return(-1);
    }

    // 读取文件内容直到文件末尾
    while (!feof(fp)) {
        ch = fgetc(fp);
        if (feof(fp)) {
            break;
        }
        printf("%c", ch);
    }

    // 关闭文件
    fclose(fp);
    return 0;
}

文件错误-ferror

ferror 函数用于检查给定的文件流是否出现错误。当进行文件操作(如读取或写入)时,可能会遇到各种错误,如果文件流出现错误,该函数能帮助诊断问题。

  • 使用 ferror 来检查错误时,仅当文件操作函数如 fread, fwrite, fscanf, fprintf, fgetc, fputc 等返回异常结果时才有意义。
  • 如果 ferror 返回非零值,表示在文件流上发生了错误。此时,可以使用 perror 或其他错误处理机制来报告或处理错误。
  • 一旦使用 ferror 检测到错误,可以通过调用 clearerr 函数来清除文件流的错误标志和EOF标志,以便继续后续的文件操作。
  • 在处理文件时,同时使用 feofferror 可以更准确地控制文件读写过程中的异常情况,确保程序的健壮性。

函数原型

int ferror(FILE *stream);

参数

  • stream:指向 FILE 对象的指针,该 FILE 对象表示一个已经打开的文件流。

返回值

  • 如果指定的文件流上发生了错误,函数返回一个非零值(真)。
  • 如果没有错误发生,函数返回0(假)。

示例代码

ferror 函数通常在文件读取或写入操作后使用,以检查操作是否成功完成。如果操作失败,ferror 可以用来确定失败的原因是否是文件级别的错误。

下面是一个使用 ferror 函数来检查文件读取操作中是否出现错误的示例:

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp;
    char ch;

    // 打开文件
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("Error opening file");
        return(-1);
    }

    // 尝试从文件中读取字符
    while ((ch = fgetc(fp)) != EOF) {
        printf("%c", ch);
    }

    // 检查是否因为错误而结束读取循环
    if (ferror(fp)) {
        printf("Error reading from file.\n");
    } else {
        printf("\nEnd of file reached.\n");
    }

    // 关闭文件
    fclose(fp);
    return 0;
}

windows系统文件操作

在Windows操作系统中,windows.h头文件提供了一系列的API函数,用于执行文件和文件夹的操作。这些函数提供了比标准C库更丰富的功能,包括创建、读取、写入、修改文件以及管理文件属性等。下面是一些基础的windows.h文件操作函数的详细教程:

创建文件

使用CreateFile函数创建新文件或打开现有文件:

#include <windows.h>
#include <stdio.h>

int main() {
    HANDLE fileHandle = CreateFile(
        "example.txt",               // 文件名
        GENERIC_WRITE,               // 打开文件的方式
        0,                           // 共享模式,0表示不共享
        NULL,                        // 安全属性
        CREATE_ALWAYS,               // 如何创建
        FILE_ATTRIBUTE_NORMAL,       // 文件属性
        NULL);                       // 模板文件的句柄

    if (fileHandle == INVALID_HANDLE_VALUE) {
        printf("Failed to create file.\n");
    } else {
        printf("File created successfully.\n");
        CloseHandle(fileHandle);
    }

    return 0;
}

写入文件

使用WriteFile函数向文件写入数据:

#include <windows.h>
#include <stdio.h>

int main() {
    HANDLE fileHandle = CreateFile("example.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (fileHandle == INVALID_HANDLE_VALUE) {
        printf("Failed to open file.\n");
        return 1;
    }

    char data[] = "Hello, World!";
    DWORD written;

    BOOL result = WriteFile(
        fileHandle,       // 文件句柄
        data,             // 要写入的数据缓冲区
        sizeof(data)-1,   // 要写入的字节数
        &written,         // 写入的字节数
        NULL);            // 重叠结构

    if (result == FALSE) {
        printf("Failed to write to file.\n");
    } else {
        printf("Data written successfully.\n");
    }

    CloseHandle(fileHandle);
    return 0;
}

读取文件

使用ReadFile函数从文件读取数据:

#include <windows.h>
#include <stdio.h>

int main() {
    HANDLE fileHandle = CreateFile("example.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (fileHandle == INVALID_HANDLE_VALUE) {
        printf("Failed to open file.\n");
        return 1;
    }

    char buffer[128];
    DWORD read;

    BOOL result = ReadFile(
        fileHandle,       // 文件句柄
        buffer,           // 数据缓冲区
        128,              // 要读取的最大字节数
        &read,            // 实际读取的字节数
        NULL);            // 重叠结构

    if (result == FALSE) {
        printf("Failed to read from file.\n");
    } else {
        printf("Data read successfully: %s\n", buffer);
    }

    CloseHandle(fileHandle);
    return 0;
}

删除文件

使用DeleteFile函数删除文件:

#include <windows.h>
#include <stdio.h>

int main() {
    if (DeleteFile("example.txt")) {
        printf("File deleted successfully.\n");
    } else {
        printf("Failed to delete file.\n");
    }

    return 0;
}

创建目录

使用CreateDirectory函数创建新目录:

#include <windows.h>
#include <stdio.h>

int main() {
    if (CreateDirectory("new_directory", NULL)) {
        printf("Directory created successfully.\n");
    } else {
        printf("Failed to create directory.\n");
    }

    return 0;
}

删除目录

使用RemoveDirectory函数删除目录:

#include <windows.h>
#include <stdio.h>

int main() {
    if (RemoveDirectory("new_directory")) {
        printf("Directory removed successfully.\n");
    } else {
        printf("Failed to remove directory.\n");
    }

    return 0;
}