开头将文章中代码所在的环境介绍下:
[Mac-10.7.1 Lion Intel-based]
Q: 看到stdio.h中有这么多关于输入或者输出的函数,怎么会这么多?
A: 其实基本的函数不多,不过从易于上层使用的角度,又被封装出很多特定情况的函数接口,所以才显得很多。
Q: 比如关于字符操作的函数有getc, fgetc, getch, getche还有getchar,它们有什么区别吗?
A: 一个重要的区别是getc, fgetc, getchar是标准函数,而getch和getche不是标准规定的。不是标准,就意味着它的实现可能在不同平台有不一样的表现。正如getch一样,windows平台被包含在conio.h头文件中,运行到getch,可能需要等待输入,当有输入时就会运行过去不需要再按回车; getche在windows下和getch类似,不过多了最后的字母e,它表示echo,即在输入后会回显输入的字符。不过它们在mac平台的表现却不大一样,这里不做具体介绍。
现在介绍下getc, fgetc和getchar.用man getc得到如下信息:

可以发现,getc基本等同于fgetc,getchar是getc的特殊形式,是从标准输入获取字符。
Q: getch和getche接收了输入即可继续运行,而getchar函数当缓冲区中没有数据时,需要输入字符最终回车才能继续执行,这是不是意味着getch和getche是不带缓冲的,而getc, fgetc和getchar是带缓冲的?
A: 是的。按照标准来说,getc和fgetc在不涉及交互的时候是全缓冲模式(在缓冲区大小满后提交数据),如果是涉及到交互(如标准输入),那么是行缓冲;而getchar就行缓冲模式(回车会提交输入缓冲区数据,不过如果缓冲区已经有数据了就不需要等待回车了).
Q: 既然fgetc是接收输入的字符,返回值用char或者unsigned char不就行了,为什么用int呢?
A: 这个主要是因为文件结束或者读写文件出错的标志被规定成EOF,也就是-1导致的。unsigned char根本取不到-1这个值,而如果用char做返回值的话,它无法分辨0xFF字符和EOF,因为这两个数值都被char认为是-1,所以它也不能作为返回值。
举个例子吧:
使用如下代码向一个文件中只写入0xFF字符,然后保存:
- #include <stdio.h>
-
- int main (int argc, const char * argv[])
- {
- FILE *fp = fopen("test", "w");
- if(fp)
- {
- fputc(0xFF, fp);
- fclose(fp);
- }
-
- return 0;
- }
运行完,用hexdump确认此文件中的数据为0xFF:

然后使用如下代码读取test文件中的数据,并打印出来:
- #include <stdio.h>
-
- int main (int argc, const char * argv[])
- {
- FILE *fp = fopen("test", "r");
- if(fp)
- {
- char ch;
-
- printf("open test ok\n");
- while((ch = fgetc(fp)) != EOF)
- {
- printf("%d", ch);
- }
-
- printf("read test end\n");
- fclose(fp);
- }
-
- return 0;
- }
运行后可以发现输出结果如下,这就意味着fgetc执行是遇到0xFF时被当做了EOF而导致结束了。

Q: 上面提到getchar等同于getc(stdin), stdin到底是什么?
A: [Mac-10.7.1 Lion Intel-based]
stdio头文件中:
- #define stdin __stdinp
- #define stdout __stdoutp
- #define stderr __stderrp
- extern FILE *__stdinp;
- extern FILE *__stdoutp;
- extern FILE *__stderrp;
可以看到,它们只是FILE *类型的一个变量,对于任何一个应用程序,使用c运行时库都会默认自动为应用程序创建这3个文件句柄。
Q: 还有关于字符串输入的gets函数,听说使用它进行输入有风险的,如何表现的?
A: 因为它没有对于输入数据大小进行确定,也就是可能导致输入的数据过多覆盖了不该覆盖的数据导致出问题。
举个例子:
- #include <stdio.h>
-
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- char ch = 'a';
- char buf[4];
- char *ret = gets(buf);
- if(ret != NULL)
- {
- PRINT_STR(ret)
- }
- PRINT_CH(ch)
-
- return 0;
- }
运行:
如果输入3个字符hel后回车,得到的结果是正常的:

如果输入了4个字符hell后回车,结果如下:

可以看到ch字符的数据已经不正常了;
如果输入5个字符hello后回车,结果如下:

可以看到ch字符已经被覆盖为hello中的o了。这和刚刚输入4个字符的方式都是发生了缓冲区溢出,也就是超过了缓冲区buf的大小,覆盖了其它数据。
至于ch为什么是字符o, 这需要明白:ch和buf都被保存在栈中,且ch和buf相邻,如果栈是从高到低,那么ch保存在高地址,数据hello将前4个字节保存在buf中,最后一个o就被保存在了ch所在的地址区域里面。
Q: 既然这样,那么使用什么可以更好地避免缓冲区溢出呢?
A: 可以使用fgets函数,它的一个参数就是缓冲区大小。
- char *fgets(char * restrict str, int size, FILE * restrict stream);
如果读取成功,函数返回读取字符串的指针;
如果开始读取直接遇到文件结尾,返回NULL,缓冲区数据和原来保持一致;
如果读取出错,返回NULL, 缓冲区数据不确定;
Q: fgets如果返回NULL,可能是直接遇到文件结束或者获取出错,怎么区分呢?
A: 这就需要如下两个函数了:feof和ferror.
- int ferror(FILE *stream);
如果遇到文件尾,那么feof返回非0的数值,否则返回0.
如果遇到文件操作错误,那么ferror返回非0的数值,否则返回0.
Q: 做一个测试吧,从一个文件中不断读取数据,然后打印出来。
A: 首先,先写个脚本,向一个文件中写入100个字符,为依次重复10次0123456789.
-
-
- i=0
- j=0
-
- while [ $i -lt 100 ];
- do
- let "j = i % 10"
- echo -n $j >> numbers
- let "i = i + 1"
- done
执行它,会在同目录下找到一个numbers的文件,使用vi打开确认一下内容:

现在使用fgets函数将此文件的数据读出来:
- #include <stdio.h>
- #include <string.h>
-
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- FILE *fp = fopen("numbers", "r");
- if(fp)
- {
- char buf[11] = {0};
- while(fgets(buf, sizeof(buf), fp) != NULL)
- {
- if(!ferror(fp))
- PRINT_STR(buf)
- else
- PRINT_STR("ferror happens...")
-
- memset(buf, 0, sizeof(buf));
- }
- fclose(fp);
- }
-
- return 0;
- }
这里,为了分便显示结果,缓冲区被设置大小为11,最后一个字节保存结束符\0, 前10个字符保存0~9.
运行结果:

Q: fgets会自动在缓冲区结尾加上\0作为结束符是吧?
A: 是的。这和strcpy在这个方面是一致的。不过,fgets也有自己的特点,正如gets一样,遇到回车符,它会提前结束;如果遇到回车符,而且缓冲区还没填满,那么回车符会被填充到缓冲区中,最后再加上\0作为结束符。
看个例子:
- #include <stdio.h>
- #include <string.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- char buf[3];
- char *ret = fgets(buf, sizeof(buf), stdin);
- if(ret)
- {
- PRINT_D(strlen(buf))
- PRINT_STR(buf)
- }
-
- return 0;
- }
运行:
输入h然后回车, 结果:

可以发现回车字符被保存在了buf中.它和gets在这方面不一致,这是需要注意的。
Q: 如果不希望到换行就结束,也可以指定缓冲区大小,该怎么办?
A: 可以使用fread(不过它是适用于二进制读的函数,用于文本方式可能在某些情况下出错,需要小心).
- size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
ptr: 表示读入数据保存的地址;
size: 表示每组数据的大小;
nitems: 表示读入数据的组数;
stream: 表示文件指针;
返回值: 成功读取数据的组数;如果返回NULL,可能是直接遇到文件尾或者读取出错,需要用feof和ferror来区分;当然,如果读取不成功,返回值会比传入的参数nitems要小。
这个函数使用的时候要注意:正因为它是用于二进制读的,它不会在读取ok后自动为缓冲区最后加上\0作为结束符,这个要小心;如果第二个和第三个参数互换了,那么返回值也会发生相应改变,这也需要小心。
举个例子吧:
- #include <stdio.h>
- #include <string.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- char buf[3] = {0};
- size_t ret = fread(buf, sizeof(buf) - 1, 1, stdin);
- if(ret == 1)
- {
- PRINT_STR(buf)
- }
-
- return 0;
- }
可以看到,开始就将buf初始化为0了,fread的第二个参数是buf总大小减去1的数值,因为后面把读取的数据当成char *字符串,最后一个字节需要保存为\0.
第三个参数为1表示1组数组,第二个参数表示每组数据大小为sizeof(buf) - 1. 最后一个参数表示从标准输入stdin读取。
需要注意:正如之前说过,文件读写为交互设备时,是属于行缓冲,需要输入回车来提交缓冲区。
运行:

输入he并回车,可以看到打印了he.
如果输入h并回车,那么效果如下:

可以发现回车字符被保存在了buf中。
在这里不用太小心,如果输入多余2个字符,不会发生缓冲区溢出,因为fread有参数已经标志了缓冲区大小。
Q: 上面是关于文件输入,文件输出的fputc, putc, putchar是不是也和fgetc, getc, getchar的关系类似?
A: 是的,使用man putc可以看到它们之间的关系。

Q: 标准中也没有putch和putche么?
A: 是的,实际上在mac系统默认下也没发现它们的非标准实现。
Q: 字符串输出的fputs和puts有什么区别么?
A:

可以看到,fputs将字符串向指定文件输出,但是并不会自动加上换行字符;而puts会自动加上换行字符。
另外,fputs返回EOF表示操作失败,返回非负数表示成功;所以判断fputs是否成功最好使用if(fputs(xxxx) != EOF)或者if(fputs(xxx) == EOF)来表示。
如下代码:
- #include <stdio.h>
- #include <string.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- int ret = fputs("hello", stdout);
- if(ret != EOF)
- {
- PRINT_D(ret)
- }
-
- return 0;
- }
执行:

可以看到,输出的hello后面并没有换行。
puts函数返回值同fputs.
puts的使用:
- #include <stdio.h>
- #include <string.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- int ret = puts("hello");
- if(ret != EOF)
- {
- PRINT_D(ret)
- }
-
- return 0;
- }
运行结果:

可以看到puts输出后自动加了换行。
Q: 文件输入和输出可以像printf和scanf一样有格式化操作么?
A: 当然可以了,printf和scanf只是文件输入输出的一个特例而已。
fprintf函数和fscanf函数可以实现此功能。
- int fprintf(FILE * restrict stream, const char * restrict format, ...);
- int fscanf(FILE *restrict stream, const char *restrict format, ...);
可以看到,它们和printf和scanf很相似,除了第一个参数表示文件指针,对于printf也就是fprintf(stdout, xxx)的特例;scanf也就是fscanf(stdin, xxx)的特例。
不过,fprintf还可以用文件指针stderr表示错误输出,它是不带缓冲的输出。
fprintf的返回值表示成功输出的字符字节个数,printf的返回值和它一致。
fscanf返回值表示成功输入的变量个数。
这里不做更多说明了。
Q: 一直提到的文件句柄,它到底是什么?
A:
- typedef struct __sFILE {
- unsigned char *_p;
- int _r;
- int _w;
- short _flags;
- short _file;
- struct __sbuf _bf;
- int _lbfsize;
-
-
- void *_cookie;
- int (*_close)(void *);
- int (*_read) (void *, char *, int);
- fpos_t (*_seek) (void *, fpos_t, int);
- int (*_write)(void *, const char *, int);
-
-
- struct __sbuf _ub;
- struct __sFILEX *_extra;
- int _ur;
-
-
- unsigned char _ubuf[3];
- unsigned char _nbuf[1];
-
-
- struct __sbuf _lb;
-
-
- int _blksize;
- fpos_t _offset;
- } FILE;
可以看到它封装了操作文件的缓冲区、当前位置等信息,这也能很好地体现带缓冲的事实。比如,fopen函数,它和open函数的一大区别就是是否使用用户层缓冲。
Q: fopen和open的关系是什么样子的?
A: open是POSIX标准,也是基于Unix系统的系统调用。fopen是C语言标准,它的内部当然必须调用open系统调用才能完成真正功能。
Q: 给个使用open, read, write, close函数的例子吧。
A:
首先使用echo命令创建一个内容为hello的文件,文件名为test.
然后编写如下代码:
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- char buf[32] = {0};
- ssize_t ret;
- int file = open("test", O_RDONLY);
-
- if(file < 0)
- {
- perror("open file error");
- return -1;
- }
- ret = read(file, buf, sizeof(buf));
- if(ret > 0)
- PRINT_STR(buf)
-
- close(file);
-
- return 0;
- }
运行,结果为:

Q: 我想在文件开头插入一个字符,用如下的代码怎么不行?
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- FILE *fp = fopen("test", "r+");
- if(!fp)
- {
- perror("fopen error");
- return -1;
- }
- fseek(fp, 0, SEEK_SET);
- if(fputc('a', fp) == EOF)
- perror("fputc error");
-
- fclose(fp);
-
- return 0;
- }
本来test文件里面的内容是hello\n,执行上面的程序后,为什么不是ahello\n,而是aello\n ?
A: 这个问题的原因在于fputc, fputs, fwrite等写操作的函数均为覆盖写,不是插入写导致的。如果需要将文件读出来,将文件开头插入字符a,然后将读出的数据全部追加到文件后面,这样才行。
Q: 一直听到二进制文件和文本文件,它们到底有什么区别?
A: 计算机底层最终只能处理所谓的二进制文件,文本文件只是人们将文件的内容做了抽象得到的一种特殊的二进制文件。不过,它们是有一定区别的,可能在某些时候,不同的平台,它们的表现也不同。但是从理论上来说,二进制文件可能更节省空间,这一方面是因为二进制文件是文件最终的形式,另一方面是因为可以用二进制的1比特表示数值1,甚至整形1,但是用文本方式,却至少用1字节。另外,不同平台对于回车换行,CR与LF, "\r\n"的表示方式不太一致,导致对于它的文本解读和二进制形式有不一致的地方。在mac上,文本形式的换行用'\n'表示,而在windows上,文本换行是\r\n, 所以有时会发现同一个文件在不同操作系统下的换行显示会有不同。不过,二进制和文本方式读取也不是水火不相容的,正如下面的例子,显示的结论没什么不同:
假设test文件中保存hello\n,
文本方式读:
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- int ch;
- FILE *fp = fopen("test", "rt");
- if(!fp)
- {
- perror("fopen error");
- return -1;
- }
- while ((ch = fgetc(fp)) != EOF)
- {
- printf("ch:%c %d\n", ch == '\n' ? 'N' : ch, ch);
- }
-
- fclose(fp);
-
- return 0;
- }
二进制读:
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- int ch;
- FILE *fp = fopen("test", "rb");
- if(!fp)
- {
- perror("fopen error");
- return -1;
- }
- while ((ch = fgetc(fp)) != EOF)
- {
- printf("ch:%c %d\n", ch == '\n' ? 'N' : ch, ch);
- }
-
- fclose(fp);
-
- return 0;
- }
二个应用程序的运行结果均为:

显示为N的地方为换行字符。
Q: 有的时候,想要直接将一个整形数据以二进制形式写入文件中,有什么更分便的调用方式?
A: putw可以实现你要的功能。
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- FILE *fp = fopen("test", "w");
- if(!fp)
- {
- perror("fopen error");
- return -1;
- }
- putw(32767, fp);
-
- fclose(fp);
-
- return 0;
- }
运行完后,用hexdump test查看test文件的十六进制形式,可以发现32767对应int类型大小的数据已经保存在test中了。
当然,如果不嫌麻烦,可以使用如下的代码实现上面类似的功能:
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- int n = 32767;
- char *pn = (char *)&n;
- int i;
- FILE *fp = fopen("test", "w");
- if(!fp)
- {
- perror("fopen error");
- return -1;
- }
- for(i = 0; i < sizeof(n); ++i)
- {
- fputc(*(pn + i), fp);
- }
-
- fclose(fp);
-
- return 0;
- }
Q: 之前看过好多关于文件打开方式的字符串,"rb", "w+"等等,到底怎么很好地明白它们的打开方式?
A: 其实很简单。以r开头的方式打开,如果文件不存在必然失败;打开方式含有b即表示是二进制方式,如果没有b或者有t,那就说明是文本方式;如果打开方式中有+,那么表示可读可写;
Q: 很多时候,如果读取输入缓冲区的数据读多了,想放回去怎么办?
A: 可以使用ungetc函数。
举个例子吧:
从标准输入读取一个十进制整数和最后一个分隔符(不是十进制整数字符),然后打印这个整数和最后的分隔符。
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <ctype.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- int n = 0;
- int ch;
- char end_ch;
-
- while ((ch = getchar()) != EOF && isdigit(ch))
- {
- n = 10 *n + (ch - '0');
- }
- if(ch != EOF)
- {
- ungetc(ch, stdin);
- }
- end_ch = getchar();
-
- PRINT_D(n)
- PRINT_CH(end_ch)
-
- return 0;
- }
运行时,输入1234;然后换行,

可以发现,ungetc会将;字符重新放回输入流中,是的end_ch被赋值为;字符;
如果没有使用ungetc的话:

可以发现,end_ch被赋值为输入1234;后面的换行了。
Q: 关于输入输出重定向已经听说了很多了,c语言中有函数可以实现重定向吗?
A: 是的, freopen函数可以实现这个作用。
- FILE *freopen(const char *restrict filename, const char *restrict mode, FILE *restrict stream);
第一个参数表示需要重定向的文件位置;第二个参数表示重定向方式,如"w"为写方式;第三个参数表示被重定向的文件句柄;
下面有个代码将展示如何将stdout重定向到test文件,然后再恢复stdout的输出;
代码如下:
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <ctype.h>
-
- #define PRINT_D(intValue) printf(#intValue" is %lu\n", (intValue));
- #define PRINT_CH(ch) printf(#ch" is %c\n", (ch));
- #define PRINT_STR(str) printf(#str" is %s\n", (str));
-
- int main (int argc, const char * argv[])
- {
- FILE *file;
- int fd;
- fpos_t pos;
-
-
- fprintf(stdout, "1111");
-
-
- fflush(stdout);
- fgetpos(stdout, &pos);
- fd = dup(fileno(stdout));
-
-
- file = freopen("test", "w", stdout);
- if(!file)
- {
- perror("freopen error");
- return -1;
- }
-
-
- fprintf(stdout, "hello");
-
-
- fflush(stdout);
- dup2(fd, fileno(stdout));
- close(fd);
- clearerr(stdout);
- fsetpos(stdout, &pos);
-
-
- fprintf(stdout, "2222");
-
- return 0;
- }
运行结果:

可以看出,最开始的标准输出和最后的标准输出都正常显示在屏幕,第二个标准输出因为被重定向输出到了test文件中;查看test文件的内容:

不过,如果是在windows平台,恢复stdout的方式和上面的代码可能不一致。
xichen
2012-5-12 18:08:31
|