自学内容网 自学内容网

嵌入式Linux学习——标准 I/O 库

目录

标准的文件IO

FILE 指针

fopen

1. 参数

2. 返回值

1. 打开文件进行读取

2. 打开文件进行写入

3. 打开文件进行追加

4. 打开文件进行二进制操作

fclose()

fread

参数

返回值

特性

fwrite函数

参数

返回值

特性

示例

fread和fwrite

fseek

参数

返回值

fseek 的工作原理

将文件指针移动到文件的开头

从文件末尾偏移

ftell 和 fseek 结合使用

fseek 在二进制文件中的应用

示例:使用 fseek 访问二进制数据

feof

1. 参数

2. 返回值

3. 使用说明

二、feof 的典型用法

读取文本文件直到文件末尾

读取二进制文件直到文件末尾

feof 与文件读取的关系

feof 与 ferror 的配合使用

示例:同时检查文件读取的结束和错误

格式化 I/O

格式化输出

printf 函数

fprintf 函数

dprintf 函数

sprintf 函数

函数原型

说明

示例

snprintf 函数

函数原型

说明

示例

格式化字符串format

标志(flags)

最小宽度(width)

精度(precision)

长度修饰符(length)

格式化输入

scanf 函数

参数说明:

返回值:

示例:

fscanf 函数

参数说明:

返回值:

示例:

sscanf 函数

参数说明:

返回值:

示例:

格式化字符串

width 字段(最大字符宽度)

length 修饰符(长度修饰符)

IO Buffer Cache

刷新缓冲区

fsync

函数原型

与 fsync 的区别


这个主题会让各位再次熟悉一些,我们在本章将会讨论的是标准IO库的学习。笔者也认为对于粗粒度的,更加一般的操作需要使用标准IO来保证程序的可移植性。但是在那之前,我们需要好好了解一些概念。

标准的文件IO

FILE 指针

FILE指针是C语言标准库提供的一种用于处理文件输入输出(I/O)的抽象类型。它作为一个重要的构造,在文件操作中扮演着核心角色。通过FILE指针,程序可以方便地进行文件的读取、写入、关闭、定位等操作,而不需要直接操作底层文件系统。

FILE指针是一个指向FILE类型的指针,FILE本质上是一个由标准库定义的结构体。虽然具体实现因平台和编译器而异,但通常FILE结构体会包含与文件操作相关的信息,如文件描述符、文件缓冲区、文件指针位置等。

typedef struct {
    int fd;             // 文件描述符
    char *buffer;       // 缓冲区指针
    size_t buffer_size; // 缓冲区大小
    size_t buffer_pos;  // 缓冲区当前位置
    int flags;          // 文件状态标志
    // 其他平台相关的文件状态信息
} FILE;

当然,这是一种可能的实现,MSVC是直接整个void* _Placeholder告诉你这是私密的内容,不允许更改。

FILE指针在文件操作过程中充当了一个中介角色,提供了对底层文件描述符的封装,使得程序员可以通过它实现对文件的访问。通过FILE指针,程序员能够使用标准库提供的I/O函数如fopen()fclose()fread()fwrite()等进行文件的打开、关闭、读取和写入。

在进行文件操作时,FILE指针会通过标准库的I/O函数与操作系统的文件系统进行交互。操作系统会为每个打开的文件分配一个文件描述符(在Unix-like系统中通常是一个整数),该描述符用于表示文件在操作系统中的位置。FILE指针封装了文件描述符,并为其提供了缓冲区管理和文件指针的位置跟踪功能。

在文件的读取和写入过程中,FILE指针会使用缓冲区来提高性能。标准I/O库通常会为每个文件流分配一个缓冲区(例如,内存中的一块连续空间),数据通过缓冲区从程序读取或写入到磁盘。这种方式可以大大减少频繁的磁盘访问,提高文件操作的效率。数据的实际读取和写入操作通常是在缓冲区满时或手动刷新时(例如调用fflush())才会与磁盘进行交互。

FILE指针的封装可以从几个角度来分析:

  1. 抽象化文件操作 FILE指针通过将文件操作与具体的底层系统细节隔离开来,使得文件操作变得更加简洁和高效。程序员只需要关心如何通过标准I/O函数来操作文件,而不需要了解操作系统如何管理文件描述符、缓冲区等细节。这种抽象化大大降低了编程的复杂性,同时也提高了代码的可移植性。通过fopen()打开文件时,系统会自动为文件分配一个FILE指针,程序员无需手动处理文件描述符。

  2. 缓冲机制的封装 FILE指针还通过封装缓冲区管理来优化文件I/O。标准库使用缓冲区来减少磁盘操作的次数。每次从文件读取数据时,数据并不会直接从磁盘读取,而是先读取到缓冲区中,只有当缓冲区的数据满了,或调用fflush()时,才将数据写入磁盘。通过这种方式,文件操作的效率得到了提升。

  3. 跨平台的统一接口 由于FILE指针的抽象,C语言的标准I/O函数提供了跨平台的统一接口,使得开发者在不同的操作系统上编写代码时无需担心底层实现的差异。例如,无论是在Windows还是Linux平台,fopen()fclose()等函数的行为和接口几乎完全相同,程序员无需关心操作系统如何处理文件描述符和文件缓冲区。这种统一性极大地提高了代码的可移植性。

虽然FILE指针为程序员提供了高层次的抽象,但它仍然需要与操作系统的文件系统进行交互。每个FILE指针都会关联到一个文件描述符,这个文件描述符是操作系统用来标识文件的一个整数值。在Unix-like系统中,文件描述符通常是一个整数,指向操作系统内部的一个文件结构;在Windows中,文件描述符也有类似的作用。

