2007/01/29

super bug

这个超级Bug的起因很不容易发现。

如果在使用MemMove的时候,算错了大小,导致写入越界,在使用MemPtrFree释放内存的时候会遇到错误。

实例:写入的时候错误格式是:
MemMove(dst, src, sizeof(TSeqAddr) * m_num - position);

这里的m_num - position忘记使用括号了。结果是第三个参数越界;然后在使用MemPtrFree释放dst的内存时,会遇到错误。这个错误在模拟器上不会报,在实机上立刻重启,无任何错误信息,#*377的结果是Fatal Exception。找到这个Bug花了我整整5个小时的时间。吐血ing....

外一则:
赋值的对象需要重载"=",尤其在使用集合类的时候,否则死得很难看。

2007/01/23

stage & stage set

今天定义了一套很变态的candidate填充方式。把所有可能拆解的因素都变成可以自定义的。这包括:

源:巨硬特殊的双源工作方式。
alpha/chinese:匹配中文还是英文
搜索模式:包括moreOrEqual, Equal, lessThan, decremental & oneByOne
渐进:all, lastOne, none

这些因素的组合构成一个搜索阶段,SearchStage,一组搜索阶段构成了搜索阶段集,可以完整定义所有可能的搜索组合。

但是在实际使用中,只打算定义两个集,一个是正常模式,一个是强制模式,用户可以在这两个模式之间切换。

低级错误

这个Blog快成了低级错误大全了。

*index--或者*index++

这样的写法会先运算(++)/(--),而不是把地址index存储的整数值 ++或--。

自己打脸一百遍呀一百遍。

2007/01/22

在集合类中使用的对象,务必重载assignment operator

不然会死的很难看。

在集合类重新分配指针的时候,要用到这个函数copy对象。如果系统不指定,那它就采用Copy内存的方式解决,这个最终在对象析构的时候就会崩溃了。

CMol& CMol::operator[](CMol& mol);

Copy Constructor与Virtual Function

没想到这种格式的写法下:

CString str = "";

编译器是直接调用Copy Constructor。我没有在Copy Constructor中初始化指针,但是在调用的Copy函数中首先释放指针的内存:

if (m_p) MemPtrFree(m_p);

结果就是完蛋了。

另外,所有的Virtual函数在弹出窗口模式下都无法使用,这可能和Virutal Function的表是全局变量有关,对编译原理我不是很熟,猜想应该是这样的原因造成的吧。

2007/01/20

segment问题及对策

PalmOS有一些比较变态的限制。

象巨硬这样的弹出程序,必须在程序的第一个64K segment。mHard3的算法复杂度提升,完成这个目标颇有一些难度。解决的办法是把哪些不会在 输入时用到的函数,都从类中拿出去,放到一个继承类中。程序的主界面和维护功能,可以使用继承类工作。这样可以尽可能的压缩核心输入用到的数据结构编译后的大小。如果还是不行的话,就职能考虑ARMlet了。

另外今天的好消息是,已经debug到导入导出数据库了。这个工作同时可以检验大量的数据对象操作的代码质量。这部分完成之后,就可以进入输入法部分的Debug了。照目前的进度,应该在春节前有能对付使用的版本了。

2007/01/17

脚手架代码

这个词真是有够形象。

大部分软件在进入Debug阶段的时候都会遇到这个问题。会写一些和主程序没什么关系,但是对于测试来说会带来很大方便的代码。这类代码被称为脚手架代码。因为脚手架代码通常不是最终发布版本的一部分,程序员就很容易偷懒,不做很好的结构设计和封装,不仔细考虑功能的设计,不做严格的错误处理,因为它的用户是非常理解这些代码会发生什么事的人。

当然这在程序开发阶段是正确的。但是谁又能保证在长期的维护阶段一直正确呢?缺乏好的文档和设计,在软件发布了一段时间之后,需要规划新的改进的时候,这些代码通常就很难读了。在一些数据结构被修改之后,这些代码就可能失效。给调试工作带来麻烦。

