跳到主要内容

标准IO

1. 基本概念

Linux 标准 IO 是指在 Linux 操作系统中用于处理输入和输出的三个流:标准输入流(stdin)、标准输出流(stdout)和标准错误流(stderr)。

  • 标准输入流(stdin)通常从键盘读取输入,并将其发送到程序。

  • 标准输出流(stdout)通常用于将程序的输出发送到屏幕。

  • 标准错误流(stderr)通常用于将程序的错误信息发送到屏幕。

设备(流)文件描述符(int)文件指针(FILE *)
标准输入设备(键盘)STDIN_FILENO:0stdin
标准输出设备(屏幕)STDOUT_FILEMO:1stdout
标准出错设备(屏幕)STDERR_FILEMO:2stderr

这三个流的处理方式可以通过重定向来改变,例如,可以将标准输出重定向到一个文件中,或者将标准输入重定向到一个文件中读取数据。

总之,Linux 标准 IO 是 Linux 操作系统中用于处理输入和输出的一组标准流,它们可以通过重定向来改变处理方式。

2. 打开文件

fopen() 函数用于在 Linux 系统中打开文件。函数原型为:

FILE *fopen(const char *path, const char *mode);

该函数接受两个参数:

  • path:指定要打开的文件的路径。
  • mode:指定文件打开模式,比如:读、写。

如果打开文件成功,则该函数返回文件指针。否则,返回 NULL。

注意:在使用完文件后,需要使用 fclose() 函数关闭文件。重复打开一个已经打开的文件或者打开不存在的文件可能导致错误。

mode 模式可选的参数有:

mode描述
r打开一个已有的文本文件,允许读取文件。
w打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则该会被截断为零长度,重新写入。
a打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+打开一个文本文件,允许读写文件。
w+打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

3. 关闭文件

fclose() 函数用于在 Linux 系统中关闭文件。函数原型为:

FILE *fclose(FILE *stream);

该函数接受一个参数:

  • stream:指定要关闭的文件的文件指针。

如果关闭文件成功,则该函数返回 0。否则,返回 EOF。

注意:重复关闭一个已经关闭了的文件或者尚未打开的文件是安全的。但是,在使用完文件后,应该尽量及时使用 fclose() 函数关闭文件,以释放系统资源。

4. 判断文件情况

feof() 函数用于判断文件是否已经到达末尾。函数原型为:

int feof(FILE *stream);

该函数接受一个参数:

  • stream:指定要判断的文件的文件指针。

该函数返回一个非零值(true),表示文件已经到达末尾;如果文件还未到达末尾,则返回零(false)。

ferror() 函数用于判断文件是否出现错误。函数原型为:

int ferror(FILE *stream);

该函数接受一个参数:

  • stream:指定要判断的文件的文件指针。

该函数返回一个非零值(true),表示文件出现错误;如果文件没有出现错误,则返回零(false)。

5. 获取或设置文件偏移量

5.1 设置文件偏移量

fseek() 函数用于将文件指针移动到指定的位置。函数原型为:

int fseek(FILE *stream, long offset, int whence);

该函数接受三个参数:

  • stream:指定要移动指针的文件的文件指针。
  • offset:指定要移动的字节数。如果为正数,则文件指针向后移动;如果为负数,则文件指针向前移动。
  • whence:指定文件指针移动的起始位置,可以取以下值:
    • SEEK_SET:文件开头。
    • SEEK_CUR:当前位置。
    • SEEK_END:文件末尾。

fseek() 函数返回 0 表示成功,返回非零值表示失败。

rewind() 函数用于将文件指针移动到文件开头。函数原型为:

void rewind(FILE *stream);

该函数接受一个参数:

  • stream:指定要移动指针的文件的文件指针。

该函数无返回值。

注意:rewind(fp); 相当于 fseek(fp, 0L, SEEK_SE);

5.2 获取文件偏移量

ftell() 函数用于获取文件指针当前位置相对于文件开头的偏移量。函数原型为:

long ftell(FILE *stream);

该函数接受一个参数:

  • stream:指定要获取指针位置的文件的文件指针。

该函数返回文件指针当前位置相对于文件开头的偏移量,如果出现错误,则返回 -1。

下面是一个使用 ftell() 函数获取文件指针当前位置的示例代码:

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