当程序调用fopen()打开文件时,标准库首先会调用操作系统的系统调用(如open())来打开文件,并获得一个文件描述符。然后,FILE指针将文件描述符封装起来,并返回给程序。之后,所有的读写操作都会通过该FILE指针来进行,标准库会自动处理缓冲区的管理,并在合适的时机将数据从缓冲区写入磁盘,或者从磁盘读取数据到缓冲区。

虽说不同平台和编译器对FILE指针的具体实现有所不同,但通常情况下,FILE指针的实现会涉及以下几个方面:

  1. 文件描述符管理 操作系统为每个打开的文件分配一个文件描述符,该文件描述符是一个整数,指向操作系统的内部结构。FILE指针封装了该文件描述符,并通过标准库函数与文件进行交互。

  2. 缓冲区管理 为了提高文件I/O的性能,FILE指针会为每个文件分配一个缓冲区。该缓冲区的大小通常是由标准库或操作系统决定的。缓冲区可以是行缓冲(每次读取一行数据)、块缓冲(每次读取一个固定大小的数据块)或者无缓冲(每次读取一个字节)。文件的读取和写入操作通常是通过缓冲区来完成的。

  3. 文件指针位置 每个FILE指针还需要跟踪当前的文件指针位置,这个位置表示文件的读取或写入位置。标准I/O函数会根据该位置决定下一次的读取或写入操作。通常情况下,文件指针的位置是在读取或写入数据后自动更新的,程序员无需手动管理。

  4. 文件状态标志 FILE指针还会维护一些文件的状态标志,例如是否发生了错误、文件是否已到达末尾等。标准库会根据这些标志来决定下一步操作的行为,例如在读取过程中遇到文件结束符(EOF)时,fgetc()等函数会返回EOF,并且文件状态标志会被相应更新。

FILE指针通常是基于缓冲区的,这意味着对于某些需要高效文件访问的场景(例如大文件的随机读取或写入),可能会出现性能瓶颈。此外,FILE指针的设计使得它在多线程环境下可能不太安全,因为多个线程同时访问同一个FILE指针可能会导致数据竞争和不一致的问题。

fopen

在 C 语言中,fopen 是一个用于打开文件的标准库函数,它提供了一种方便的方式来操作文件。通过 fopen,程序可以以不同的方式(只读、写入、追加等)打开文件,并返回一个文件指针,这个指针用于后续的文件操作,如读写、关闭文件等。

FILE *fopen(const char *filename, const char *mode);
1. 参数
  • filename:要打开的文件的名称,可以是相对路径或绝对路径。

  • mode:指定文件的打开模式,确定文件的访问方式。例如,是否可以读取、写入、追加等。

2. 返回值
  • 成功时,返回一个 FILE 类型的指针,该指针用于指向打开的文件。

  • 失败时,返回 NULL,并且可以通过 errno 获取错误信息。

fopen 的第二个参数 mode 用于指定打开文件的方式。这一点,还真是跟我们之前讨论过的open函数是一样的。常用的模式包括:

打开模式描述
"r"只读方式打开文件。文件必须存在。
"w"写入方式打开文件。若文件存在,内容被清空;若文件不存在,创建文件。
"a"追加方式打开文件。若文件存在,内容会添加到文件末尾;若文件不存在,创建文件。
"r+"读写方式打开文件。文件必须存在。
"w+"读写方式打开文件。若文件存在,内容被清空;若文件不存在,创建文件。
"a+"读写追加方式打开文件。若文件存在,内容会添加到文件末尾;若文件不存在,创建文件。
"rb"以二进制只读方式打开文件。文件必须存在。
"wb"以二进制写入方式打开文件。若文件存在,内容被清空;若文件不存在,创建文件。
"ab"以二进制追加方式打开文件。若文件存在,内容会添加到文件末尾;若文件不存在,创建文件。
"r+b"以二进制读写方式打开文件。文件必须存在。
1. 打开文件进行读取
#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    // 文件操作...
​
    fclose(file);
    return 0;
}

在这个示例中,fopen 以只读模式打开文件 example.txt。如果文件不存在,fopen 返回 NULL,并且通过 perror 输出错误信息。

2. 打开文件进行写入
#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "w");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    fprintf(file, "Hello, World!\n");
​
    fclose(file);
    return 0;
}

在这个示例中,fopen 以写入模式打开文件 example.txt。如果文件已存在,原内容将被清空;如果文件不存在,fopen 会创建一个新文件。然后,fprintf 将字符串 "Hello, World!" 写入文件。

3. 打开文件进行追加
#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "a");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    fprintf(file, "Appending some text...\n");
​
    fclose(file);
    return 0;
}

在这个示例中,fopen 以追加模式打开文件。如果文件已存在,新数据将被添加到文件的末尾。如果文件不存在,fopen 会创建一个新文件。

4. 打开文件进行二进制操作
#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    int data = 123;
    fwrite(&data, sizeof(data), 1, file);
​
    fclose(file);
    return 0;
}

在这个示例中,fopen 以二进制写入模式打开文件 example.binfwrite 将一个整数 123 以二进制形式写入文件

fclose()

调用fclose()库函数可以关闭一个由 fopen()打开的文件,其函数原型如下所示:

#include <stdio.h> 
int fclose(FILE *stream); 

这个没啥好说的。

fread

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

fread毫无疑问的从给定的FILE文件指针中读东西进来。

参数
  • ptr:指向一个内存区域的指针,用于存储从文件中读取的数据。该内存区域必须足够大,以容纳所读取的数据。

  • size:每个元素的字节数,即要读取的每个数据单元的大小。

  • nmemb:要读取的元素数量,即总共要读取多少个元素。

  • stream:指向文件的指针,由 fopenfdopen 返回,表示操作的目标文件。

