跳转至

『C语言教程』7. 文件

T20:50:00+08:00

〇、引入

假如有一个函数 void printValue() ,怎么在这个函数中访问到外部的一个变量呢? 我们使通过一个指针变量来访问的:

C
1
2
3
void printValue(int *x) {
    printf("%d\n", *x);
}

在调用此函数并传入变量地址作为参数时,在函数内部即可通过指针 x 来访问到对应的变量。

文件也是如此,在程序外部有一个文件,我们也可以通过一个类似的 FILE* 型指针变量来访问到文件。

一、输入/输出流 与 FILE *

<stdio.h>

C
FILE *fp; // 声明了一个名为 fp 的 FILE* 类型指针变量

每一个 输入/输出流(I/O Stream) 都和一个外部的物理设备相关联(文件、标准输入流、打印机、端口号等等), 可以被一个 FILE* 类型的指针变量来表示,并可通过此指针来访问、控制 输入/输出流(I/O Stream)

二、打开与关闭文件

2.1 fopen() 函数

C
FILE *fopen( const char *filename, const char *mode ); // until C99

C89,C99,C11,C17等都是C语言的标准,新的标准会包含新的内容和修改,可以理解为语言的版本。

数字表示发表的年份,比如C11是2011年发表的标准。

标准C 指 C89.

参数:

  • filename 文件流要关联到的文件的名
  • mode 文件访问模式

| 文件访问标志 | 含义 | 描述 | > | ------ | ---- | ---- | > | r | read | 打开一个存在的文件用来 读 (若不存在则返回空指针) | > | w | write | 创建一个文件用来 写(若文件存在则覆盖) | > | a | append | 打开/创建一个文件用来 在尾部追加写 | > | r+ | read extended | 打开一个存在的文件用来 读/写 (若不存在则返回空指针) | > | w+ | write extended | 创建一个文件用来 读/写(若文件存在则覆盖) | > | a+ | append extended| 打开/创建一个文件用来 读/在尾部追加写 |

Text Only
1
> 上述每一个都可以添加 `b` 标志来表示以二进制方式打开

2.2 fclose() 函数

C
int fclose( FILE *stream );

用完了要记得关闭,取消对文件的占用。

Closes the given file stream. Any unwritten buffered data are flushed to the OS. Any unread buffered data are discarded.

Whether or not the operation succeeds, the stream is no longer associated with a file, and the buffer allocated by setbuf or setvbuf, if any, is also disassociated and deallocated if automatic allocation was used.

The behavior is undefined if the value of the pointer stream is used after fclose returns. 成功返回0,否则返回EOF(-1)。

三、读写文件

2.1 一些函数

先来看普通的读入、输出函数:

  • 读入:

  • int getchar(void);

    stdin 中读入一个字符。

    成功返回读到的字符,失败返回 EOF

  • char *gets( char *str );

    stdin 中读入字符串,存入str中(以换行或文件结束为终止)。

    成功则返回读到的字符串,失败则返回 NULL 空指针。

  • int scanf( const char *format, ... );

    略。

  • 输出:

  • int putchar( int ch );

    stdout 中写入一个由 ch 转化为的字符。

    成功则返回写入的字符,失败则返回 EOF

  • int puts( const char *str );

    stdout 中写入字符串 str 和一个 \n

    成功则返回一个非负值,失败则返回 EOF

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

    略。

对于从特定的 输入/输出流 读入、输出,有以下函数:

C
1
2
3
4
5
6
7
int fgetc( FILE *stream );
int fputc( int ch, FILE *stream );
char *fgets( char *str, int count, FILE *stream ); // until C99
int fputs( const char *str, FILE *stream ); // until C99

int fscanf( FILE *stream, const char *format, ... ); // until C99
int fprintf( FILE *stream, const char *format, ... ); // until C99

这里 fgets() 比较特殊,与 gets() 有个较大区别,多了一个参数 count 表示每次读入最多读入的字符数量,即每次读入不只截止于换行、文件末尾,还会截止于读入 count 个字符后,并且不会忽略 \n

同时 fputs()puts() 有个区别,不会在末尾添加 \n

两个新的函数:

C
size_t fread( void *buffer, size_t size, size_t count, FILE *stream ); // until C99

参数:

  • buffer 存储所读到的数据的地址
  • size 读到每一个对象的字节数
  • count 读的对象数
  • stream 要执行读操作的流