int main(int argc, char *argv[])
{
// 定义文件指针
FILE *fp;

// 使用 fopen() 函数打开文件,并获取文件指针
fp = fopen("test.txt", "w+");
if (fp == NULL) {
// 打开文件失败,输出错误信息
perror("open() file");
exit(EXIT_FAILURE);
}

// 向文件中写入字符
if (fputs("AAAA", fp) == EOF) {
// 写入失败,输出错误信息
perror("fputc() error");
}

// 获取文件指针当前位置
long pos = ftell(fp);
if (pos == -1) {
// 获取失败,输出错误信息
perror("ftell() error");
} else {
// 输出文件指针当前位置
printf("current file position: %ld\n", pos);
}

// 使用 fclose() 函数关闭文件
fclose(fp);

return 0;
}

6. 读写文件(一个字符)

标准IO的读写接口是非常丰富的。

6.1 一个字符的读取

fgetc()\getc()\getchar() 函数用于从文件中读取一个字符。函数原型为:

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

该函数接受一个参数:

  • stream:指定要读取的文件的文件指针。
  • void:无。

该函数返回读取到的字符,如果读取失败或者到达文件末尾,则返回 EOF。

6.2 一个字符的写入

fputc()\putc()\putchar() 函数用于向文件中写入一个字符。函数原型为:

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(void);

该函数接受两个参数:

  • c:指定要写入文件中的字符。
  • stream:指定要写入的文件的文件指针。
  • void:无。

该函数返回写入的字符,如果写入失败,则返回 EOF。

6.3 注意点

  • fgec()\getc()\getchar()返回值是 int,而不是 char,原因是因为他们在出错或者读到文件末尾的时候需要返回一个值为-1的EOF标记,而char型数据有可能因为系统的差异而无法表示负整数。

  • fgec()\getc()\getchar()返回EOF时,有可能是发生了错误,也有可能是读到了文件末尾,可以用feof()\ferror() 函数接口来进一步加以判断。

  • getchar()缺省从标准输入设备读取一个字符,putchar()缺省从标准输出设备输出一个字符。

  • fgetc()\fputc()是函数,getc( )\putc()是宏定义。

  • 两组输入输出函数一般成对地使用,fgetc()fputc()getc()putc( )getchar()putchar()

6.4 示例代码

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

int main(int argc, char *argv[])
{
// 定义文件指针
FILE *fp;

// 使用 fopen() 函数打开文件,并获取文件指针
fp = fopen("test.txt", "w+");
if (fp == NULL) {
// 打开文件失败,输出错误信息
perror("open() file");
exit(EXIT_FAILURE);
}

// 向文件中写入字符
if (fputc('A', fp) == EOF) {
// 写入失败,输出错误信息
perror("fputc() error");
exit(EXIT_FAILURE);
}

// 将文件指针移动到文件开头
if (fseek(fp, 0, SEEK_SET) != 0) {
// 移动文件指针失败,输出错误信息
perror("fseek() error");
exit(EXIT_FAILURE);
}

// 循环读取文件中的每一个字符
int c;
while ((!feof(fp)) && ((c = fgetc(fp)) != EOF)) {
// 判断文件是否出现错误
if (ferror(fp)) {
// 出现错误,输出错误信息
perror("fgetc() error");
break;
}
printf("%c", c);
}

// 判断是否到达文件末尾
if (feof(fp)) {
printf("\nEOF reached.\n");
}

// 使用 fclose() 函数关闭文件
fclose(fp);

return 0;
}

7. 读写文件(一行数据)

标准IO的读写接口是非常丰富的。

7.1 一行数据的读取

fgets()\gets() 函数用于从文件中读取一行字符串。函数原型为:

char *fgets(char *str, int n, FILE *stream);
char *gets(char *str);

该函数接受三个参数:

  • str:指向要存储读取到的字符串的缓冲区的指针。
  • n:指定缓冲区的大小。
  • stream:指定要读取的文件的文件指针。

该函数返回读取到的字符串,如果读取失败或者到达文件末尾,则返回 NULL

7.2 一行数据的写入

fputs() 函数用于将一个字符串写入文件。函数原型为:

int fputs(const char *str, FILE *stream);
int puts(const char *str);

该函数接受两个参数:

  • str:指定要写入文件的字符串。
  • stream:指定要写入的文件的文件指针。

该函数返回写入的字符数,如果写入失败,则返回 EOF

7.3 注意点

  • fgets()fgetc()一样,当其返回NULL时并不能确定究竟是达到文件末尾还是碰到错误,需要用feof( )/ferror()来进一步判断。
  • fgets()每次读取至多不超过size个字节的一行,所谓“一行”即数据至多包含一个换行符'\n'。
  • gets()是一个已经过时的接口,因为他没有指定自定义缓冲区s的大小,这样很容易造成缓冲区溢出,导致程序段访问错误。
  • fgets()fputs()gets()puts() 一般成对使用,鉴于 gets() 的不安全性,一般建议使用前者。

7.4 示例代码

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

#define BUFFER_SIZE 256

int main(int argc, char *argv[])
{
// 打开文件
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
// 文件打开失败,退出程序
perror("fopen() error");
exit(EXIT_FAILURE);
}

// 跳过文件的前 4 个字符
if (fseek(fp, 4, SEEK_SET) != 0) {
perror("fseek() error");
exit(EXIT_FAILURE);
}

// 循环读取文件的每一行
char buffer[BUFFER_SIZE];
while (!feof(fp) && !ferror(fp)) {
// 使用 fgets() 读取文件的一行
if (fgets(buffer, BUFFER_SIZE, fp) != NULL){
// 使用 fputs() 打印读取到的一行
fputs(buffer, stdout);
}
}

// 关闭文件
fclose(fp);

return 0;
}

该程序打开了一个名为 "test.txt" 的文件,然后跳过了文件的前 4 个字符。然后,它使用 fgets() 读取文件的每一行,并使用 fputs() 函数将读取到的内容输出到标准输出。最后,它关闭了打开的文件。

8. 读写文件(若干数据)

8.1 读取若干数据

fread() 函数用于从文件中读取数据。函数原型为:

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

该函数接受四个参数:

  • ptr:指向要存储读取到的数据的缓冲区的指针。
  • size:指定每个数据项的大小,以字节为单位。
  • nmemb:指定要读取的数据项的个数。
  • stream:指定要读取的文件的文件指针。

该函数返回读取的数据项的数量。如果发生错误,则返回一个小于 nmemb 的值。

8.2 写入若干数据

fwrite() 函数用于将数据写入文件。函数原型为:

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

该函数接受四个参数:

  • ptr:指向要写入文件的数据的指针。
  • size:每个数据项的大小(以字节为单位)。
  • nmemb:要写入的数据项的数量。
  • stream:指定要写入的文件的文件指针。

该函数返回写入的数据项的数量,如果写入失败,则返回 0

8.3 注意点

  • 如果fread()返回值小于nmemb时,则可能已达末尾,或者遇到错误,需要借助于feof( )/ferror()来加以进一步判断。
  • 当发生上述第1种情况时,其返回值并不能真正反映其读取或者写入的数据块数,而只是一个所谓的“截短值”,比如正常读取5个数据块,每个数据块100个字节,在执行成功的情况下返回值是5,表示读到5个数据块总共500个字节,但是如果只读到499个数据块,那么返回值就变成4,而如果读到99个字节,那么fread()会返回0。因此当发生返回值小于nmemb 时,需要仔细确定究竟读取了几个字节,而不能直接从返回值确定。

8.4 示例程序

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

#define BUFSIZE 1024