返回值
  • 成功时,返回实际读取的元素数量,通常是 nmemb 的值。如果返回的数量小于 nmemb,则可能是文件末尾(EOF)或者发生了读取错误。

  • 失败时,返回 0,并且可以通过 ferror 函数检查文件是否发生了读取错误。

特性
  • fread 会按指定的 sizenmemb 从文件中读取数据,并将数据存入 ptr 所指向的内存。

  • 读取是按二进制方式进行的,不会进行数据转换,因此能够准确读取所有类型的数据,包括结构体、数组等。

#include <stdio.h>
​
int main() {
    FILE *file = fopen("data.bin", "rb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    int buffer[10];
    size_t items_read = fread(buffer, sizeof(int), 10, file);
    if (items_read != 10) {
        if (feof(file)) {
            printf("End of file reached.\n");
        } else {
            perror("Error reading file");
        }
    }
​
    for (size_t i = 0; i < items_read; ++i) {
        printf("Read value: %d\n", buffer[i]);
    }
​
    fclose(file);
    return 0;
}

fwrite函数

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数
  • ptr:指向包含要写入数据的内存区域的指针。这个内存区域中的数据将被写入到文件中。

  • size:每个元素的字节数,即要写入的每个数据单元的大小。

  • nmemb:要写入的元素数量,即总共要写入多少个元素。

  • stream:指向目标文件的指针,由 fopenfdopen 返回,表示操作的目标文件。

返回值
  • 成功时,返回实际写入的元素数量,通常是 nmemb 的值。如果返回的数量小于 nmemb,则可能是文件写入错误,或者文件的写入操作受到了某些限制。

  • 失败时,返回 0,并且可以通过 ferror 函数检查文件是否发生了写入错误。

特性
  • fwrite 会按二进制方式将内存中的数据写入文件中,不会进行数据格式转换。

  • 它适用于写入任何类型的数据(如结构体、数组等),确保数据能够正确写入。

示例

假设我们希望将一个整数数组写入到文件中:

#include <stdio.h>
​
int main() {
    FILE *file = fopen("data.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    int data[] = {1, 2, 3, 4, 5};
    size_t items_written = fwrite(data, sizeof(int), 5, file);
    if (items_written != 5) {
        perror("Error writing to file");
    }
​
    fclose(file);
    return 0;
}

在这个例子中,fwrite 将整数数组 data 中的 5 个元素写入到 data.bin 文件中。如果写入的元素数量少于预期的 5 个,程序会检查并输出错误信息。

fread和fwrite
  1. 读取/写入大小不匹配:使用 freadfwrite 时,sizenmemb 应该根据数据的实际大小和数量来指定。如果读取或写入的字节数不正确,可能会导致数据损坏或未完全读取/写入。

  2. 返回值检查:为了保证数据操作的正确性,应该检查 freadfwrite 的返回值。如果返回的元素数量少于请求的数量,可能是由于文件结束或发生了错误。

  3. 二进制操作freadfwrite 都是按二进制方式操作的,因此它们能够准确地读取或写入所有数据类型,包括结构体、数组等。这一点与标准的字符流操作(如 fscanffprintf)不同,后者会进行格式化处理。

  4. 缓冲区大小:在使用 freadfwrite 时,缓冲区(即 ptr)必须足够大,以容纳所读取或写入的数据。如果缓冲区过小,可能会导致未完全读取或写入数据,或者引发缓冲区溢出问题。

  5. 文件位置指针:在使用 freadfwrite 时,文件指针会在读取或写入后自动更新。因此,如果你在多次读取或写入操作中需要控制文件的位置,可能需要使用 fseekftell 来显式地操作文件指针。

fseek

在 C 语言中,fseek 是一个用于移动文件指针的标准库函数,它允许你在文件中定位到指定的位置。通过 fseek,你可以控制从文件的开始位置、当前位置或文件末尾进行偏移,从而实现随机访问文件中的内容。是不是让你想起来了seek函数?做一样的事情的!

int fseek(FILE *stream, long offset, int whence);
参数
  • stream:指向文件的指针,由 fopenfdopen 返回,表示目标文件。

  • offset:偏移量,表示相对位置的偏移量。这个值通常是一个整数,表示文件指针应该移动的字节数。具体含义由 whence 参数来决定。

  • whence:指定偏移量 offset 是相对于哪个位置的。可以是以下常量之一:

    • SEEK_SET:相对于文件的开头。offset 为从文件开头的字节数。

    • SEEK_CUR:相对于当前位置。offset 为从当前文件指针的位置起偏移的字节数。

    • SEEK_END:相对于文件的末尾。offset 为从文件末尾的字节数。

返回值
  • 成功时,返回 0

  • 失败时,返回 -1,并且可以通过 errno 获取详细错误信息。

fseek 的工作原理

fseek 函数改变文件指针的位置,允许随机访问文件中的数据。它会将文件指针移动到指定的位置,从而使得后续的文件操作(如 freadfwritefgetc 等)会从该位置开始进行。offset 可以为负数,从而使得文件指针向回移动。

将文件指针移动到文件的开头

如果你希望将文件指针重新定位到文件的开头,可以使用 fseek 并传入 SEEK_SET,将 offset 设置为 0

#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    // 将文件指针移到文件的开头
    fseek(file, 0, SEEK_SET);
​
    // 继续文件操作...
    fclose(file);
    return 0;
}

你希望从当前文件指针位置开始,向前或向后移动一定的字节数,可以使用 SEEK_CUR 来指定偏移量。例如,向前移动 10 个字节:

#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    // 将文件指针向前移动 10 字节
    fseek(file, -10, SEEK_CUR);
​
    // 继续文件操作...
    fclose(file);
    return 0;
}