要知道想盖好一座楼,好的脚手架也是成功的保证。

2007/01/09

巨硬III进度

元旦没能象预期的一样有大幅度进展,不过进展还是有一些的。今天连续写了5~6个小时,把很关键的candidate填充算法写完了,里面做了一些偷懒的设计,到最终版本的时候还要修改,不过在debug之前,就先这样了。

机器人的设想被推到后面去了。在debug之后再添加这个功能。不过基于拼音的输入法不能指望有多智能,因为要涉及到多音字的问题;如果是形码,那确实可以全自动无错误的学习了,音码理论上不可能完成,实际上应该可以完成一个有些无伤大雅的小错误的实用版本。

填充算法完成之后就剩下后处理了。一个hop算法要完成,不过这个比较简单,词频调整的算法也简单,增加sequence和更改sequence状态的算法有些小麻烦,还有match算法还没写,不过Anyway,这个月应该可以进入调试阶段了。

上次的版本编译后是52K,这里面包含了一个字体,加密的算法,还有图形界面等等;现在的巨硬III已经进展到编译后38K,离最终完成不是很远了。

2006/12/27

Err, Boolean, MemPtr

把函数的返回值设计成Boolean或者MemPtr在使用的时候是非常方便的。而且也容易理解,返回true就是成功或真, false就是失败或假。返回指针的时候,如果返回空指针就是失败了,如果是实指针就是需要的地址。

但是这两种情况就不能处理错误,除非使用C++的异常处理机制。如果把返回值设计成Err,那么错误终归是可以处理的,但是这样使用的时候不方便,而且需要处理多少错误也是个问题。

通常,私有函数可以偷偷懒,因为你很清楚这些函数在什么地方会被调用,可以约定参数都是有效的(看看参数的来源,不要有从公有函数传递过来的外部参数,否则就不能做这样的假定),这样可以用Boolean或者MemPtr这样易用的形式;但是对于公有函数,在写这个类的时候没法预料将来会怎样被调用,所以假定所有可能的参数比较好,考虑所有的错误发生情况并返回正确的错误信息。

如果用了Boolean或者MemPtr这样的形式,要看看函数内部资源失败的时候会有什么后果(比如Malloc),是否会导致数据不一致,包括内存对象和存储对象。如果会,那还是不要偷懒,否则因为小几率事件导致数据损坏的时候就追悔莫及了。

2006/12/26

数据完整性

在数据库中,有一个功能叫做Transaction,它的意思是,如果要做一系列的更新、创建、或者删除操作的话,应该有一个显示Commit;如果Commit成功,则所有的改动都生效,如果Commit失败,则没有任何改动生效----而不是部分生效。

在涉及到数据存储的时候,通常也会很好的考虑这个问题,简单的办法就是做一个缓冲出来,全部写完之后一次性写入文件;这也要求尽量把有相关性的数据写在一个地方,否则很难保证不在多个文件写入的时候发生错误,导致数据不一致。

在对象中同样要考虑这个问题。虽然我们可以假定在函数返回错误的时候,传递的参数变量会失效(如果对象较大又不希望被改变的话,传递对象的Copy的引用,而不是直接传递引用),但是在返回错误的时候,如果本来有效的数据对象本身同样被摧毁,是不能接受的。所以不要在函数中直接改动成员变量,除非你明确知道不再可能有错误发生。不要做太大胆的假设,实际上除了MemMove这样屈指可数的几个函数之外,即使是内存分配也有发生错误的可能。较好的办法是做出一个成员变量的复制品,对这个复制品进行改动,如果过程中出现错误,丢弃这些复制品并返回错误;如果没有出现错误,那么用复制品对成员变量赋值,或者把指针指向复制品,把原件丢弃。

如果每个过程都遵循这样的原则设计,那么就不用为对象内部出现数据不一致担心了。