返回值:

Number of objects read successfully, which may be less than count if an error or end-of-file condition occurs.

If size or count is zero, fread returns zero and performs no other action.

C
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream ); // until C99

参数同理,buffer 为要写入的数据起始地址。

返回值同理,The number of objects written successfully, which may be less than count if an error occurs.

If size or count is zero, fwrite returns zero and performs no other action.

2.2 基础读与基础写 r w

C
#include <stdio.h>

int main() {
    FILE *srcFile, *dstFile;
    srcFile = fopen("src.txt", "r");
    if (srcFile == NULL) { // 空指针,表明 fopen 打开的文件不存在
        puts("File src.txt is not exist.");
        exit(1); // 以 1 为返回值退出程序(相当于main函数中return 1)
    }
    dstFile = fopen("dst.txt", "w");

    char c;
    while ((c = getc(srcFile)) != EOF) {
        putc(c, dstFile);
    }

    return 0;
}

getc 在读到文件末尾后再读就会会失败,会返回 EOF (End Of File)。

EOF 是一个常量,值为 -1,被定义在在 <stdio.h> 中:

C
#define EOF (-1)

2.3 二进制读与二进制写 rb wb

C
#include <stdio.h>

int main() {
    FILE *srcFile, *dstFile;
    srcFile = fopen("src.txt", "rb");
    if (srcFile == NULL) { // 空指针,表明 fopen 打开的文件不存在
        puts("File src.txt is not exist.");
        exit(1); // 以 1 为返回值退出程序(相当于main函数中return 1)
    }
    dstFile = fopen("dst.txt", "wb");

    char buffer[1024], cnt;
    while ((cnt = fread(buffer, 1, 1024, srcFile)) != 0) {
        fwrite(buffer, 1, cnt, dstFile);
    }

    return 0;
}

2.4 二进制读写与普通读写的区别

单位不同:

  • 普通读写:字符
  • 二进制读写:字节

数据不同:

  • 普通读写:字符
  • 二进制读写:二进制数据

比如同样存储一个数字 -2147483648

  • 普通读写:
Text Only
'-' '2' '1' '4' '7' '4' '8' '3' '6' '4' '8'

最终写入的数据为

Text Only
00101101 00110010 00110001 00110100 00110111 00110100 00111000 00110011 00110110 00110100 00111000

对应 ASCII 码的二进制表示

  • 二进制读写:

最终写入的数据为

Text Only
11111111 11111111 11111111 11111111

-2147483648 的二进制表示

2.5 理解文件位置指针

文件位置指针决定了我们在使用诸如 fgetc() , fgets() 等函数从文件中读取内容时读取到的是哪个位置的数据,每一次读取时均是从文件位置指针所在位置开始读,读完后将文件位置指针移动到之后的位置。

假如一个文件有如下内容(方括号代表一个字符(以文本模式打开)或一个字节(以二进制模式打开)):

