流的概念

流是磁盘或其他外围设备中存储的数据的源点或终点。尽管在一些系统中(如在著名的UNIX系统中),文本流和二进制流是没有差别的,但是标准库中还是提供了这两种流。

文本流是由文本行组成的序列,每一行有 0个或多个字符并以 \n 结尾。在某些环境中也许需要把文本流转换成其他表示形式(例如把 \n 映射成回车符和换行符)或从其他表示形式转换过来。二进制流是未经处理的字节组成的序列,这些字节记录着内部数据,具有如下性质:如果在同一系统中把写出的二进制流再读入进来,读入的和写出的内容是完全相同的。

通过打开一个流可以将该流与一个文件或设备关联起来,这一关联可以通过关闭流而终止。打开一个文件将返回一个指向 FILE 类型对象的指针,该指针中记录有用于控制该流的所有必要的信息。在不会引起歧义性的情况下,我们在下文中将不再区分“文件指针”和“流”。

在程序开始执行时,stdin、stdout 和 stderr 这三个流已经被打开。

格式化输入输出

格式化输出

printf 类函数用于提供格式化的输出转换。

格式化字符

格式化字符串由两种类型的对象组成:普通字符(它们被拷贝到输出流)与转换规格说明(它们决定参数的转换和输出格式)。每个转换规格说明均以字符 % 开头,以转换字符结束。

printf转换说明的可选项

选项 描述 举例
# 八进制前面加0(转换字符为o),十六进制前面加0x(转换字符为x)或0X(转换字符为X)。 printf("%#x", 0xff)打印0xffprintf("%x", 0xff)打印ff
- 格式化后的内容居左,右边可以留空格。 见下面的例子
宽度 用一个整数指定格式化后的最小长度,如果格式化后的内容没有这么长,可以在左边留空格,如果前面指定了-号就在右边留空格。宽度有一种特别的形式,不指定整数值而是写成一个 * 号,表示取一个int型参数作为宽度。 printf("-%10s-", "hello")打印-␣␣␣␣␣hello-printf("-%-*s-", 10, "hello")打印-hello␣␣␣␣␣-
. 用于分隔上一条提到的最小长度和下一条要讲的精度。 见下面的例子
精度 用一个整数表示精度。精度也可以不指定整数值而是写成一个 * 号,表示取下一个int型参数作为精度。 printf("%.4s", "hello")打印hellprintf("-%6.4d-", 100)打印-␣␣0100-printf("-%*.*f-", 8, 4, 3.14)打印-␣␣3.1400-
字长 对于整型参数,hh、h、l、ll分别表示是char、short、long、long long型的字长,至于是有符号数还是无符号数则取决于转换字符;对于浮点型参数,L表示long double型的字长。 printf("%hhd", 255)打印-1

printf 常用的转换字符

% 与转换字符这二者之间依次可以有以下转换字符:

格式码 参数类型:转换效果
d, i int: 有符号十进制表示法
o int;无符号八进制表示法(无前导 0)
x, X int;无符号十六进制表示法(无前导 0 X和0x),对0x用abcdef,对0X用ABCDEF
u int;无符号十进制表示法
c int;单个字符,转换为 unsigned char类型后
s char *;输出字符串直到遇到 \0或者已达到由精度指定的字符数
f double;形如[-] mmm.ddd的十进制浮点数表示法,d的数目由精度确定。缺省精度为6位,精度为0时不输出小数点
e, E double;形如[-]m.d ddddde±xx或[-] m .dddd ddE±xx的十进制表示法。d的数目由精度确定,缺省精度为6位。精度为0时不输出小数点
g, G double;当指数值小于-4或大于等于精度时,采用 %e%E格式;否则采用 %f 的格式。尾部的0与小数点不打印
p void *;输出指针值(具体表示与实现有关)
n int *;到目前为止以此格式调用 printf 输出的字符的数目将被写入到相应参数中。
% 不进行参数转换;输出符号 %

fprintf

fprintf函数用于按照 format 说明的格式把参数列表中参数的内容进行转换,并写入stream指向的流。返回值是实际写入的字符数。若出错则返回一个负值。

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

