对C语言的写文件操作fwrite的一个初学者常见误解
当初对C语言的 写文件操作 到底会有什么样的结果很困惑,今天读CSAPP的时候又把这段回忆勾起来了。以下,希望能对如当年我一样的同学们理解fwrite有点帮助。
1.题外话,文件的重要意义
教科书中一般都会提到C语言的文件有这么个特色,它把所有设备都看成相同的东西,并称这是个优势。有的同学可能会奇怪,这有什么可提的。这种优点是教科书的作者和教师们从更早的书里抄来的,更早的书是对当时的情况发表的看法。在UNIX系统和C语言以前,操作系统处于一个更萌芽和早期的状态,对不同的设备的操作都使用专门方法--你可以理解为各有单独的函数。试想,键盘、鼠标、显示器、打印机、磁带、磁盘、磁鼓,所有这些东西都要作读写操作,而读写操作从人类的视角看来如此之像,却使用全然不同的函数和参数。UNIX和C语言改变了这一点,它把所有的设备"抽象"为文件,对所有设备的操作,都用相同的一组函数,即文件读写,来完成。这些各种各样的文件当中,也包括目录,所以目录也是一种文件。网络socket也是一种文件,进程间通信,也是一种文件。当很多不同的东西都统一于文件的时候,它们的个性就抹杀掉了,容易管理和控制多了。
因为吾生也晚,咱们已经习惯于这个格局了,就认为用文件管理所有的东西是天经地义的事情了,所以难以感觉到诸侯割据各自为政时的不方便。
2.问题
当我们读文件的时候,事情相对简单。打开,然后读,然后关闭。我们读到的正是我们期待读到的东西。当我们写文件的时候,情况就不同了。
常见出现的错误是,我们可以有个文件,内容是:
abcdefghijlmn
我们的C代码是:
打开文件, (可能在中间某个位置) 写操作,关闭文件。
执行完C程序以后,我们发现文件的内容不是我们期待的结果。我们原本期待中间某处变成我们修改以后的样,比如:
abcAAfghijlmn。
但是文件的内容却变成了:
^@^@^@AA
3. 原因
造成上面的问题,其原因肯定不是"因为微软的编译器太垃圾了",而是我们没有仔细阅读手册。
我们错误地假想,C语言操作文件流 (流,也是个UNIX语境下的重要概念),就像操作内存里的数组一样,找开文件就是找到数组的头 (起始位置),写操作就是改变数组中某处的元素。
但是事情并不是这样。世界并非如此简单。把文件作为流操作,那是文件打开以后的事情,在文件打开的时候,非常重要地,程序员必须指出,准备创建或打开一个什么样的流。
手册 (man fopen) 说:
fopen的第一个参数是文件名,第二个参数需要我们注意。第二个参数称为 mode,我们可以理解为创造的流的"模式"。
其中对"w"这种模式的解释的第一句是:
"Truncate file to zero length or create text file for writing."
truncate的意思是"截断"。上述这句可以译为:把文件截断为0长度,或者创造用于写操作的文本文件。这里插一句,在UNIX/POSIX系统下,文本文件与二进制文件没有区别。所以,"w"模式所创造的流是,要么如果原来这个名字的文件已经存在,把它截断为0字节,无论里面有什么内容;要么如果原来没有这个文件,创造一个新的文件。
前面问题一节里的文件,原本是:
abcdefghijlmn
我们期待:
abcAAfghijlmn
但是却变成了:
^@^@^@AA
1.AA以后的东西会丢失,就是因为原来的文件被trancate到了0长度. 2."^@"是单独一个字符,即'\0',这是由于我们的写操作是向某个特定位置进行造成的洞.
4. 解决
也许我们这些唯物主义者关注一下唯心的书,可能理解这个问题更容易一些。康德的《纯粹理性批判》、哈耶克的《科学的反革命》,还有 Design of Everyday Things,都提到一个观点。我们对于所有事物的理解和理解发以后的操作,都是基于心中的一个"模型",或者说,我们认为它会 (或者应该) 那样工作。
如果它不是那样工作的,我们会说建模有问题。不过这个字眼很学术味。它的意思大致等同于,那玩意根本就不是你想的那么回事。对于fwritet这个具体问题而言,它不是如我们误以为的数组这样的流,而是在fopen时决定了,会对文件系统有些副作用的操作,然后创造了流中的某一种。
我们想要的效果,需要用以下方法解决.
FILE * f = fopen ("test.in", "r+");
// 注意此处不是 FILE * f = fopen ("test.in", "w");
// w模式会把文件内容清空
// r+这种模式的意思是:
// Open for reading and writing. The stream is positioned at the beginning of the file.
5. 补充个无关的,sizeof
char output[3] = "AA"; /* sizeof -> 3, the number of the array elements */
对数组sizeof操作 (其实sizeof不是函数,而是关键字),得到的是数级中元素的个数.
char* output = "AA"; /\* sizeof -> 4, the size of pointer type *\/ */
对指针sizeof操作,得到的是指针这一数据类型 (不是指针的基类型)的长度,指针在32位系统中是4字节。
6. 代码
6.1 覆盖文件内容的写操作
test.in的文件内容:
abcdefghijk
执行结果为:
^@^@^@AA
或者十六进制形式为:
$ hexdump -C test.in
00000000 00 00 00 41 41 |...AA|
00000005
C代码:
#include <stdio.h>
int main(int argc, char *argv[])
{
char output[] = "AA";
//FILE * f = fopen ("test.in", "r+");
FILE * f = fopen ("test.in", "w");
fseek(f, 3, SEEK_SET);
fwrite(output, sizeof (output)-1, 1, f);
fclose (f);
return 0;
}
6.2 替换文件内容的写操作
test.in的文件内容:
abcdefghijk
执行结果为:
abAAefghijk
C代码:
#include <stdio.h>
int main(int argc, char *argv[])
{
char output[] = "AA";
FILE * f = fopen ("test.in", "r+");
//FILE * f = fopen ("test.in", "w");
fseek(f, 3, SEEK_SET);
fwrite(output, sizeof (output)-1, 1, f);
fclose (f);
return 0;
}
7. 致谢
想起当年从BASIC语言向C语言迁移时,师兄们的教导令我受益良多。感谢张仕鹏师兄,还有一位一时名字没想起来的师兄,还有于寅虎师兄。恩,还有灌我酒的唐猛师兄,通过TC的BGI教会了我指针。
--------------------
博客会手工同步到以下地址: