20130830

对C语言的写文件操作fwrite的一个初学者常见误解

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

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

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



20130829

用windows sdk写一个贪吃蛇

用windows sdk写一个贪吃蛇

这是上学期程序设计与实践3课程的一部分,用windows sdk写一个贪吃蛇。此项目由教师在课堂上演示、查SDK手册和解释、design和实现及解释代码,学生观摩。一共进行了五次课。持续五周,每周一次课,每次课90分钟。

1. 原由

之所以选用 windows sdk的原因如下。1.我的学生刚好学了一部分 windows sdk,虽然sdk更难,但是对于我的学生来说,比MFC却要简单。原因很明显,因为MFC无论有多么好,他们没学过。同理,.net也不如sdk适合。有的时候用户需求就是这样诡异,而你只能随着用户的知识结构调整自己,而不是反过来。2.java awt/swing同学们倒是学过,但是大家以后用java做gui的机会比较少。3.虽然不少人以后可能会做前端,但是用java script之类的也不合适,理由也是他们没学过。教师不能把所有的课程都变成 补基础知识的课程。只能因陋就简了。

选sdk的另方面的原因是希望通过贪吃蛇训练学生:1.即使没有学过某种知识(比如画图、音乐、刷新GDI等等),仍然能通过查手册建立技术原型;2.了解消息循环。

没有学习过,但是却可以自学,是一项重要的能力。假期的时候王师兄提到他遇到的一位女导师,似乎是用GTK从VC移植界面,花了两个 (?)小时就完成了。问她以前学过吗,她答:没,看看手册,然后就做出来了。我们希望培训学生这样的能力。我得承认,这很难,对教师比学生还要难。所以此贴谨供有此理想的教师和同学参考,并热烈期待指导和意见,而此贴本身绝非指导意见。

2. 流程

一般的软件工程 (和/或示范)都假设同学 (或工程师)已经具备和掌握了相关技术知识,对于底层机制的调用是routine类的工作。在本案例中,由于同学们对sdk能做些什么尚无完善的认识,因此大体的流程与通常的软件工程有所不同。我们先做简单的需求分析,估计有哪些功能是我们需要的,然后通过google和手册到sdk中找,找到了以后要做实验,证明这个功能是我们可以实现的,最后再把这些功能的代码在最后要用的项目中写一次。

这有点类似于快速原型法,但是动机不同。快速原型法的目的是确保软件工程师对用户的需求理解无误,用呈现出来的方式与用户确认。我们做原型的目的,是确保SDK这样的底层机制确实支持你要使用的功能。这对于初学者或者探索类的项目具有格外重要的意义。

3. 技术原型

把在需求非常粗糙的时候,先写出来的一些小程序,用于验证底层机制 (如SDK)支持这样的功能,这种小程序我们估且称为技术原型。

在课堂上,我们认为贪吃蛇有以下这样几个技术原型需要确认。

(1)画一些线,组成矩形; (2)把矩形擦掉; (3)把擦和重画结合起来,形成动画效果; (4)不阻塞地获取按键的状态,即程序不停,而在按键时做出某种影响。

前三种技术原型在写程序之前,先有"手动"的演示。教师在画图工具上,画出矩形,然后擦、画,形成动画效果,同时简单介绍视觉暂留现象。

按键获取这一技术原型,教师指出 getchar 或 scanf 这一类的函数不适合的原因,然后引入消息循环的概念。

4. 技术原型的补充

在开发的过程中,又发现需要 timer 的技术原型。把主项目停下来,写 timer技术原型,验证之后,把代码复制和修改到主项目中。

在开发中,发现当初考虑得不周到,有未尽的技术原型,这是正常的现象。除非你正完成的是与既有工作没大差别的项目,未计划周全是正常的。这也是教师在课堂写程序同时学生观摩的意义之一,使学生认识到"错误"不可避免,同时重要的是遇到"错误"如何修正。教师在课前把代码写好调好,课堂上按下F5就运行,同学们无法看到教师修正错误的过程,而这一过程会是他们写下最初100行时最需要的经验。