在这个例子中,文件指针从当前位置向前移动了 10 个字节。

从文件末尾偏移

如果你希望从文件的末尾向前移动一定的字节数(例如,读取文件的最后 100 字节),可以使用 SEEK_END

#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    // 将文件指针移到文件末尾前 100 字节的位置
    fseek(file, -100, SEEK_END);
​
    // 继续文件操作...
    fclose(file);
    return 0;
}

文件指针被移动到距离文件末尾 100 字节的位置。

fseek 可能会失败,通常是因为以下原因:

  1. 无效的偏移量:偏移量大于文件的总字节数,或文件打开模式不支持偏移(例如只读模式尝试进行写操作)。

  2. 文件描述符的错误:传入的 FILE * 指针无效,或者文件没有正确打开。

  3. 文件操作权限问题:如果文件在打开时没有足够的权限,可能会导致 fseek 调用失败。

可以使用 errno 来获取错误码,或者使用 perror 输出错误信息。

ftellfseek 结合使用

ftell获取我们当前的文件描述符的偏移量,ftellfseek 通常是配合使用的。ftell 返回当前文件指针的位置,而 fseek 用来设置文件指针的位置。通过 ftell 获取当前位置后,你可以在文件中进行定位操作。

#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    // 获取当前文件指针位置
    long pos = ftell(file);
    if (pos == -1) {
        perror("ftell error");
    }
​
    // 将文件指针移到文件的开头
    fseek(file, 0, SEEK_SET);
​
    // 可以在这里进行其他操作
​
    // 恢复到原来的位置
    fseek(file, pos, SEEK_SET);
​
    fclose(file);
    return 0;
}
fseek 在二进制文件中的应用

fseek 在二进制文件中非常有用,尤其是当文件的格式允许随机访问时。例如,如果文件存储了多个固定大小的数据块(如结构体或数组),你可以使用 fseek 定位到特定的数据块,然后使用 freadfwrite 读取或写入数据。

示例:使用 fseek 访问二进制数据

假设我们有一个二进制文件,其中存储了若干个结构体,每个结构体占用 100 字节。如果我们想读取文件中的第 3 个结构体,可以使用 fseek 定位到相应的位置,然后调用 fread 读取数据。

#include <stdio.h>
​
typedef struct {
    int id;
    char name[30];
    float score;
} Student;
​
int main() {
    FILE *file = fopen("students.dat", "rb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    // 定位到第 3 个学生数据的位置(假设每个结构体 100 字节)
    fseek(file, 2 * sizeof(Student), SEEK_SET);
​
    Student student;
    size_t items_read = fread(&student, sizeof(Student), 1, file);
    if (items_read != 1) {
        perror("Error reading file");
        fclose(file);
        return 1;
    }
​
    // 打印读取的数据
    printf("Student ID: %d\n", student.id);
    printf("Name: %s\n", student.name);
    printf("Score: %.2f\n", student.score);
​
    fclose(file);
    return 0;
}

在这个例子中,我们通过 fseek 定位到文件中第 3 个 Student 结构体的位置,然后使用 fread 读取数据。

feof

在 C 语言中,feof 是一个用于检查文件是否已到达末尾(EOF,End of File)的标准库函数。它常用于在读取文件时判断是否已经读取完所有内容,以防止继续尝试读取无效的数据。

int feof(FILE *stream);
1. 参数
  • stream:指向 FILE 类型的文件指针。该指针是通过 fopenfdopen 等函数打开文件时获得的。

2. 返回值
  • 非零值:表示文件已经到达文件末尾(EOF)。

  • 0:表示文件没有到达末尾,仍然可以继续读取。

3. 使用说明

feof 仅在文件指针移动到文件末尾时返回非零值(通常是 1)。当文件指针在文件中任何位置时,feof 返回 0,表示文件没有到达末尾。需要注意的是,feof 并不表示文件读取是否成功,它仅表示文件指针是否已到达文件末尾。

二、feof 的典型用法

feof 通常在循环读取文件的过程中使用,以判断是否已读取到文件的末尾。与读取函数(如 fgetcfreadfgets)的返回值配合使用是最常见的场景。需要注意的是,feof 只在尝试读取文件时,且文件指针到达文件末尾后才会返回非零值。

读取文本文件直到文件末尾
#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    char ch;
    while ((ch = fgetc(file)) != EOF) {  // fgetc 会返回 EOF 当到达文件末尾
        putchar(ch);  // 输出读取的字符
    }
​
    if (feof(file)) {
        printf("\nEnd of file reached.\n");
    } else {
        printf("\nError occurred while reading the file.\n");
    }
​
    fclose(file);
    return 0;
}

在这个例子中,fgetc 函数用来逐字符读取文件。如果文件读取到末尾,fgetc 会返回 EOF,然后 feof 被用来确认是否真的到达文件的末尾。

读取二进制文件直到文件末尾

在处理二进制文件时,fread 函数通常用来读取数据块。feof 可以用来检查是否已读取完文件。

#include <stdio.h>
​
int main() {
    FILE *file = fopen("data.bin", "rb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    char buffer[100];
    size_t bytesRead;
​
    while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        // 处理读取的内容
        fwrite(buffer, 1, bytesRead, stdout);  // 将读取的内容输出到标准输出
    }
​
    if (feof(file)) {
        printf("\nEnd of file reached.\n");
    } else {
        printf("\nError occurred while reading the file.\n");
    }
​
    fclose(file);
    return 0;
}

在这个例子中,fread 会返回实际读取的字节数,直到文件末尾。feof 在循环结束后用来确认是否已到达文件末尾。

feof 与文件读取的关系