printf

printf(...) 函数等价于 fprintf(stdout, ...)

1
int printf(const char *format, ...)

sprintf

sprintf 函数与 printf 函数的功能基本相同,但输出到数组 s 中,并以 \0 结束。 s必须足够大,以便能装下输出结果。该函数返回实际输出的字符数,不包括 \0

1
int sprintf(char *str, const char *format, ...);

snprintf

sprintf并不打印到文件,而是打印到用户提供的缓冲区str中并在末尾加’\0’,由于格式化后的字符串长度很难预计,所以很可能造成缓冲区溢出,用snprintf更好一些,参数size指定了缓冲区长度,如果格式化后的字符串长度超过缓冲区长度,snprintf就把字符串截断到size-1字节,再加上一个’\0’写入缓冲区,也就是说snprintf保证字符串以’\0’结尾。snprintf的返回值是格式化后的字符串长度(不包括结尾的’\0’),如果字符串被截断,返回的是截断之前的长度,把它和实际缓冲区中的字符串长度相比较就可以知道是否发生了截断。

1
int snprintf(char *str, size_t size, const char *format, ...);

vprintf, vfprintf 与 vsprintf

vprintf、vfprintf 与 vsprintf 这几个函数与对应的 printf 函数等价,但参数列表由 arg 代替。 arg由宏 va_start 初始化,由 va_arg 调用。

1
2
3
vprintf(const char *format, va_list arg)
vfprintf(FILE *stream, const char *format, va_list arg)
vsprintf(char *s, const char *format, va_list arg)

示例:实现格式化打印错误的err_sys函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include
#include
#include
#include
#include
#define MAXLINE 80
void err_sys(const char *fmt, ...)
{
int err = errno;
char buf[MAXLINE+1];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, MAXLINE, fmt, ap);
snprintf(buf+strlen(buf), MAXLINE-strlen(buf), ": %s", strerror(err));
strcat(buf, "\n");
fputs(buf, stderr);
va_end(ap);
exit(1);
}
int main(int argc, char *argv[])
{
FILE *fp;
if (argc != 2) {
fputs("Usage: ./a.out pathname\n", stderr);
exit(1);
}
fp = fopen(argv[1], "r");
if (fp == NULL)
err_sys("Line %d - Open file %s", __LINE__, argv[1]);
printf("Open %s OK\n", argv[1]);
fclose(fp);
return 0;
}

格式化输入

scanf 类函数用于提供格式化输入转换。

格式化字符

格式字符串 format 通常包含有用于指导输入转换的转换规格说明。格式字符串中可以包含:

  • 空格或制表符,它们将被忽略。
  • 普通字符(“%”除外),与输入流中下一个非空白的字符相匹配。
  • 转换规格说明,由一个 %、一个赋值屏蔽字符 *(任选)、一个用于指定最大字段宽的数(任选)、一个用于指定目标字段的字符 (h、l或 L )(任选)以及一个转换字符组成。

转换规格说明决定了输入字段的转换方式。通常把结果保存在由对应参数指向的变量中。然而,如果转换规格说明中包含有赋值屏蔽字符“*”,例如% *s,那么就跳过对应的输入字段,不进行赋值。输入字段是一个由非空白符组成的字符串,当遇到空白符或到达最大字段宽(如果有的话)时,对输入字段的读入结束。这意味着 scanf 函数可以跨越行的界限来读入其输入,因为换行符也是空白符(空白符包括空格、横向制表符、纵向制表符、换行符、回车符和换页符)。

转换字符说明了输入字段的解释含义,对应的参数必须是指针。合法的转换符如下表所示。

字符 输入数据;参数类型
d 十进制整数; int *
i 整数;int * 。该整数可以是以 0开头的八进制数,也可以是以 0x 或 0X 开头的十六进制数
o 八进制整数(可以带或不带前导 0);int *
u 无符号十进制整数; unsigned int *
x 十六进制整数(可以带或不带前导 0x 或 0X);int *
c 字符;char * 。按照字段宽的大小把读入的字符保存在指定的数组中,不加入字符 \0。字段宽的缺省值为 1。在这种情况下,不跳过空白符 ;如果要读入下一个非空白符,使用 %1s
s 由非空白符组成的字符串(不包含引号); char*。该参数指针指向一个字符数组,该字符数组有足够空间来保存该字符串以及在末尾添加的 \0
efg 浮点数;float *。float 浮点数的输入格式为:一个任选的正负号,一串可能包含小数点的数字和一个任选的指数字段。指数字段由字母 e 或 E 以及后跟的一个可能带正负号的整数组成
p printf("%p")函数调用输出的指针值; void *
n 将到目前为止此调用所读的字符数写入参数; int * 。
[...] 不读入输入字符。不增加转换项目计数用方括号括起的字符集中的字符来匹配输入,以找到最长的非空字符串; char *。在末尾添加字符\0。格式 []...] 表示字符集中包含字符 ]
[^...] 用不在方括号括起的字符集中的字符来匹配输入,以找到最长的非空字符串; char *。在末尾添加字符\0。格式 [ ^ ]. ..] 表示字符集中包含字符 ]
% 字面值%,不进行赋值

fscanf

1
int fscanf(FILE *stream, const char *format, ...)

fscanf 函数用于在格式串 format 的控制下从流stream中读入字符,把转换后的值赋给后续各个参数,在此每一个参数都必须是一个指针。当格式串 format 结束时函数返回。如果到达文件的末尾或在参数被转换前出错,那么该函数返回 EOF;否则返回实际被转换并赋值的输入项的数目。

scanf

scanf(...)函数等价于fscanf(stdin, ...)

1
int scanf(const char* format, ...)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main(void){
char str[] = "hello world";
int num;
float value;
scanf("%s", str);
scanf("%d", &num);
scanf("%f", &value);
printf("%s\n", str);
printf("%d\n", num);
printf("%f\n", value);
return 0;
}

sscanf

sscanf(s, ...)函数与scanf (... )等价,不同的是前者的输入字符来源于 s。

1
int sscanf(char *s,const *format, ... )

文件输入输出

fopen

1
FILE *fopen(const char *filename, const char *mode)

fopen函数用于打开以filename所指内容为名字的文件,返回与之相关联的流。如果打开操作失败,那么返回 NULL。

访问方式mode的合法值如下:

用途
“r” 打开文本文件用于读。
“w” 创建文本文件用于写,并删除已存在的内容(如果有的话)。
“a” 添加;打开或创建文本文件用于在文件末尾写。
“r+” 打开文本文件用于更新(即读和写)。
“w+” 创建文本文件用于更新,并删除已存在的内容(如果有的话)。
“a+” 添加、打开或创建文本文件用于更新和在文件末尾写。

后三种方式允许对同一文件进行读和写。在读和写的交替过程中,必须调用 fflush 函数或文件定位函数。如果方式值在上述字符之后再加上 “b” ,如 “rb” 或 "w+b "等,那么表示文件是二进制文件。文件名 filename 长度的最大值为 FILENAME_MAX 个字符。一次最多可打开 FOPEN_MAX 个文件。

在打开一个文件时如果出错,fopen将返回NULL并设置errno,errno稍后介绍。在程序中应该做出错处理,通常这样写:

1
2
3
4
if ( (fp = fopen("/tmp/file1", "r")) == NULL) {
printf("error open file /tmp/file1!\n");
exit(1);
}

freopen

1
FILE *freopen(const char *filename, const char *mode, FILE *stream)

freopen 函数用于以参数 mode 指定的方式打开参数 filename 指定的文件,并使该文件与参数 stream 指定的流相关联。它返回指向 stream 的指针;若出错则返回 NULL。freopen 函数一般用于 stdin、stdout 和 stderr 等标准流的重定向。

fflush

用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作。

1
int fflush(FILE *stream)

对输出流, fflush 函数用于将已写到缓冲区但尚未写出的全部数据都写到文件中。对输入流,其结果是未定义的。如果写的过程中发生错误则返回 EOF ,否则返回 0。fflush(NULL)函数用于刷新所有的输出流。

C标准库的 I/O 缓冲区

C标准库的I/O缓冲区

C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时,不同类型的缓冲区具有不同的特性。

  • 全缓冲:如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。
  • 行缓冲:如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。
  • 无缓冲:用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

fclose

1
int fclose(FILE *stream)

fclose函数用于刷新 stream 的全部未写出数据,丢弃任何未读的缓冲区内的输入数据并释放自动分配的缓冲区,最后关闭流。若出错则返回 EOF,否则返回 0。

fopen调用应该和fclose调用配对,打开文件操作完之后一定要记得关闭。如果不调用fclose,在进程退出时系统会自动关闭文件,但是不能因此就忽略fclose调用,如果写一个长年累月运行的程序(比如网络服务器程序),打开的文件都不关闭,堆积得越来越多,就会占用越来越多的系统资源。

关于fopen、fgetc、fputc、fclose几个函数的底层实现可以参考[Unbuffered I/O](/wiki/kernel/Unbuffered I/O)一节的内容

字符输入输出

fgetc

fgetc 函数用于以 unsigned char 类型返回 stream 流中的下一个字符(转换为 int 类型)。如果到达文件的末尾或发生错误,则返回 EOF。

1
int fgetc(FILE *stream)

对于fgetc函数的使用有以下几点说明:

  • 要用fgetc函数读一个文件,该文件的打开方式必须是可读的。
  • 系统对于每个打开的文件都记录着当前读写位置在文件中的地址(或者说距离文件开头的字节数),也叫偏移量(Offset)。当文件打开时,读写位置是0,每调用一次fgetc,读写位置向后移动一个字节,因此可以连续多次调用fgetc函数依次读取多个字节。
  • fgetc成功时返回读到一个字节,本来应该是unsigned char型的,但由于函数原型中返回值是int型,所以这个字节要转换成int型再返回,那为什么要规定返回值是int型呢?因为出错或读到文件末尾时fgetc将返回EOF,即-1,保存在int型的返回值中是0xffffffff,如果读到字节0xff,由unsigned char型转换为int型是0x000000ff,只有规定返回值是int型才能把这两种情况区分开,如果规定返回值是unsigned char型,那么当返回值是0xff时无法区分到底是EOF还是字节0xff。如果需要保存fgetc的返回值,一定要保存在int型变量中,如果写成unsigned char c = fgetc(fp);,那么根据c的值又无法区分EOF和0xff字节了。注意,fgetc读到文件末尾时返回EOF,只是用这个返回值表示已读到文件末尾,并不是说每个文件末尾都有一个字节是EOF(根据上面的分析,EOF并不是一个字节)。

fputc

fputc函数用于把字符 c(转换为 unsigned char类型)输出到流 stream 中。它返回所写的字符,若出错则返回EOF。

1
int fputc(int c, FILE *stream)

对于fputc函数的使用也要说明几点:

  • 要用fputc函数写一个文件,该文件的打开方式必须是可写的(包括追加)。
  • 每调用一次fputc,读写位置向后移动一个字节,因此可以连续多次调用fputc函数依次写入多个字节。但如果文件是以追加方式打开的,每次调用fputc时总是将读写位置移到文件末尾然后把要写入的字节追加到后面。

getc

getc函数等价于 fgetc,不同之处为:当 getc 函数被定义为宏时,它可能多次计算 stream 的值。

1
int getc(FILE *stream)

getchar

getchar函数等价于 getc(stdin)。

1
int getchar(void)

putc

putc 函数等价于 fputc,不同之处为:当 putc 函数被定义为宏时,它可能多次计算 stream 的值。

1
int putc(int c, FILE *stream)

putchar

putchar© 函数等价于 putc(c, stdout)。

1
int putchar(int c)

ungetc

ungetc用于把字符 c(转换为 unsigned char类型)写回到流 stream 中,下次对该流进行读操作时,将返回该字符。对每个流只能写回一个字符,且此字符不能是 EOF。ungetc函数返回被写回的字符;如果发生错误,那么它返回 EOF。

