Skip to content

讲义:I/O 与文件

内容提要

  • C 的输入输出模型:缓冲与流的概念
  • C 标准 I/O 函数
  • 文件编码与文件输入输出

C 的输入输出模型

缓冲

广义的缓冲区是内存空间的一部分,在内存中预留了一定的存储空间,用来暂时保存输入和输出等 I/O 操作的一些数据,这些预留的空间就叫做缓冲区;而 buffer 缓冲区和 Cache 缓存区都属于缓冲区的一种。

buffer 缓冲区和 cache 缓存区

  • buffer 缓冲区存储速度不同步的设备或者优先级不同的设备之间的传输数据,比如键盘、鼠标等;此外,buffer 一般是用在写入磁盘的;
  • Cache 缓存区是位于 CPU 和主内存之间的容量较小但速度很快的存储器,Cache 保存着 CPU 刚用过的数据或循环使用的数据;Cache 缓存区的运用一般是在 I/O 的请求上

C 语言中,用户输入的字符被收集并储存在缓冲区(buffer)中,按下 Enter 键后程序才能使用用户输入的字符。

  • 完全缓冲:仅缓冲区填满时刷新缓冲区(将内容发送至目的地,通常用于文件输入。缓冲区的大小取决于系统,常见大小为 512bits 4096bits
  • 行缓冲:出现换行符时刷新缓冲区,通常用于键盘输入。所以在按下 Enter 键后程序才能使用用户输入的字符。

与之对应地,无缓冲输入的程序能够立即使用用户输入的内容。

stdin,stdout,stderr 的缓冲类型

Unix 约定 stdin stdout 若与终端关联则为行缓冲,而 stderr 为无缓冲。

为什么要有缓冲区?

  • 系统层面,减少 CPU 对磁盘的读写次数:CPU 读取磁盘中的数据并不是直接读取磁盘,而是先将磁盘的内容读入到内存,也就是 Cache,然后 CPU Cache 进行读取,进而操作数据;计算机对 Cache 的操作时间远远小于对磁盘的操作时间,大大的加快了运行速度,提高 CPU 的使用效率。

  • C 语言输入中,把若干字符作为一个块进行传输比逐个发送这些字符节省时间,打错字符可以直接通过键盘修正错误。

虽然缓冲输入好处很多,但是某些交互式程序也需要无缓冲输入。例如在游戏中,玩家的输入需要立即反应在屏幕上,而不是等待用户按下 Enter 键。

C 标准规定:输入是缓冲的。你能解释为什么 C 标准要规定输入是缓冲的吗?

为什么 C 标准规定输入是缓冲的?

一些计算机不允许无缓冲输入。ANSI 没有提供调用无缓冲输入的标准方式,这意味着是否能进行无缓冲输入取决于计算机系统。

如果你的计算机允许无缓冲输入,那么你所用的 C 编译器很可能会提供一个无缓冲输入的选项。例如,许多 IBM PC 兼容机的编译器都为支持无缓冲输入提供一系列特殊函数,其原型在conio.h中。在 Unix 系统中,可以使用iotcl()函数指定待输入的类型,然后使用getchar()执行相应操作。在 ANSI C 中,用setbuf()setbufv()控制缓冲,但是这两个函数可能不起作用。

我可以更改输入方式吗?

UNIX 库中有 ioctl() 函数用于指定待输入的类型,但这不属于 C 标准。

ANSI C 中,可以使用 setbuf()setvbuf() 控制缓冲,但受限于系统的设置。

C 库提供的输入输出方式称为标准 I/O,它们是建立在操作系统提供的底层 I/O 上的。底层 I/O 之间常常会有一些差异:

各个系统的文件差异

差异 UNIX Windows MacOS
换行符 \n
LF
\r\n
CRLF
\n ( 较早的 MacOS 使用\r )
LF
文件结束符 ^D ^Z ^D

注:如 ^Z 代表 Ctrl+Z,你可以使用该组合键结束键盘输入。

文件结尾也不一定由文件结束符标记。事实上,UNIX 系统储存文件大小信息,依据文件大小信息决定文件末尾。

从概念上看,C 处理的是而不是文件。不同属性和不同种类的输入,由属性更统一的来表示。流告诉我们,我们可以用处理文件的方式来处理键盘输入。

C 语言中 I/O 流由File类型的对象表示,该对象只能通过FILE*类型的指针访问及操作。每个流都与外部的物理设备相关联。

流就是一系列连续的字节。

打开文件的过程就是把流与文件相关联,读写都通过流来完成。

getchar()scanf() 等函数读取到文件结尾时会返回一个特殊的值 EOF,在 stdio.h 中定义了:

#define EOF (-1) 
你能解释为什么要把它定为 -1 吗?

The value of EOF is -1 because it has to be different from any return value from getchar that is an actual character. So getchar returns any character value as an unsigned char, converted to int, which will therefore be non-negative.

因此,如果你在终端中进行输入,可以使用Ctrl+Z(Windows)或者Ctrl+D(unix-style systems)作为EOF结束输入。

由以上两部分,我们可以知道在 C 的键盘输入中,我们可以用Enter处理缓冲区中的内容,用Ctrl+Z或者Ctrl+D结束输入。

标准流

在标准头文件<stidio.h>中,定义了三个文本流。

#define stdin  /* 由实现定义 */
#define stdout /* 由实现定义 */
#define stderr /* 由实现定义 */

这三个文本流是预定义的。这些流在程序启动时隐式打开,且为无取向的。

  1. 与标准输入流关联,用于读取传统的输入。程序启动时,当且仅当能确定流不引用交互式设备时该流为完全缓冲。
  2. 与标准输出流关联,用于写入传统的输出。程序启动时,当且仅当能确定流不引用交互式设备时该流为完全缓冲。
  3. 与标准错误流关联,用于写入诊断输出。程序启动时,该流不为完全缓冲。

文件描述符

当一个程序成功向操作系统请求访问一个打开的文件 , 内核会返回一个指向内核中全局文件表 (global file table) 中的入口点 (entry) 的文件描述符 . 文件表入口点包含如 : 文件的 inode( 硬盘中的位置 ), 字节偏移量 (byte offset), 以及对这个数据流的访问限制 ( 只读 , 只写等 )

文件描述符 : 是计算机操作系统中被打开文件的唯一标识 . 它用来描述一种数据资源 , 以及这个数据资源可以如何被访问到。

Unix 系统当中 , 前三个文件描述符 0, 1, 2 默认为 stdin stdout stderr

字符输入输出

重定向

在开始介绍字符输入输出前,先介绍系统的重定向。

重定向是指改变标准输入输出的目的地。在 UNIX 系统中,可以使用><来重定向输入输出。

例如,ls > ls.txtls的输出重定向到ls.txt文件中,cat < ls.txtls.txt文件的内容重定向到cat命令中。

重定向的目的地可以是文件,也可以是其他程序。

例如,ls | catls的输出重定向到cat命令中。

更进一步,我们可以使用>>来追加输出重定向,<<来追加输入重定向。

结合上一节讲的内容,你可以说出2>2>>的作用吗?

无格式输入 / 输出

在字符 I/O 中,我们常使用以下函数来处理字符,这些函数位于<stdio.h>头文件中。<wchar.h> 头文件提供了具有宽字符输入 / 输出功能的函数。

e.g.

echo_eof.c
/* echo_eof.c -- repeats input to end of file */
#include <stdio.h>
int main(void)
{
    int ch;

    while ((ch = getchar()) != EOF)
    putchar(ch);

    return 0;
}

结合重定向运算符,我们可以使用echo_eof < echo_eof.c来测试该程序。

$ echo_eof < echo_eof.c
$ echo_eof < echo_eof.c > echo_eof.txt
$ echo_eof < echo_eof.c | cat

窄字符无格式输入输出函数,定义于 <stdio.h> 头文件中:

功能 函数 失败返回值
int getchar(void)
int getc(FILE *stream)
int fgetc(FILE *stream)
单字符输入 EOF
char *fgets(char *s, int size, FILE *stream)
char *gets_s( char *str, rsize_t n)
字符串输入 NULL
int ungetc(int c, FILE *stream) 放回文件流 EOF
int putchar(int c)
int putc(int c, FILE *stream)
int fputc(int c, FILE *stream)
单字符输出 EOF
int puts(const char *s)
int fputs(const char *s, FILE *stream)
字符串输出 EOF

注意

  • 以上函数都将字符从缓冲区中的 unsigned char 类型转换为 int 类型。

  • getchar()它的返回值是 int 类型而非 char 类型,值得注意。所以为什么可以使用char c = getchar() ?

那么这是否会造成 EOF 不能被识别,而是被看作字符呢?
#define EOF (-1) // unsigned char转换成int后,值都大于等于0

宽字符输入输出定义于 <wchar.h> 头文件中,区别在于其一个字符的长度不同。C 语言中有一种类型wchar_t , 其长度取决于编译器:

  • msvc 中,wchar_t 16 位,即unsigned short类型。
  • gcc/Clang 中,wchar_t 32 位,即unsigned int类型。

在使用宽字符前,需要在字符和字符串前加上L前缀,例如L'c'L"string",未加前缀的字符和字符串为窄字符。 对于宽字符/字符串读写函数,其使用方法与窄字符类似。

功能 函数 失败返回值
int getwchar(void)
int getwc(FILE *stream)
int fgetwc(FILE *stream)
单字符输入 EOF
char *fgetws(char *s, int size, FILE *stream) 字符串输入 NULL
int ungetwc(int c, FILE *stream) 放回文件流 EOF
int putwchar(int c)
int fputwc(int c, FILE *stream)`
单字符输出 EOF
int fputws(const char *s, FILE *stream) 字符串输出 EOF

格式化输入输出

在标头<stdio.h><wchar.h>中,定义了一系列格式化输入输出函数,这些函数可以用于读写格式化的数据。

格式字符串

格式字符串const char *format由普通多字节字符(除了 %)和转换指示构成,前者被复制到输出流而无更改。每个转换指示拥有下列格式:

  • 引入的 % 字符
  • ( 可选 ) 一或多个修改转换行为的标签:
    • - : 转换结果在域内左校正(默认为右校正)
    • +: 有符号转换的符号始终前置于转换结果(默认结果前置负号仅当它为负)
    • 空格:若有符号转换的结果不以符号开始,或为空,则前置空格于结果。若存在 + 标签则忽略空格。
    • # :进行替用形式的转换。准确的效果见下表,其他情况下行为未定义。
    • 0:对于整数和浮点数转换,使用前导零代替空格字符填充域。对于整数,若显式指定精度,则忽略此标签。对于其他转换,使用此标签导致未定义行为。若存在 - 标签则忽略 0
  • ( 可选 ) 指定最小域宽的整数值或 * 。若有要求,则结果会以空格字符(默认情况)填充,在右校正时于左,左校正时于右。使用 * 的情况下,以一个额外的int类型参数指定宽度。若参数值为负数,则它导致指定 - 标签和正域宽(注意:这是最小宽度:决不被截断值
  • ( 可选 ) 后随整数或 * 或两者皆无的 . 指示转换的精度。在使用 * 的情况下,精度由额外的 int 类型参数指定。若此参数的值为负数,则它被忽略。若既不使用数字亦不使用 * ,则精度采用零。精度的准确效果见下表。
  • ( 可选 ) 指定参数大小的长度修饰符
  • 转换格式指示符

printf

printf()中转换说明的意义

转换说明实际上是在将以二进制存存储在计算机中的值转换成一些列字符便于显示。转换实际上是一种翻译说明。

例如:d:将一个有符号的十进制整数转换成十进制数。

下面讨论几种转换说明的情况:

  1. 转换不匹配

    转换说明与待打印的值不匹配可能导致数据错误或者出现未定义的行为。

    e.g.

    wrong_cnv.c
    #include <stdio.h>
    #define num 336
    #define b 65618
    int main(void)
    {
        printf("num as short and unsigned short:  %hd %hu\n", num,num);
        printf("-num as short and unsigned short: %hd %hu\n", -num,-num);
        printf("num as int and char: %d %c\n", num, num);
        printf("b as int, short, and char: %d %hd %c\n",b, b, b);
    
        float n1 = 3.0;
        double n2 = 3.0;
        long n3 = 2000000000;
        long n4 = 1234567890;
        printf("%.1e %.1e %.1e %.1e\n", n1, n2, n3, n4);
        printf("%ld %ld\n", n3, n4);
        printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);
        return 0;
    }
    
    该程序的输出结果为 (由于实现是未定义的,部分结果因系统和编译器而异):
        num as short and unsigned short:  336 336
        -num as short and unsigned short: -336 65200
        num as int and char: 336 P
        b as int, short, and char: 65618 82 R
        3.0e+00 3.0e+00 2.0e+09 1.2e+09
        2000000000 1234567890
        0 1074266112 0 1074266112
    
    你能解释为什么使用%ld 输出 long 类型的值时,也会出现错误吗?

  2. printf()的返回值

    printf()函数返回打印的字符数,若出现错误则返回负值。

  3. 打印长的字符串

    不能在双引号括起来的字符串中间断行。

    e.g.

    longstrg.c
    #include <stdio.h>
    int main(void)
    {
        printf("Here's one way to print a ");
        printf("long string.\n");
        printf("Here's another way to print a \
        long string.\n");
        printf("Here's the newest way to print a "
        "long string.\n"); /* ANSI C */
        /*
        printf("Here's wrong way to print a
        long string.\n");
        */
        return 0;
    }
    

scanf()

在前面我们介绍了printf()中的格式化字符串和转换说明,scanf()中也有类似的格式化字符串和转换说明。

复杂的使用方法请查阅 scanf

下面介绍一些scanf()的特性:

  1. scanf()要将读取的内容存储到对应变量地址中,而不是变量名。因此,scanf()的参数应该是变量的地址。

    e.g.
    scanf.c
    #include <stdio.h>
    #include <stdio.h>
    int main(void)
    {
        int a;
        float b;
        char str[30];
        int c[10];
    
        scanf("%d %f", &a, &b); // 读取整数和浮点数
        scanf("%s", str);       // 读取字符串
        scanf("%d", c);        // 读取整数数组
        return 0;
    }
    
  2. scanf() 函数,除了 %c 以外的转换说明,都不会读取空白字符(空格、制表符和回车

    典型问题:换行符问题

    e.g.

    confusing_scanf.c
    #include <stdio.h>
    int main(void)
    {
        char c[100] = {0};
        scanf("%s\n",&c);
        printf("%s",c);
    }
    
  3. scanf()中的输入过程

    假设scanf()根据%d转换读取一个整数,scanf()函数每次读取一个字符,跳过所有的空白字符,直到遇到第一个非空白字符才开始读取。scanf()希望找到一个数字字符或者一个符号,若找到,则会继续寻找下一个数字字符或者符号,直到遇到一个非数字字符,此时scanf()会将读取的字符放回输入流中,然后将读取的字符转换成整数。

    如果第一个非空白字符不是数字或者正负号,scanf()会停在那里,并把字符放回输入中,不会把值赋给相应变量。C 语言规定了在第 1 个出错的地方停止读取输入。

    如果使用%s转换说明,scanf()会跳过空白开始读取除空白以外的所有字符,直到遇到空白字符之后将空白符重新放回输入后结束。

  4. 格式字符串中的普通字符

    scanf()中格式化字符串中的空白意味着跳过下一个输入项前的所有空白。

    除了%c其他转换说明都会自动跳过待输入值前面的所有空白。

    以下代码有什么区别?

    scanf("%d %d", &n,&m);
    scanf("%d%d", &n,&m);
    
    scanf("%c", &c);
    scanf(" %c", &c);
    
  5. scanf()的返回值

    读取成功则返回读取的项数。没有读取任何项则返回 0。当scanf()读取到文件末尾则返回EOF

实例

安全的输入函数

写一个安全的输入函数

众所周知,C 语言的gets函数是一个非常不安全的函数 ( 请参考系统知识拾遗 1),在 C11 中被弃用,转为gets_s()gets()极有可能造成缓冲区溢出,所以我们需要写一个安全的输入函数。

s_gets.c
char * s_gets(char* st, int n)
{
    char * ret_val;
    int i = 0;

    ret_val = fgets(st, n, stdin);
    if (ret_val)
    {
        while (st[i] != '\n' && st[i] != '\0)
            i++;
        if (st[i] == '\n')
            st[i] = '\0';
        else //must have words[i] == '\0'
            while (getchar() != '\n')
                continue;
    }
    return ret_val;
}

这个函数来自《C Primer Plus,也是书中 13 章以后一直使用的输入函数。这个函数示范了如何使用安全的 fgets() 函数,并将输入统一,并处理剩余的字符。

在涉及行的输入时,一定要注意统一行结尾的形式。这对于换行符,特别是文件结尾处薛定谔的换行符,有很大作用。

检查输入

可以使用scanf() 的返回值来检查输入是否正确。

文件编码与文件输入输出

基础概念

什么是文件?

简单的说,文件是在硬盘上的一段已命名的储存区。

对操作系统而言,文件更加复杂。文件是具有符号名的,在逻辑上具有完整意义的一组相关信息项的序列。文件还包括了一些额外数据,便于操作系统确定文件的种类。

c_file.c
#include<stdio.h>
#include<string.h>

int main(int argc, char *argv[]){
    const char *str = "ckc-agc programming lec3";

    FILE *fp1 = fopen("test1.txt", "wb+");
    FILE *fp2 = fopen("test2.txt", "w+");
    fprintf(fp1, "%s\n%s\n", str, argv[0]);
    fprintf(fp2, "%s\n%s\n", str, argv[0]);
    fwrite(str, sizeof(char), strlen(str), fp1);

    // fseek(fp1, 0, SEEK_SET);
    // fprintf(fp1, "%s\n", "SEEK_SET");
    fseek(fp1, 0, SEEK_END);
    fprintf(fp1, "%s\n", "SEEK_END");

    rewind(fp1);
    fprintf(fp1, "%s\n", "rewind");

    fclose(fp1);
    fclose(fp2);
}
file c_file.c
file c_file.o
file c_file
objdump -h -s -d c_file.o
objdump -h -s c_file

对于 C 语言来说,C 把文件看作连续的字节,每个字节都能被单独读取。这与 UNIX 环境中的文件结构相对应。便于其他操作系统,C 提供两种文件模式:文本模式和二进制模式。

文件编码

字符编码是把字符集中的字符编码为指定集合中某一对象(例如:bit ,以便文本在计算机中存储和通过通信网络的传递。

常见的文件字符集 / 编码:

  • ASCII 字符集: 8bit(一个字节),能表示的最大的整数就是 255(2^8-1=255)。ASCII 编码,占用 0 - 127 用来表示大小写英文字母、数字和一些符号,这个编码表被称为 ASCII 编码,比如大写字母 A 的编码是 65,小写字母 z 的编码是 122。还对一些如'\n','\t','#','@'等字符进行了编码。
  • base 家族编码 :(base16/base32/base64/base58/base85/base100) 使用对应数量的字符编码二进制数据
  • GB2312 编码: 16bit(2 个字节),适用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆。中国大陆几乎所有的中文系统和国际化的软件都支持 GB2312。
  • GBK 编码: 16bit(2 个字节),兼容 GB2312,收录 21003 个汉字,共有 23940 个码位,与 Unicode 组织的 Unicode 编码完全兼容。
  • Unicode 字符集: 为了统一所有文字的编码,Unicode 应运而生,这是一种所有符号的编码。Unicode 是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换 Unicode 字符集的编码范围是 0x0000 - 0x10FFFF , 可以容纳一百多万个字符,每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫码点
  • UTF(UTF-8,UTF-16 ): Unicode Transformation Format,可变长度编码,通常使用 1~4 字节为每个字符编码,兼容 ASCII 编码,这是一种 Unicode 的一种转换格式。

utf-8

现在在内存中通常以 UTF-16(Windows API 大量偏好 UTF-16 LE 编码的字符串作为参数 ) 来储存,保存到文件中替换为 UTF-8 等格式,可以压缩空间。

文本模式和二进制模式

所有文件的内容都以二进制形式存储。但是,如果文件最初使用二进制编码的字符 ( 例如 ASCII ) 表示文本,该文件就是文本文件,其中包含文本内容。如果文件中的二进制值代表及其语言代码或数值数据,该文件是二进制文件,其中包含二进制内容。

Unix 使用同一种文件格式处理文本文件和二进制文件的内容。Unix 目录中有一个统计文件大小的计数,程序可以根据该计数确定是否读到文件尾。

C 语言提供两种访问文件的途径:二进制模式和文本模式。在二进制模式中,程序可以访问文件的每个字节,而在文本模式中,程序所见的内容和文件的实际内容不同。程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射成 C 模式。

mode

这告诉我们文本和二进制模式不能随意混用,否则可能会出现正确性上的问题。

I/O 级别

事实上我们除了选择处理文件的模式,还能够选择 I/O 的级别。底层 I/O 使用操作系统提供的 I/O 服务。标准高级 I/O 使用 C 库的标准包和stdio.h头文件定义。标准高级 I/O 使用底层 I/O 服务,但是它们提供了更高级别的接口。因为无法保证所有的操作系统都适用相同的底层 I/O 模型,C 标准只支持标准 I/O 包。

标准文件

C 程序会自动打开 3 个文件,它们被称为标准输入(standard input、标准输出(standard output)和标准错误输出(standard error output。在默认情况下,标准输入是系统的普通输入设备,通常为键盘;标准输出和标准错误输出是系统的普通输出设备,通常为显示屏。

通常,标准输入为程序提供输入,它是 getchar()scanf()使用的文件。程序通常输出到标准输出,它是putchar()puts()printf()使用的文件。前文提到的重定向把其他文件视为标准输入或标准输出。标准错误输出提供了一个逻辑上不同的地方来发送错误消息。例如,如果使用重定向把输出发送给文件而不是屏幕,那么发送至标准错误输出的内容仍然会被发送到屏幕上。这样很好,因为如果把错误消息发送至文件,就只能打开文件才能看到。

理解:标准 I/O 中用 FILE(流)表示一个打开的文件

流和文件

C 标准是这样描述两种流的:

  • 文本流:组成文本行的有序字符序列,每一行由零个或多个字符加上标志结束的换行符组成。
    • 实现定义:最后一行是否需要换行符、换行符正前面的空格是否在读取时出现等。
  • 二进制流:字符的有序序列。

关于文件动作:

  • 打开文件:流打开一个文件,就是与该文件关联
    • 创建文件会丢弃内容。
    • 和流相关的文件定位符定位在文件起始位置。
    • 附加模式下定位位置由实现决定。
  • 关闭文件:关闭文件会释放与该文件相关的资源。

类型和宏

<stdio.h> 中有如下与文件相关的类型和宏:

  • FILE 对象类型,记录控制所需要的所有信息,包括:文件定位符、指向相关缓冲的指针、错误指示符和文件结束符。
    • 不要试图探索 FILE * 数据对象的内部,即使实现给出了某些可见域。不要修改对象、不要拷贝对象并代替使用,因为实现假定知道流数据对象的所有地址
  • fpos_t 对象类型,含有唯一指定文件中每个位置所需的所有信息。
  • stderr, stdin, stdout都是 FILE* 类型的表达式。
  • EOF 展开为一个负的整值常量。
  • NULL
  • SEEK_CUR 文件当前位置
  • SEEK_END 文件结束位置
  • SEEK_SET 文件开始位置

函数

文件操作

  • int remove (const char *filename)
  • int rename (const char *old, const char *new)
  • FILE *tmpfile(void)
  • char * tmpnam(char *s)

文件访问

  • int fclose(FILE *stream)
    • 关闭文件,清空流(清空流就是传递缓冲数据,释放缓冲。
  • int fflush(FILE *stream)
    • 立即写入(要求上一次操作是输出
  • FILE *fopen(const char *filename, const char *mode)
    • 打开文件,返回文件流指针。
  • FILE *freopen(const char *filename, const char *mode, FILE *stream)
  • void setbuf(FILE *stream, char *buf)
    • 定义流 stream 应如何缓冲。该函数应在与流 stream 相关的文件被打开时,且还未发生任何输入或输出操作之前被调用一次
  • int setvbuf(FILE *stream, char *buf, int mode, size_t size)
    • 创建供标准 I/O 函数替换使用的缓冲区

mode 字符串的含义

flag meaning
r read
w write (new or cut)
a append (new or append)
b binary
+ update

注意上面词语的含义,update append 能写入的范围应该是不同的。

  • append 模式下所有写操作强制加到文件结束处。
  • update 模式下,若不调用文件定位函数,输入输出不一定相互紧跟。

对于 UNIX 这种只有一种文件类型的系统,带 b 与否的模式是相同的。 C11 中新增了 x,带该字母的写模式打开存在文件会失败(相当于加了保护),且允许独占。

格式化输入 / 输出

fprintf,fscanf,vfprintf,vprintf,vsprintf

字符输入 / 输出

fgetc,fgets,fputc,fputs,getc,getchar,gets_s/(gets),putc,putchar,puts,ungetc

其中,fgetc()getc() 这类函数的不同是后者可能被实现为宏。

直接 I/O

  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
  • size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

文件定位

  • int fseek(FILE *stream, long int offset, int whence)
    • whence 可以是 SEEK_SETSEEK_CURSEEK_END
    • 第二个参数表示偏移量
    • 将文件类比为数组,单位是字节数
  • long int ftell(FILE *stream)
    • 距文件开始处的字节数
  • void rewind(FILE *stream)

其他

以下两个函数用于处理更大型的文件(long 无法表示的偏移值)

  • int fgetpos(FILE *stream, fpos_t *pos)
  • int fsetpos(FILE *stream, const fpos_t *pos)