Text Only
[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
那么文件位置指针最开始则位于 0 处:
Text Only
[*] [ ] [ ] [ ] [ ] [ ] [ ] [ ]
在调用一次 fgetc() 后会获取到文件位置指针所在位置的字符,并将其后移一个字符:
Text Only
[ ] [*] [ ] [ ] [ ] [ ] [ ] [ ]
当文件位置指针最终会抵达末尾之后的位置
Text Only
[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] *
此后再进行读入,将不能成功读入到东西,文件位置指针也不会移动。

(此处的内容其实就是EOF)

Text Only
                                 *
[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] EOF

同理,如果是如下的字符串,文件位置指针在读后会移动到字符串尾后一个字符的位置:

Text Only
1
2
3
4
5
6
7
8
*
Hello World!

     *
Hello world!

            *
Hello world!

2.5.1 ftell() 函数

C
long ftell( FILE *stream );

获得 stream 流的文件位置指针位置

以内容如下的文件为例:

Text Only
Hello World~
NB!

运行此程序,清晰地显示出了读入前后文件位置指针的变化:

C
#include <stdio.h>

int main() {
    FILE *fp = fopen("txtt.txt", "rb");

    char c, cnt;
    while (1) {
        printf("[%d] ", ftell(fp));

        if ((c = fgetc(fp)) == EOF) break;

        if (c == '\r') printf("\\r");
        else if (c == '\n') printf("\\n");
        else           putchar(c);

        printf(" [%d]\n", ftell(fp));
    }

    return 0;
}

输出如下:

Text Only
[0] H [1]
[1] e [2]
[2] l [3]
[3] l [4]
[4] o [5]
[5]   [6]
[6] W [7]
[7] o [8]
[8] r [9]
[9] l [10]
[10] d [11]
[11] ~ [12]
[12] \r [13]
[13] \n [14]
[14] N [15]
[15] B [16]
[16] ! [17]
[17]

windows 下换行为 \r \n,而 linux 中为 \n

  • \r回车,光标回到本行行首
  • \n换行,光标移到下一行

这一点细节要时常注意,经常因为忽略 \r 的存在而产生很多错误。

再看下面这个以字符串读入的例子:

C
#include <stdio.h>

int main() {
    FILE *fp = fopen("txtt.txt", "r");

    char str[1024], cnt;
    while (1) {
        printf("[%d] ", ftell(fp));

        if (fscanf(fp, "%s", str) == EOF) break;

        printf("%s [%d]\n", str, ftell(fp));
    }

    return 0;
}

输出如下:

Text Only
1
2
3
4
[0] Hello [5]
[5] World~ [12]
[12] NB! [17]
[17]

这样应该就理解了文件位置指针的概念了吧。

(刚才文件的文件位置指针对应关系如下:

Text Only
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
H e l l o   W o r l d  ~  \r \n  N  B  !

2.5.2 rewind()

C
void rewind( FILE *stream );

stream 流的 文件位置指针 移回起始位置

2.5.3 fseek()

C
int fseek( FILE *stream, long offset, int origin );

设置文件读取指针的位置

参数: - stream 文件流 - offset 偏移量 - origin 偏移量起始位置 - SEEK_SET 文件开始 - SEEK_CUR 当前位置 - SEEK_END 文件结尾

如果文件流是在 二进制模式 打开的,新的位置就确切的是 offset 所给的字节数造成的偏移。

注意:二进制模式下不能使用 SEEK_END

Binary streams are not required to support SEEK_END, in particular if additional null bytes are output.

If the stream is open in text mode, the only supported values for offset are zero (which works with any origin) and a value returned by an earlier call to ftell on a stream associated with the same file (which only works with origin of SEEK_SET).

If the stream is wide-oriented, the restrictions of both text and binary streams apply (result of ftell is allowed with SEEK_SET and zero offset is allowed from SEEK_SET and SEEK_CUR, but not SEEK_END).

In addition to changing the file position indicator, fseek undoes the effects of ungetc and clears the end-of-file status, if applicable.

If a read or write error occurs, the error indicator for the stream (ferror) is set and the file position is unaffected.

2.6 基础写与二进制写 a ab

a 模式打开文件时,文件位置指针位于起始位置(0),但是一旦执行任何写操作,会立即将其移动到文件末尾,并开始写入。

2.7 基础读写 r+ w+ a+

方法 若文件不存在 若文件存在 初始文件位置指针的位置
r+ 返回 空指针 NULL --- 0
w+ 创建文件 覆盖 文件 0
a+ 创建文件 --- 0

输入输出共用一个文件位置指针。

  • a+ 当文件位置指针不在末尾时(文件位置指针所在位置已有内容时),输出函数都会成功,但不会覆盖掉文件中得分内容;当文件位置指针在末尾时,才能够实质性的向文件中写入内容。

  • w+ 当文件位置指针不在末尾时(文件位置指针所在位置已有内容时),输出会替换掉那个位置的内容。

例:

C
#include <stdio.h>

int main() {
    FILE *writeExtended = fopen("test.txt", "w+");
    
    fputs("EDG Niu Biiiiiii\n", writeExtended);
    fputs("Champion!!!!!!!", writeExtended);    
    
    fseek(writeExtended, 0L, SEEK_SET);
    
    fputs("Peng Ge", writeExtended);
    
    fclose(writeExtended);   
    
    return 0;
}
    

文件内容为:

C
Peng Ge Biiiiiii
Champion!!!!!!!
  • r+w+ 的区别在于,当文件存在时不会覆盖文件。

2.8 二进制读写 rb+ wb+ ab+

由题可知,不难发现,显然得到,略。

发挥你的主观能动性吧。

评论