对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
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
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教会了我指针。


--------------------


博客会手工同步到以下地址:


[http://giftdotyoung.blogspot.com]


[http://blog.csdn.net/younggift]

你可能感兴趣的:(程序设计)