虽然 feof 可以帮助我们检查是否到达了文件末尾,但在使用时需要注意以下几点:

  1. feof 仅在读取后有效:调用 feof 函数时,必须在文件读取函数(如 fgetcfreadfgets 等)调用之后使用。因为 feof 是根据文件指针的位置来判断的,而文件指针只有在进行读操作时才会移动。

  2. 避免误用 feof 检查文件结束:在读取文件时,不应直接依赖 feof 来判断文件是否结束。feof 应该是在每次读取操作后检查,因为文件读取函数在返回 EOF 或零时,文件指针已经到达文件末尾。

  3. 错误和文件结束的区分feof 返回非零值仅表示文件末尾,并不能区分是否由于其他错误导致读取失败。为了准确判断错误,通常需要同时检查 ferror

feofferror 的配合使用

在实际文件操作中,我们通常需要配合使用 feofferror 来判断文件读取的状态。feof 用于检查是否到达文件末尾,而 ferror 用于检查文件是否发生了读取错误。

示例:同时检查文件读取的结束和错误
#include <stdio.h>
​
int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }
​
    char ch;
    while ((ch = fgetc(file)) != EOF) {
        putchar(ch);  // 输出读取的字符
    }
​
    if (feof(file)) {
        printf("\nEnd of file reached.\n");
    } else if (ferror(file)) {
        printf("\nError occurred while reading the file.\n");
    }
​
    fclose(file);
    return 0;
}

在此示例中,feof 用来判断文件是否已到达末尾,而 ferror 用来检查是否发生了错误。如果在读取过程中发生了错误,ferror 会返回非零值。

feof 适用于所有类型的文件,包括文本文件和二进制文件。在文本文件中,feof 能帮助判断文件是否已到达末尾。在二进制文件中,feof 同样适用,并且配合 fread 等函数使用时,可以有效地处理文件结束。对于文本文件,feof 主要用于判断是否已读取完所有内容。对于二进制文件,feof 则用来判断文件是否已完全读取。

格式化 I/O

欸!还记得我们的printf函数吗,我相信很多人都是用过这个,它隶属于格式化IO的一部分。对于格式化的输出,我们有printf()、fprintf()、dprintf()、sprintf()、snprintf()。

格式化输出

printf 函数

int printf(const char *format, ...);
  • 参数

    • format:格式化字符串,指定如何格式化输出的文本。

    • ...:可变参数列表,传入与格式化字符串中格式控制符相匹配的数据。

  • 返回值

    • 返回输出的字符数(不包括终止的空字符),如果发生错误,返回负数。

printf 是 C 语言中最常用的输出函数之一,它将格式化的数据输出到标准输出(通常是屏幕)。格式化字符串通过控制符(例如 %d, %s, %f)来指定如何显示不同类型的数据。printf 可以输出文本、整数、浮点数、字符等数据类型。

#include <stdio.h>
​
int main() {
    int x = 10;
    float y = 3.14;
​
    printf("Integer: %d, Float: %.2f\n", x, y);
    return 0;
}

输出:

Integer: 10, Float: 3.14

fprintf 函数

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

    • stream:目标输出流,通常是文件指针(由 fopen 返回)。它也可以是 stdout(标准输出)或 stderr(标准错误)等。

    • format:格式化字符串,指定如何格式化输出的文本。

    • ...:与格式化字符串相匹配的可变参数列表。

  • 返回值

    • 返回写入的字符数(不包括终止的空字符),如果发生错误,返回负数。

fprintfprintf 类似,不同之处在于它将格式化的输出写入到指定的文件流中,而不是标准输出。这使得你可以将输出重定向到文件中。

#include <stdio.h>
​
int main() {
    FILE *file = fopen("output.txt", "w");
    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }
​
    int x = 10;
    float y = 3.14;
    fprintf(file, "Integer: %d, Float: %.2f\n", x, y);
​
    fclose(file);
    return 0;
}

此代码将输出写入到文件 output.txt,内容如下:

Integer: 10, Float: 3.14

dprintf 函数

int dprintf(int fd, const char *format, ...);
  • 参数

    • fd:文件描述符,可以是标准输出(STDOUT_FILENO)、标准错误(STDERR_FILENO)等,或者是通过 open 返回的文件描述符。

    • format:格式化字符串,指定如何格式化输出的文本。

    • ...:与格式化字符串相匹配的可变参数列表。

  • 返回值

    • 返回写入的字节数(不包括终止的空字符)。如果发生错误,返回负数。

dprintffprintf 类似,但它是基于文件描述符的而非 FILE * 类型的流。它允许你将格式化的输出发送到任何打开的文件描述符,而不仅仅是标准输出或标准错误。这在底层 I/O 操作中非常有用。

#include <stdio.h>
#include <unistd.h>
​
int main() {
    int x = 10;
    float y = 3.14;
    dprintf(STDOUT_FILENO, "Integer: %d, Float: %.2f\n", x, y);
    return 0;
}

输出:

Integer: 10, Float: 3.14

sprintf 函数

int sprintf(char *buf, const char *format, ...);
函数原型
  • 参数

    • buf:一个字符数组(缓冲区),用于存储格式化后的字符串。

    • format:格式化字符串,指定如何格式化输出的文本。

    • ...:与格式化字符串相匹配的可变参数列表。

  • 返回值

    • 返回写入缓冲区的字符数(不包括终止的空字符),如果发生错误,返回负数。

说明

sprintf 将格式化的输出写入到提供的缓冲区中,而不是输出到终端或文件。这使得它非常适合需要构建字符串的场景。然而,sprintf 并不执行缓冲区溢出检查,因此在使用时需要非常小心,确保目标缓冲区足够大。

示例
#include <stdio.h>
​
int main() {
    char buffer[100];
    int x = 10;
    float y = 3.14;
​
    sprintf(buffer, "Integer: %d, Float: %.2f", x, y);
    printf("%s\n", buffer);
​
    return 0;
}

输出:

Integer: 10, Float: 3.14

snprintf 函数

int snprintf(char *buf, size_t size, const char *format, ...);
函数原型
  • 参数

    • buf:一个字符数组(缓冲区),用于存储格式化后的字符串。

    • size:缓冲区的大小,指定最多可以写入的字符数(包括终止空字符)。

    • format:格式化字符串,指定如何格式化输出的文本。

    • ...:与格式化字符串相匹配的可变参数列表。

  • 返回值

    • 返回写入缓冲区的字符数(不包括终止的空字符)。如果返回的字符数大于或等于 size,说明缓冲区没有足够的空间来存储所有格式化数据。

说明

snprintfsprintf 的更安全版本,它允许你指定缓冲区的大小,以避免缓冲区溢出。当格式化后的数据超出指定大小时,snprintf 会截断输出,只写入 size - 1 个字符,并保证缓冲区末尾有一个终止空字符。

示例
#include <stdio.h>
​
int main() {
    char buffer[20];
    int x = 10;
    float y = 3.14;
​
    snprintf(buffer, sizeof(buffer), "Integer: %d, Float: %.2f", x, y);
    printf("%s\n", buffer);
​
    return 0;
}

输出:

Integer: 10, Float: 3.14

如果缓冲区不够大,snprintf 会截断字符串,并保证输出不会溢出。

格式化字符串format

%[flags][width][.precision][length]type 
字符对应的数据类型含义示例说明
d/iint输出有符号十进制表示的整数,i 是老式写法printf("%d\n", 123); 输出 123
ounsigned int输出无符号八进制表示的整数,默认不输出前缀 0,# 标志可以加前缀 0printf("%o\n", 123); 输出 173
uunsigned int输出无符号十进制表示的整数printf("%u\n", 123); 输出 123
x/Xunsigned int输出无符号十六进制表示的整数,xX 的区别在于字母的大小写printf("%x\n", 123); 输出 7b
f/Fdouble输出浮点数,支持单精度浮点数和双精度浮点数,fF 没有区别printf("%f\n", 520.1314); 输出 520.131400
e/Edouble输出以科学计数法表示的浮点数,eE 的区别在于小写和大写printf("%e\n", 520.1314); 输出 5.201314e+02
g/Gdouble根据数值的长度,选择最短的表示方式 %f/%e%F/%Eprintf("%g %g\n", 0.000000123, 0.123); 输出 1.23e-07 0.123
schar*输出字符串,直至遇到终止字符 \0printf("%s\n", "Hello World"); 输出 Hello World
pvoid*输出指针地址的十六进制表示printf("%p\n", "Hello World"); 输出 0x400624
cchar输出字符型,可以将数字按照 ASCII 码转换为字符printf("%c\n", 64); 输出 @
标志(flags)
字符名称作用
#井号对于 o,输出时加前缀 0;对于 x/X,加前缀 0x/0X。对于浮点数,始终输出小数点。
0数字 0对于数字类型,输出时补 0,直到达到最小宽度。
-减号左对齐,若宽度不足,右侧填充空格(如果 0 标志存在,则 - 会覆盖 0)。
' '空格输出正数时在前面加空格,负数时加负号。
+加号输出时无论正负,均输出符号,正数显示 +,负数显示 -
最小宽度(width)
描述示例
数字printf("%06d", 1000); 输出 001000
* 星号printf("%0*d", 6, 1000); 输出 001000
精度(precision)
描述示例
对整型(d、i、o、u、x、X)printf("%8.5d\n", 100); 输出 00100
对浮点型(a、A、e、E、f、F)printf("%.8f\n", 520.1314); 输出 520.13140000
对字符串(s)printf("%.5s\n", "hello world"); 输出 hello
动态精度printf("%.*s\n", 5, "hello world"); 输出 hello
长度修饰符(length)
长度修饰符对应类型
hhsigned char / unsigned char
hshort int / unsigned short int
llong int / unsigned long int
lllong long int / unsigned long long int
Llong double
jintmax_t / uintmax_t
zsize_t / ssize_t
tptrdiff_t / ptrdiff_t

格式化输入

scanf 函数

scanf 用于从标准输入(通常是键盘)读取数据。它的原型如下:

c
​
​
复制代码
int scanf(const char *format, ...);
参数说明:
  • format:格式控制字符串,用于指定输入数据的类型和格式。格式字符串中的每个格式控制符与后续参数一一对应,用于指定如何解析输入数据。

  • ...:一个或多个指针参数,指定每个输入项存储的地址。

返回值:
  • 返回成功读取的项目数。如果遇到错误或者输入的数据不匹配格式控制字符串,则返回 EOF(通常是 -1)。

示例:
int a;
float b;
char str[100];
​
scanf("%d %f %s", &a, &b, str);

这段代码会从标准输入读取一个整数、一个浮点数和一个字符串。

fscanf 函数

fscanf 用于从指定的文件流(如文件)读取数据。它的原型如下:

int fscanf(FILE *stream, const char *format, ...);
参数说明:
  • stream:文件流指针,指向输入文件。可以是 stdin(标准输入)以从标准输入读取,或者是一个由 fopen 打开的文件指针。

  • format:格式控制字符串,功能与 scanf 相同,用于解析输入数据。

  • ...:一个或多个指针参数,指定每个输入项存储的地址。

返回值:
  • 返回成功读取的项目数。如果遇到错误或数据格式不匹配,返回 EOF

示例:
FILE *fp = fopen("data.txt", "r");
int a;
float b;
char str[100];
​
fscanf(fp, "%d %f %s", &a, &b, str);
fclose(fp);