5. 对技术原型的再讨论

每个原型都在200行左右,除去由wizard生成的消息循环代码,就更短了。这在学生注意力和理解力可达的范围内。如果以整个工程的面目示人,则学生就需要从大的背景中找出某种技术对应的代码来,这就难多了,学生更容易产生挫败感。

每个原型都比较短,即使加上教师课堂上解释的时间,读文档的时间,一般地一到半次到两次课也能够完成。从无到有,虽然只是实现了一个小功能,但是如果学生事先就知道这一功能将出现在最后的工程中,这点小成就还是可以让他们小小地满足一下。

这些原型除了课堂演示,还要求同学们作为作业在当周完成。观摩教师写代码,和自己亲手历经各种困难把自己在课堂上已经见到效果的小项目重现出来,课程实践表明,难度上还是有很大差距的。事实上,全程能基本跟下来所有原型实验的同学,只有一名。

另,把技术原型中的代码抄到主工程中的时候,教师强调 业务逻辑与技术原型的分离。业务逻辑是尽可能与所依赖的平台和技术原型无关的。当然,类似于消息循环和按键消息响应这一类的技术原型,不可避免地与框架相关联。好在,这样的技术已经可以在相当程度上视为通用技术了,在很多平台上都类似。

6. 抽象

利用原型已经验证过的那些api,代码半抄半改,形成了整个贪吃蛇项目。当程序能跑起来没有bug的时候,同学们非常兴奋。我记得当时大家还鼓掌喊"oh yeah"了。

对于软件工程想要贯彻地教学目标而言,这才只是开始,如何抽象,比如用面向对象,比如把重构extract函数,这些才是真正的动机。而这些内容的教学,可以建立在上述刚刚由大家一起完成的贪吃蛇项目中。

在课堂中,我提出"需求变更",蛇头的颜色要求不同,碰撞的规则的变更,要求有更多的关卡等等。有些需求变更的满足,演示了extract函数,比如蛇头的颜色;有些要求重新设计数据结构 (结构体) ,变修改访问过这些结构体的函数;要求更多关卡这个,则演示了DSL的代码生成技术,我们又写了一个程序,专门用于把ASCII艺术画方式实现的关卡地图转换为C代码。比如下面几个关卡中,用"*"代表有障碍物的地方,"."代表空白处:

......
......
......
......*
.....**
......*
......*
......*
......*
.....***
------------------------------------
.
.
.
.
....*******
..........*
....*******
....*
....*******
-----------------------------------
.
.
.
.
....*******
..........*
....*******
..........*
....*******
-----------------------------------
.
.
.
.
....*.....*
....*.....*.
....*******
..........*
..........*
-----------------------------------
.
.
.
.
....*******
....*.....
....*******
..........*
....*******

上述关卡会由代码生成器转换成这样的代码: 
#define MAX_BLOCK_LENGTH 100
int block_length_stage[5] = {0, 10,23,23,13 };
int block_x_stage[5][MAX_BLOCK_LENGTH]={{0},
{6, 5, 6, 6, 6, 6, 6, 5, 6, 7},
{4, 5, 6, 7, 8, 9, 10, 10, 4, 5, 6, 7, 8, 9, 10, 4, 4, 5, 6, 7, 8, 9, 10},
{4, 5, 6, 7, 8, 9, 10, 10, 4, 5, 6, 7, 8, 9, 10, 10, 4, 5, 6, 7, 8, 9, 10},
{4, 10, 4, 10, 4, 5, 6, 7, 8, 9, 10, 10, 10}
};
int block_y_stage[5][MAX_BLOCK_LENGTH]={{0},
{3, 4, 4, 5, 6, 7, 8, 9, 9, 9},
{4, 4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 8, 8, 8, 8, 8, 8},
{4, 4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 8, 8, 8, 8, 8, 8},
{4, 4, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8}
};