int main(int argc, char *argv[]) {
// 确保程序接收到两个文件名参数
if (argc < 3) {
printf("Usage: %s <src_file> <dest_file>\n", argv[0]);
exit(EXIT_FAILURE);
}

// 以只读模式打开源文件
FILE *src_file = fopen(argv[1], "r");
if (src_file == NULL) {
printf("Failed to open file '%s'\n", argv[1]);
exit(EXIT_FAILURE);
}

// 以写入模式打开目标文件
FILE *dest_file = fopen(argv[2], "w");
if (dest_file == NULL) {
printf("Failed to open file '%s'\n", argv[2]);
fclose(src_file);
exit(EXIT_FAILURE);
}

// 定义缓冲区和读取的字节数
char buffer[BUFSIZE];
size_t bytes_read;

// 循环读取源文件中的数据,直到遇到 EOF
while ((bytes_read = fread(buffer, 1, BUFSIZE, src_file)) > 0) {
// 将读取到的数据写入目标文件
size_t bytes_written = fwrite(buffer, 1, bytes_read, dest_file);

// 如果写入的字节数不等于读取的字节数,则发生错误
if (bytes_written != bytes_read) {
printf("Error writing to file '%s'\n", argv[2]);
fclose(src_file);
fclose(dest_file);
exit(EXIT_FAILURE);
}
}

// 检查读取操作是否出错
if (ferror(src_file)) {
printf("Error reading from file '%s'\n", argv[1]);
fclose(src_file);
fclose(dest_file);
exit(EXIT_FAILURE);
}

// 检查文件结尾标志是否设置
if (feof(src_file)) {
printf("Reached end of file '%s'\n", argv[1]);
}
// 获取文件的当前位置
long cur_pos = ftell(src_file);
printf("Current position in file '%s': %ld\n", argv[1], cur_pos);

// 使用 fseek() 函数设置文件的当前位置
int res = fseek(src_file, 0, SEEK_SET);
if (res != 0) {
printf("Error setting position in file '%s'\n", argv[1]);
}

// 关闭打开的文件
fclose(src_file);
fclose(dest_file);

return 0;
}

该程序接受两个文件名参数,并以只读模式打开源文件,以写入模式打开目标文件。然后,它循环读取源文件中的数据并写入目标文件,直到遇到文件结尾。

9. 标准格式化IO

9.1 格式化写入

fprintf()\printf()\snprintf()\sprintf 函数用于将格式化的字符串写入指定的文件流。这些函数原型为:

int fprintf(FILE *restrict stream, const char *restrict format, ...);
int printf(const char *restrict format, ...);
int snprintf(char *restrict s, size_t n, const char *restrict format, ...);
int sprintf(char *restrict str, const char *restrict format, ...);

这些函数接受的参数:

  • stream:指向要写入的文件流。
  • str:指向要写入的字符串(指向自定义缓冲区)。
  • n:指定要写入的字符数(指定的缓冲区大小)。
  • format:指定格式化的字符串。
  • ...:表示可变的参数列表,用于格式化字符串。

这些函数返回值为已写入的字符数,如果失败则返回一个负数。

9.2 格式化读取

fscanf() 函数用于从指定的文件流中读取格式化的字符串。该函数原型为:

int fscanf(FILE *restrict stream, const char *restrict format, ...);
int scanf(const char *restrict format, ...);
int sscanf(const char *restrict str, const char *restrict format, ...);

这些函数接受的参数:

  • stream:指向要读取的文件流。
  • str:指向要读取的字符串(指向自定义缓冲区)。
  • format:指定格式化的字符串。
  • ...:表示可变的参数列表,用于存储读取的数据。

该函数返回已读取的正确匹配且赋值的数据格式,如果失败则返回一个负数。

9.3 注意点

格式化IO函数中最常用的莫过于printf()和 scanf()了,但从上表中可以看到,他们其实各自都有一些功能类似的兄弟函数可用,使用这些函数需要注意以下几点:

  • fprintf() 不仅可以像 printf() 一样向标准输出设备输出信息,也可以向由 stream 指定的任何有相应权限的文件写入数据。
  • sprintf()snprintf() 都是向一块自定义缓冲区写入数据,不同的是后者第二个参数提供了这块缓冲区的大小,避免缓冲区溢出,因此应尽量使用后者,放弃使用前者。
  • fscanf() 不仅可以像 scanf() 一样从标准输入设备读入信息,也可以从由 stream 指定的任何有相应权限的文件读入数据。
  • sscanf() 从一块由 str 指定的自定义缓冲区中读入数据。

9.4 示例代码

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

int main(int argc, char *argv[])
{
int num = 42;

// 将整数 num 的值写入文件
FILE *fp = fopen("file.txt", "w");
if (fp == NULL) {
perror("fopen() fail");
exit(EXIT_FAILURE);
}

if (fprintf(fp, "%d", num) < 0) {
perror("fprintf() fail");
exit(EXIT_FAILURE);
}
fclose(fp);

// 从文件中读取整数
fp = fopen("file.txt", "r");
if (fp == NULL) {
perror("fopen() fail");
exit(EXIT_FAILURE);
}

if (fscanf(fp, "%d", &num) < 0) {
perror("fscanf() fail");
exit(EXIT_FAILURE);
}
printf("The value read from the file is: %d\n", num);
fclose(fp);

return 0;
}