1
int ungetc(int c, FILE *stream)

字符串输入输出

fgets

fgets函数用于读入最多 n-1 个字符到数组 s 中。当遇到换行符 \n 时,把换行符保留在数组s中,读入不再进行。数组 s以 \0 结尾。 fgets 函数返回数组 s,如果到达文件的末尾或发生错误,则返回 NULL。

1
char *fgets(char *s, int n, FILE *stream)

对于fgets来说,\n 是一个特别的字符,而 \0 并无任何特别之处,如果读到 \0 就当作普通字符读入。如果文件中存在 \0 字符(或者说0x00字节),调用fgets之后就无法判断缓冲区中的 \0 究竟是从文件读上来的字符还是由fgets自动添加的结束符,所以fgets只适合读文本文件而不适合读二进制文件,并且文本文件中的所有字符都应该是可见字符,不能有 \0

fputs

fputs函数用于把字符串 s(不包含字符 \n)输出到流 stream 中;它返回一个非负值,若出错则返回EOF。

1
int fputs(const char *s, FILE *stream)

gets

gets函数用于把下一个字符串读入到数组 s 中,并把读入的换行符替换为字符 \0 。它返回数组s,如果到达文件的末尾或发生错误则返回 NULL。

1
char *gets(char *s)

Never use gets()。gets函数的存在只是为了兼容以前的程序,我们写的代码都不应该调用这个函数。gets函数的接口设计得很有问题,就像strcpy一样,用户提供一个缓冲区,却不能指定缓冲区的大小,很可能导致缓冲区溢出错误,这个函数比strcpy更加危险,strcpy的输入和输出都来自程序内部,只要程序员小心一点就可以避免出问题,而gets读取的输入直接来自程序外部,用户可能通过标准输入提供任意长的字符串,程序员无法避免gets函数导致的缓冲区溢出错误,所以唯一的办法就是不要用它。

puts

puts函数用于把字符串 s 和一个换行符输出到 stdout。如果发生错误,那么它返回 EOF;否则返回一个非负值。

1
int puts(const char *s)

直接输入输出

fread

fread函数用于从输入流 stream 中读入最多 nobj 个长度为 size 的对象到 ptr 指向的数组中,并返回所读入的对象数,此返回值可能小于 nobj。实际执行状态可用函数 feof 或 ferror 得到。

1
size_t fread(void *ptr, size_t size, size_t nobj, FILE *stream)

fwrite

fwrite 函数用于把 ptr 所指向的数组中 nobj 个长度为 size 的对象输出到流 stream 中,并返回被输出的对象数。如果发生错误,则返回一个小于 nobj 的值。

1
size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *stream)

示例

writerec.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* writerec.c */
#include <stdio.h>
#include <stdlib.h>
struct record {
char name[10];
int age;
};
int main(void)
{
struct record array[2] = { {"Ken", 24}, {"Knuth", 28} };
FILE *fp = fopen("recfile", "w");
if (fp == NULL) {
perror("Open file recfile");
exit(1);
}
fwrite(array, sizeof(struct record), 2, fp);
fclose(fp);
return 0;
}