关卡生成器也放在了附件中。

7. 未尽之事

最后的结果并非漂亮的设计,课堂中我也提到,真正的工程一般不会给我们机会做这么多地技术原型--它表明工程师的技术仍不成熟。但是,技术不成熟如果是事实的话,那么事实只能通过弥补来减弱影响,而不可能通过掩盖来避免负面效果。所以,如果学生或工程师技术不成熟,技术原型是一个帮助的工具。我们不能因为工具会暴露人类在体力上相对其他动物不足而拒绝使用。

代码也并非漂亮的代码,事实上,有很多不尽人意的地方。这些地方未进行修改的原因,一是我作为教师确实能力有限,对SDK的了解远不全面和深刻,二是它们超出了我原定的教学目的。

8. 感谢

我上课的时候犯了一个错误,没有先建立版本控制就开始写代码,每次课后备份的时候都覆盖了前次的代码。最后的结果固然还在,但是中间的过程也是我希望呈现的,不可或缺。

好在尤其同学备份了每次的代码,并非未如我一般愚蠢地操作,所以中间过程得以保留。所不完满者,尤其同学只备份了代码,而非整个工程,虽然保留了核心的代码,但是需要一些VC的使用知识才能编译。

9. 附件的目录树

|-- draw 绘制矩形的技术原型
|-- erase 擦除矩形的技术原型
|-- keyup 按键检测的技术原型
|-- from_YOUQI 尤其同学给出的有版本控制的代码
|   |-- week10
|   |   |-- draw
|   |   `-- erase
|   |-- week11
|   |   |-- keyup
|   |   `-- snake
|   |-- week12
|   |   |-- snak_business_logic_imp.h
|   |   `-- snake.cpp
|   |-- week13
|   |   |-- snak_business_logic_imp.h
|   |   `-- snake.cpp
|   `-- week14
|       |-- gamedesigner.cpp
|       |-- run.bat
|       |-- snak_business_logic_imp.h
|       |-- snake.cpp
|       `-- stage.txt
`-- snake
    |-- gamedesigner 关卡生成器
    |   |-- Debug
    |   |   |-- run.bat 脚本/批处理,用于把stage.txt转换为stage.h
    |   |   `-- stage.txt 关卡定义的源文件,关卡生成器的源文件
    |-- snake
    |   |-- snak_business_logic_imp.h 业务逻辑
    |   |-- snake.cpp 消息循环框架
    |   |-- stage.h 关卡生成器的输出文件


我无法通过GFW,因此不能向blogspot传附件,请参见[http://download.csdn.net/detail/younggift/6029671]

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

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



20130828

今天暑假我做了什么

今天暑假我做了什么

今天暑假是我从初中二年级开始,唯一算得上假期的一个夏天。初二开始,假期不是各种学校要求的补习,就是竞赛,反正没有消停时候,连初中毕业、高中毕业、大学毕业后的假期也没歇着。今年假期算是例外了。

暑假原计划去趟通化老家,再跟好朋友去旅游一下。结果,刚放假,老猫就病了。它屁股上长了青春痘一类的东西 (学名肛门腺) ,烂了个洞,得天天上药。所以人类也无法远行。我本以为这是给猫上刑,结果猫妈准备了老猫酷爱的妙鲜包,每天上刑的时候老猫都屁颠屁颠地往前凑。

即然走不了,就只好读书。暑假在继续读没看完的几本,有进度的包括:CSAPP,实时系统,历史理性的重建,Algorithmic Adventures,On Lisp, The Design of Everyday things。及一些乱七八糟的东西。以上都没看完。

另一个重要收获是假期快结束的时候开始打星际争霸任务。星际争霸是1998年的老游戏,那个时候windows2000也还没出世,更不用说xp。显示器那时都是4:3的,所以这款老游戏不支持宽屏,得先调成窄屏然后才能玩。插话评论一下,我一直震惊于有些人在宽屏上看普屏的电视节目,人压缩得矮胖,但是他们还是能看下去。星际争霸似乎是640*480的如此之低的分辨率,更不用说好象还是16分游戏,不是16位,而是16种颜色。平衡性什么的,这么有名气的事情,我就不赘述了。任务的故事性挺强,如看美国大片。欺骗与背叛,情理和公义,隐约的爱情,古老家族 (或者贵族?)的腐烂,你能想到的基本都有。更难得的是身在其间,感觉格外不同。

这游戏给我最大的启发是:如果需要训练什么技能,一定要尽早。这道理我最初是在李笑来先生的《把时间当作朋友》里看到的。他说,如果很早就会五笔的话,一定能比别人多记很多笔记;如果很早学会快速阅读,一定能比别人多读很多书。大致如此,不是原意,李笑来先生反对五笔和快速阅读这种东西。我的体会,如果发展徒手攻击力需要100矿+100气的话,那么升级比生产同样价格的兵要有价值得多。因为它会使你手头所有的兵及今后的兵都是更有价值的。

唯一需要考虑的前提就是,如果当前能活下去。所以,只要现在还能对付活下去,那么,发展未来远比现在重要。我们活在未来,而不是此刻。

不那么幸运的就是在星际争霸里,正打得顺风顺水,突然被翻盘的可能性太大了。敌人会根据你的兵种情况生产克星。所以,经常性地了解对手的发展情况,对我们来说也非常重要。

再就是,有些任务的关卡实在太难打了,简直不是人类能通过的。可能一,咱们技艺就是不行。虫族最后一关,我要花近两个小时,而网上录像我看到十几分钟推平敌人的,有的是。可能二,任务的目标根本不是打平天下,而是在一片战火中送个人到什么地方去--这相对容易多了。所以,充分了解用户需求是多么地重要啊。

暑假最重要的收获,我真正地成为了物理系毕业的学生。不是指同学聚会,我终于能用筷子起开啤酒了。这是所有物理男的必修课,今夏补考通过。有窍门,以后再讲给你们听。

这个奢侈地暑假,你做了些什么?

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

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



20130818

同学聚会

同学聚会

今天假期的一件大事,就是大学同学要聚会,早早就把时间定下来,别的事情都为聚会让路。

我们年级一共95多人,此次回来50多个,大家在一起吃睡玩了几天。作息一致,就是大学时的感觉吧。有些同学已经叫不出名字了,有的同学干脆就完全没有印象。大多数同学,基本没有什么变化。也许变老了些,性格上基本都是以前的强化或者加些世俗的因素。

一般都问问职业。不少人从事IT相关的行业。我第一次听到这个词还愣了一下。IT,这是当年还没有的词汇。15年来,产生了不少新词,世界变化真大啊。

我原以为15年来自己成长成熟了很多,兴冲冲地准备向大家汇报。结果大家说的话里的机锋,我仍然得别人背地里解释了才能明白。显然,我虽然进步了,但是仍然跟不上大家的脚步。

2013夏天似乎是聚会高峰,我听说地看到的不少大小聚会。也许,2012过完了,我们终于认识到仍然活着该有多么宝贵。所以得聚聚了,以示珍惜。

在我们去物理系认养松树那天,我早到了,看到门口挂的条幅写着89级同学聚会。我想,系里这也太不靠谱了,核计着找谁联系换一下。正想着,从门里哗啦啦出来一批老头老太太 (和我的同学比)。原来,真的是89级同学在聚会。后来在东师会馆等同学,一位男士径直走到我面前热烈握手,自报家门说他是谁谁谁。我说:师兄你好,我不是你们那届的。

今天夏天,还有师兄回来长春,我跟着师兄的同学 (我的师兄们及宝贵的师姐1人) 聚了一次,又单独谈了两个下午。他给我讲了科学问题是什么意思,我收获很大。


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

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