这段代码从文件 "data.txt" 中读取数据并将其存储到 abstr 中。

sscanf 函数

sscanf 用于从一个字符串中读取数据。它的原型如下:

int sscanf(const char *str, const char *format, ...);
参数说明:
  • str:输入的字符串,函数会从这个字符串中读取数据。

  • format:格式控制字符串,功能与 scanffscanf 相同,用于解析输入数据。

  • ...:一个或多个指针参数,指定每个输入项存储的地址。

返回值:
  • 返回成功读取的项目数。如果数据格式不匹配或者遇到错误,返回 EOF

示例:
char str[] = "123 45.67 Hello";
int a;
float b;
char c[100];
​
sscanf(str, "%d %f %s", &a, &b, c);
printf("a = %d, b = %.2f, c = %s\n", a, b, c);

这段代码会从字符串 str 中解析出一个整数、一个浮点数和一个字符串,并打印出来。

格式化字符串

格式符数据类型说明
%dint匹配一个有符号十进制整数。
%iint匹配一个有符号整数,可以是十进制、八进制或十六进制。
%ounsigned int匹配一个无符号八进制整数。
%uunsigned int匹配一个无符号十进制整数。
%xunsigned int匹配一个无符号十六进制整数。输入可以是以 0x0X 开头。
%Xunsigned int匹配一个无符号十六进制整数,数字以 0X 开头。
%ffloat匹配一个浮点数。
%efloat等效于 %f,用于科学计数法表示的浮点数。
%Efloat等效于 %f,用于科学计数法表示的浮点数(使用大写 E)。
%gfloat等效于 %f,根据数值选择最短的表示方式(可以是 %f%e)。
%afloat等效于 %f,用于以十六进制浮点格式表示数值。
%schar *匹配一个字符串,读取直到遇到空白字符(如空格、换行符、制表符)。
%cchar匹配一个字符。
%pvoid *匹配一个指针值(以十六进制格式表示)。
%[char *匹配一组指定范围的字符,如 %[a-z] 匹配所有小写字母,%[^a-z] 匹配不包含小写字母的任何字符。
width 字段(最大字符宽度)

width 字段指定了格式控制符匹配数据时的最大字符宽度。当读取到最大字符宽度或遇到不匹配的字符时,输入会停止。对于字符串类型(%s),如果指定了 width,则不会读取超过指定宽度的字符,并且会在字符串末尾自动添加终止符 \0

length 修饰符(长度修饰符)

length 修饰符用于扩展对不同数据类型的识别,指明数据类型的长度,确保可以处理不同大小的整数或浮点数。常见的修饰符如下:

修饰符数据类型
hhsigned charunsigned char
hshort intunsigned short int
llong intunsigned long int
lllong long intunsigned long long int
Llong double
jintmax_tuintmax_t
zsize_tssize_t
tptrdiff_t

IO Buffer Cache

IO是有Buffer Cache的。这是因为一般而言我们向外设写数据需要的Clocks远大于操作内存的Clocks的,意味着啥都开一下往磁盘里写,速度会慢到发指。所以现代的IO都会自备缓冲区,缓冲区一满或者是上面要求强制写入的时候才会打开一次磁盘写入。

举个例子:

write(fd, "Hello", 5); // 写入 5 个字节数据 

调用write()后仅仅只是将这5 个字节数据拷贝到了内核空间的缓冲区中,拷贝完成之后函数就返回了,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中,所以由此可知,系统调用write()与磁盘操作并不是同步的,write()函数并不会等待数据真正写入到磁盘之后再返回。如果在此期间,其它进程调用 read()函数读取该文件的这几个字节数据,那么内核将自动从缓冲区中读取这几个字节数据返回给应用程序。

与此同理,对于读文件而言亦是如此,内核会从磁盘设备中读取文件的数据并存储到内核的缓冲区中,当调用 read()函数读取数据时,read()调用将从内核缓冲区中读取数据,直至把缓冲区中的数据读完,这时,内核会将文件的下一段内容读入到内核缓冲区中进行缓存。

我们把这个内核缓冲区就称为文件I/O 的内核缓冲。这样的设计,目的是为了提高文件 I/O 的速度和效率,使得系统调用 read()、write()的操作更为快速,不需要等待磁盘操作(将数据写入到磁盘或从磁盘读取出数据),磁盘操作通常是比较缓慢的。同时这一设计也更为高效,减少了内核操作磁盘的次数,譬如线程1 调用write()向文件写入数据"abcd",线程2 也调用write()向文件写入数据"1234",这样的话,数据"abcd"和"1234"都被缓存在了内核的缓冲区中,在稍后内核会将它们一起写入到磁盘中,只发起一次磁盘操作请求;加入没有内核缓冲区,那么每一次调用write(),内核就会执行一次磁盘操作。

前面提到,当调用write()之后,内核稍后会将数据写入到磁盘设备中,具体是什么时间点写入到磁盘,这个其实是不确定的,由内核根据相应的存储算法自动判断。 通过前面的介绍可知,文件 I/O 的内核缓冲区自然是越大越好,Linux 内核本身对内核缓冲区的大小没有固定上限。内核会分配尽可能多的内核来作为文件I/O 的内核缓冲区,但受限于物理内存的总量,如果系统可用的物理内存越多,那自然对应的内核缓冲区也就越大,操作越大的文件也要依赖于更大空间的内核缓冲。

刷新缓冲区

强制将文件I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备中,对于某些应用程序来说,可能是很有必要的。一些例子:

  • 数据库系统:当需要确保数据的持久性时,数据库会使用 fsync 来确保事务的数据在磁盘上的可靠存储。

  • 日志系统:当程序写入日志时,调用 fsync 可以确保日志条目被及时写入磁盘,而不是依赖系统的缓存机制。

  • 文件修改:如果一个程序修改了重要文件,并且希望确保文件的修改不会丢失,应该调用 fsync 来同步数据。

当我们在Ubuntu 系统下拷贝文件到U 盘时,文件拷贝完成之后,通常在拔掉 U 盘之前,需要执行sync 命令进行同步操作,这个同步操作其实就是将文件 I/O 内核缓冲区中的数据更新到 U 盘硬件设备,所以如果在没有执行 sync 命令时拔掉 U 盘,很可能就会导致拷贝到 U 盘中的文件遭到破坏!

fsync

int fsync(int fd);
  • fd:一个有效的文件描述符,表示要同步的文件。这个文件描述符通常是通过 opencreat 或其他文件操作函数获得的。

  • 成功:返回 0,表示同步操作成功。

  • 失败

    :返回 -1,并设置 errno 来指示错误类型。常见的错误包括:

    • EBADF:传入的 fd 不是一个有效的文件描述符,或者它没有打开文件的写入权限。

    • EINVAL:传入的 fd 不是一个支持 fsync 操作的文件描述符。

    • EIO:I/O 错误,可能是在写入磁盘时发生了硬件故障。

fsync 函数会强制将文件 fd 所代表的文件的所有未写入的数据和元数据(如修改时间、权限等)从内存缓冲区刷新到磁盘中。这意味着,即使操作系统和硬件已经对文件做了优化(如缓存和延迟写入),调用 fsync 也会确保数据在磁盘上的持久性。

通常,文件系统会将文件的写操作缓存到内存中,以提高性能,而不是立即写入磁盘。但是,这可能导致数据丢失,例如在系统崩溃或电源故障时。通过调用 fsync,程序可以确保文件数据及时同步到磁盘,避免此类问题。

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
​
int main() {
    // 打开文件进行写操作
    int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        return 1;
    }
​
    // 写入一些数据
    const char *data = "Hello, fsync!";
    if (write(fd, data, 14) == -1) {
        perror("write");
        close(fd);
        return 1;
    }
​
    // 强制将文件内容同步到磁盘
    if (fsync(fd) == -1) {
        perror("fsync");
        close(fd);
        return 1;
    }
​
    printf("Data successfully synced to disk.\n");
​
    // 关闭文件
    close(fd);
    return 0;
}
  • fsync 是一个相对较慢的操作,尤其是在涉及大文件时,因为它需要确保所有的数据和元数据都已经被写入磁盘。

  • 在使用 fsync 时,要注意它只会同步单个文件。如果程序操作多个文件,可能需要对每个文件调用 fsync

  • 对于目录,可以使用 fdatasyncsync 来确保整个文件系统的同步,尤其是在需要确保所有文件都写入磁盘时。

sync 是一个系统调用,它的作用是将所有修改过的文件数据和元数据(如文件权限、时间戳等)同步到磁盘中。与 fsync 类似,sync 也确保内存中的文件数据被写入到磁盘,以防止系统崩溃或电源故障时数据丢失。

函数原型

int sync(void);
  • 无参数sync 函数没有任何参数,它会将系统中所有已修改的数据和元数据写入磁盘。

  • 成功:返回 0。

  • 失败:返回 -1,并设置 errno。但是,通常 sync 不会失败,因为它的目的是进行系统级别的同步操作,而不是针对单一文件的同步。

sync 函数会遍历整个系统中所有已被修改(有待写入磁盘)的文件,强制将它们的数据和元数据从内存中写入到磁盘。与 fsync 不同的是,sync 是全局操作,它会影响系统中所有被修改过的文件,而不是某个特定文件。

调用 sync 后,所有挂起的磁盘写操作都会被提交到磁盘,但 sync 并不等待文件写入完成。它将这些操作排队处理,系统会在稍后的某个时间实际执行这些操作。通常,sync 用于确保操作系统中的所有数据在进程结束或系统关机前被安全保存。

  • 系统关闭前的数据保护sync 常用于在系统关机之前调用,确保所有文件系统中的修改都被写入磁盘,避免因断电或强制关闭系统而导致数据丢失。

  • 磁盘缓存清理:程序可能会调用 sync 来强制清理操作系统的文件缓存,确保所有挂起的写入操作被实际提交到磁盘。

#include <unistd.h>
#include <stdio.h>
​
int main() {
    // 在程序结束时调用 sync,确保所有数据被写入磁盘
    sync();
    printf("All file system modifications have been synced to disk.\n");
    return 0;
}
fsync 的区别
  • fsyncfsync 作用于单一的文件描述符,强制同步特定文件的数据和元数据。它确保该文件的修改被写入磁盘并且完成时返回。

  • syncsync 是全局的,作用于所有文件。它将所有已经修改的数据和元数据提交到磁盘,但它不会等到写入操作完成。sync 的调用更像是一个后台操作,旨在保证数据的持久性,但不需要与特定文件关联。

  • sync 是一种相对较慢的操作,尤其在有大量数据等待同步时,因为它会处理整个文件系统的所有挂起修改。它的作用非常广泛,可能会影响整个系统的性能。

  • sync 一般不需要程序员手动调用,除非在某些特定场景下(如程序结束前、系统关机前等)需要确保所有文件的数据都已写入磁盘。

  • fsync 不同,sync 并不需要特定的文件描述符,因此它只能作为一个全局同步操作,而 fsync 用于针对特定文件执行同步操作。

通过调用 sync,系统可以确保所有的文件修改都被写入磁盘,这对于系统安全和数据持久性至关重要。


原文地址:https://blog.csdn.net/charlie114514191/article/details/143996648

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!