readrec.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* readrec.c */
#include <stdio.h>
#include <stdlib.h>
struct record {
char name[10];
int age;
};
int main(void)
{
struct record array[2];
FILE *fp = fopen("recfile", "r");
if (fp == NULL) {
perror("Open file recfile");
exit(1);
}
fread(array, sizeof(struct record), 2, fp);
printf("Name1: %s\tAge1: %d\n", array[0].name, array[0].age);
printf("Name2: %s\tAge2: %d\n", array[1].name, array[1].age);
fclose(fp);
return 0;
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
$ gcc writerec.c -o writerec
$ gcc readrec.c -o readrec
$ ./writerec
$ od -tx1 -tc -Ax recfile
000000 4b 65 6e 00 00 00 00 00 00 00 00 00 18 00 00 00
K e n \0 \0 \0 \0 \0 \0 \0 \0 \0 030 \0 \0 \0
000010 4b 6e 75 74 68 00 00 00 00 00 00 00 1c 00 00 00
K n u t h \0 \0 \0 \0 \0 \0 \0 034 \0 \0 \0
000020
$ ./readrec
Name1: Ken Age1: 24
Name2: Knuth Age2: 28

文件定位函数

fseek

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

fseek函数用于给与stream流相关的文件定位,随后的读写操作将从新位置开始。对于二进制文件,此位置被定位在由 whence 开始的 offset 个字符处。 whence的值可能为:

  • SEEK_SET:文件开始处
  • SEEK_CUR:当前位置
  • SEEK_END:文件结束处。

offset可正可负,负值表示向前(向文件开头的方向)移动,正值表示向后(向文件末尾的方向)移动,如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸,从原来的文件末尾到fseek移动之后的读写位置之间的字节都是0。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
if ( (fp = fopen("textfile","r+")) == NULL) {
perror("Open file textfile");
exit(1);
}
if (fseek(fp, 10, SEEK_SET) != 0) {
perror("Seek file textfile");
exit(1);
}
fputc('K', fp);
fclose(fp);
return 0;
}

ftell

1
long ftell(FILE *stream)

ftell 函数用于返回与 stream 流相关的文件的当前位置。在出错时返回 -1L。

rewind

rewind(fp)函数等价于 fseek(fp,0L,SEEK_SET) 与 clearerr(fp) 这两个函数顺序执行的效果。

1
void rewind(FILE *stream)

fgetpos

1
int fgetpos(FILE *stream, fpos_t *ptr)

fgetpos函数用于把 stream流的当前位置记录在 *ptr中,供随后的 fsetpos 函数调用时使用。若出错则返回一个非 0 值。

fsetpos

fsetpos函数用于将流 stream 的位置定位在 ptr 所指向的位置。 * ptr的值是在函数 fgetpos 调用时记录下来的。若出错则返回一个非 0 值。

1
int fsetpos(FILE *stream, const fpos_t *ptr)

错误处理

当发生错误或到达文件末尾时,标准库中的许多函数将设置状态指示符。这些状态指示符可被显式地设置和测试。另外,很多系统函数在错误返回时将错误原因记录在libc定义的全局变量errno中,每种错误原因对应一个错误码。请查阅errno(3)的Man Page了解各种错误码,errno在头文件errno.h中声明,是一个整型变量,所有错误码都是正整数。

clearerr

clearerr 函数用于清除与流 stream 相关的文件结束指示符和错误指示符。

1
void clearerr(FILE *stream)

feof

feof函数用于在与stream流相关的文件结束指示符被设置时返回一个非0值。

1
int feof(FILE *stream)

ferror

ferror函数用于在与stream相关的文件出错指示符被设置时返回一个非0值。

1
int ferror(FILE *stream)

perror

perror(s) 函数用于输出字符串 s 以及与全局变量 errno 中的整数值相对应的出错信息,具体出错信息的内容依赖于实现。该函数的功能类似于 fprintf(stderr, "%s: %s\n", s, "出错信息")

1
void perror(const char *s)

如果在程序中打印错误信息时直接打印errno变量,打印出来的只是一个整数值,仍然看不出是什么错误。比较好的办法是用perror或strerror函数将errno解释成字符串再打印。

示例

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp = fopen("abcde", "r");
if (fp == NULL) {
perror("Open file abcde");
exit(1);
}
return 0;
}

说明

如果文件abcde不存在,fopen返回-1并设置errno为ENOENT,紧接着perror函数读取errno的值,将ENOENT解释成字符串No such file or directory并打印,最后打印的结果是Open file abcde: No such file or directory。虽然perror可以打印出错误原因,传给perror的字符串参数仍然应该提供一些额外的信息,以便在看到错误信息时能够很快定位是程序中哪里出了错,如果在程序中有很多个fopen调用,每个fopen打开不同的文件,那么在每个fopen的错误处理中打印文件名就很有帮助。

errno是一个全局变量,很多系统函数都会改变它,所以一个系统函数错误返回后应该马上检查errno,在检查errno之前不能再调用其它系统函数