寄件者: xioax
收件者: 侯 捷
传送日期: 2002年1月22日 AM 09:36
主旨: 关於MFCLite的一些问题
候老师:
您好,我是一名刚从浙大毕业的学生,也是您的忠实读者。最近我在阅读您的MFCLite,发现了些问题,列举如下(如有不对的地方,还请老师指点):
1 对象持久性。由於两年前我曾仔细研究过Turbo Vision的持久性,对这个问题比较熟悉。所以一开始我就发现了您的代码中所存在的问题,下面是我的测试程序(部份),还有输出文件的内容:
...
CSquare *psqr1 = new CSquare () ;
CSquare *psqr2 = psqr1 ; // psqr1 and psqr2 point to same object
assert (psqr1 == psqr2) ;
{
CFile write ("test.tmp", CFile::modeWrite) ;
CArchive store (&write, CArchive::store) ;
store << psqr1 << psqr2 ;
}
delete psqr1 ;
psqr1 = psqr2 = NULL ;
{
CFile read ("test.tmp", CFile::modeRead) ;
CArchive load (&read, CArchive::load) ;
load >> psqr1 >> psqr2 ;
}
assert (psqr1 == psqr2) ; // here is an assert failure, not point to same object!
...
00 00 07 00 43 53 71 75 61 72 65 00 00 00 00 00 ....CSquare.....
00 00 00 00 00 00 00 00 00 07 00 43 53 71 75 61 ...........CSqua
72 65 00 00 00 00 00 00 00 00 00 00 00 00 re..............
psqr1和psqr2指向同一对象,写入文件时应该只有一份,但是在您的实现中却写了两次 导致了读出时,psqr1和psqr2指向了不同的对象。显然这是不正确的。我觉得对於C++ 对象持久性而言,最重要的问题:一个是如何保存相关的类信息,另一个就是如何解决上述问题 在您的两本着作《多形与虚拟》 《深入浅出MFC》中对前者都有很精辟的论述,唯独後者一点也没有提及,不能不说是一个很大的瑕疵。对於如何解决这个问题也不是很困难,只要先实现CMapPtrtoPtr和CPtrArray,在写入时先查map如果已写过,就只把输出序号写入文件,如果没有就把对象的地址和输出序号插入map,再把数据写入文件。读出时,遇到第二种情况(即文件中有实际数据,而非只是序号),就先创建一个对象把数据读出,接着再把新建对象的
地址加到数组array尾端,遇到第一种情况,就以输出序号为索引直接从数组中得到对象(由於写入和读出的顺序一样,仅用输出序号就可以完全解决问题)。
2 CPtrList的实现有内存泄漏,下面是相关代码和我的修改(红色):
...
我觉得就MFCLite的目的而言,完全没有必要象MFC那样实现一些比较高级的内存管理技巧。因为一来这些内存管理与MFC的应用程序架构完全没有关系,二来加大了读者的阅读难度,三来加大了MFCLite的出错机会。如果CPtrList使用CObList的实现方法就没有这麽多问题
另外,您说在下面的代码中,删除CPtrList中的元素程序会崩溃,不知是不是上面的原因?
void CMultiDocTemplate::RemoveDocument(CDocument* pDoc)
{
CDocTemplate::RemoveDocument(pDoc);
// m_docList.RemoveAt(m_docList.Find(pDoc)); // 把 doc 节点移走 <- 有问题. crash!
}
3 CDocument::OnOpenDocument和CDocument::OnSaveDocument有内存泄漏:
BOOL CDocument::OnOpenDocument(const string& strFileName)
{
CFile* pFile = new CFile(strFileName.c_str(), CFile::modeRead);
CArchive loadArchive(pFile, CArchive::load);
Serialize(loadArchive);
loadArchive.Close();
pFile->Close();
// should delete pFile
delete pFile ;
return TRUE;
}
BOOL CDocument::OnSaveDocument(const string& strFileName)
{
CFile* pFile = new CFile(strFileName.c_str(), CFile::modeWrite);
CArchive saveArchive(pFile, CArchive::store);
Serialize(saveArchive);
saveArchive.Close();
pFile->Close();
// should delete pFile
delete pFile ;
return TRUE;
}
4 我发现CObject的析构函数不是虚拟的。我想一定是老师的一时疏忽。:-)面对几千行的
代码,谁敢保证没错呢?
5 MFCLite对新建文件 打开文件和保存文件模拟得很好;但是对关闭窗口,进而造成文档的摧毁却模拟得不够。其实在C++中由於析构错误造成的内存泄漏,往往是最常见,也是最难
排除的Bug。希望在MFCLite的新版本能看到相应的模拟
虽然MFCLite存在一些不足,但是瑕不掩玉,我觉得它对C++和MFC的学习都有非常非常大的帮助。老师的每一本书(无论是译作还是着作)都是相关领域的精品,凡是在市面上见得到的我都买了下来,给我带来的极大的帮助。现在我正在急切地等待老师的《STL原码剖析》 《泛型设计与STL》 《标准C++程式库》等书籍。希望老师注意身体,给我们写出更好的作品
Hi, xioax, 你好:
以下回答你的一些疑问与建议。
> 1 对象持久性。由於两年前我曾仔细研究过Turbo Vision的持久性,对这个问题比较熟悉。所以一开始我就发现了您的代码中所存在的问题,下面是我的测试程序(部份),还有输出文件的内容:┅
> 我觉得对於C++ 对象持久性而言,最重要的问题:一个是如何保存相关的类信息,另一个就是如何解决上述问题 在您的两本着作《多形与虚拟》 《深入浅出MFC》中对前者都有很精辟的论述,唯独後者一点也没有提及,不能不说是一个很大的瑕疵。对於如何解决这个问题也不是很困难,只要先实现CMapPtrtoPtr和CPtrArray,在写入时先查map如果已写过,就把输出序号写入,如果没有就把对象的地址和输出序号插入map,再把数据写入文件。读出时,遇到第二种情况,就先创建一个对象把数据读出,接着再把新建对象的地址加到数组尾端,遇到第一种情况,就以输出序号为索引直接从数组中得到对象(由於写入和读出的顺序一样,仅用输出序号就可以完全解决问题)。
●侯捷回覆:你所指出的,对我是当头棒喝。的确,模拟Persistence时我很少考虑你所说的(alias)情况。针对你的提议,我已修正 MFCLite3并开放於本网页。
MFC(Lite) 不但考虑了你所说的那种(alias)情况,还对「隶属相同class」的不同objects的文件读写做了优化考量。对於已储存 class information(例如class name和 schema no.)的 class而言,再储存一次是一种浪费 ─ 浪费空间也浪费时间。因此,MFC的 CArchive利用CMapPtrToPtr做为cache,不但用来安置CObject*,也用来安置CRuntimeClass*。读取文件时,道理相同,使用CPtrArray,不但安置CObject*也安置CRuntimeClass*。我不打算在MFCLite中再多加上CMapPtrToPtr和CPtrArray的实作(那恐怕又多出1000行代码,而且CMapPtrToPtr是以 hash table完成,那就使阅读的门槛又高了些),我已在新版的 MFCLite 3.0 中以C++ 标准程式库的 map和 vector 取而代之。
MFCLite的阅读门槛愈垒愈高,乐了诸位功底深厚的人,可难为了其他功力尚浅的读者呀。对我而言,取舍成了难事。
我保留并扩大了你的测试,放在 MFCLite Application (mfclapp.cpp) 的 CMyWinApp::InitInstance()中。程式一执行就会在萤幕上显示这段测试结果并显示 StoreMap 和 LoadArray,然後才开始一般正常的执行流程。你对persistence 的这段 hint 带给我莫大乐趣,迫使我完成《深入浅出MFC》第 8 章关於 document format 中的 tags(图8-10a, 图8-10b内的 FFFF, 8001, 8003...以及未出现於该图的 tags 如 0002, 0005...)的深刻体会。
> 2 CPtrList的实现有内存泄漏,下面是相关代码和我的修改(红色):┅
●侯捷回覆:修改完毕。我没有采用你的方式,而是采用MFC的方式。当初实作MFCLite时,为求简化略去了一些东西,不想造成了memory leak 而不自觉。现补上。我的额头开始冒汗了。
> 我觉得就MFCLite的目的而言,完全没有必要象MFC那样实现一些比较高级的内存管理技巧。因为一来这些内存管理与MFC的应用程序架构完全没有关系,二来加大了读者的阅读难度,三来加大了MFCLite的出错机会。如果CPtrList使用CObList的实现方法就没有这麽多问题
●侯捷回覆:CPtrList的精巧设计,与application framework之间非常独立。虽然它比较复杂,应该不至於混乱读者对application framework的学习。保留这麽复杂的list实作,有两个用意,(1) CPtrList是MFC内部自我维护管理时,大量运用的一个data structure,因此对於效率(空间和时间)都很要求 (2)既然其十分独立,再复杂也不至於混乱读者对application framework的学习,那麽展示一下这种不错的设计,满好的。
> 另外,您说在下面的代码中,删除CPtrList中的元素程序会崩溃,不知是不是上面的原因?
●侯捷回覆:不是。原因已查出,乃因我把所有的documents的 delete动作放错位置。这些动作在 MFC 之中应该由 CFrameWnd::OnClose()触发(目前MFCLite尚未实作之),我却把它们放在 CMultiDocTemplate::~CMultiDocTemplate()。下面是 MFCLite 的 CallStack:
CMultiDocTemplate::RemoveDocument() // (B)
CDocument::~CDocument()
CMultiDocTemplate::~CMultiDocTemplate() // (A)
CDocManager::~CDocManager()
CWinApp::~CWinApp()
(A) 已针对 pDoc 呼叫 m_docList.RemoveAt(),
(B) 又针对 pDoc 呼叫 m_docList.RemoveAt(),但 pDoc 当时已经不在 m_docList 中了,Find() 传回 NULL,RemoveAt(NULL) 当然就挂了。
治本之道是模拟 MFC,开发 window close system(也就是你的第5个问题),治标之道则是在 CPtrList::RemoveAt() 一开始增加一行,判断删除位置是否为 NULL,如是则立刻回返。
> 3 CDocument::OnOpenDocument和CDocument::OnSaveDocument有内存泄漏:
●侯捷回覆:应该说是资源泄漏(resource leak)。已全部补妥。我的身上开始冒汗了。修补这个问题的同时,一并修补了 CFile::~CFile() 和 CFile::Close() 内的安全检验工作。
> 4 我发现CObject的析构函数不是虚拟的。我想一定是老师的一时疏忽。:-)面对几千行的代码,谁敢保证没错呢?
●侯捷回覆:全部补妥。我汗流夹背了。
> 5 MFCLite对新建文件 打开文件和保存文件模拟得很好;但是对关闭窗口,进而造成文档的摧毁却模拟得不够。其实在C++中由於析构错误造成的内存泄漏,往往是最常见,也是最难排除的Bug。希望在MFCLite的新版本能看到相应的模拟
●侯捷回覆:汗如雨下。对於你所提的问题,我原本模拟了一些,後来觉得有点烦,不在我最关心的主轴范围内,就放下了。针对你的提议,我可能会在MFCLite3.0中实作出来,也可能在书中描述这些问题,留给读者去实现。
> 虽然MFCLite存在一些不足,但是瑕不掩玉,我觉得它对C++和MFC的学习都有非常非常大的帮助。老师的每一本书(无论是译作还是着作)都是相关领域的精品,凡是在市面上见得到的我都买了下来,给我带来的极大的帮助。现在我正在急切地等待老师的《STL原码剖析》 《泛型设计与STL》 《标准C++程式库》等书籍。希望老师注意身体,给我们写出更好的作品
●侯捷回覆:你非常优秀,基本功非常好。浙大电子工程系的学生真是令我刮目相看呀。《STL源码剖析》和《C++标准程式库》简体版出版後各赠你一本,表示我的感谢,以及对你的期许。《泛型程式设计与STL》侯捷译本只有繁体版,连同《C++ Primer 3e》侯捷译本(只有繁体版)近日一起寄送给你。请告诉我你的邮寄地址。我只能寄海运(大约需时25天),空运实在太贵了 :)
你有非常好的代码追踪能力,像猎犬一样的鼻子 :)。我想你肯定用心追踪过 MFC 源码。赏析名家手法,是将自己拉抬到制高点的一个重要法门,形同大师灌顶。我自己离开编程第一线後,以此法修练自己,开拓自己。对於你,一个刚毕业的小夥子,我感到十分好奇。(你怎麽知道你的第一个问题的解法?你自行阅读MFC源码而叁悟的吗?真是不简单)
感谢你给我如此丰富的讯息。MFCLite 十分复杂,能够看懂它又指出问题,切中要害,甚至提出解法,实在很不容易。目前尚未有任何一位读者给我关於MFCLite 的讯息。我听说浙大的计算机水准很高,从你的来信得到了一些证明 :)
--------------------------------------------------------------------------------
传送日期: 2002年1月23日 PM 01:35
候老师:
您好,没想到能这麽快收您的回信 从这点小事就足以看出老师待人是多麽热忱,多麽平易近人 无论是在治学 还是在待人上,都是我学习的榜样。
其实我的经历很平凡,刚过23岁的生日(按照传统习俗今年是我的本命年),出生在贵州都匀(一个风景很美的小城市)一个普通的工人家庭,97年考上了浙江大学,专业是和计算机比较相近的电子工程;记得在上大学前,我最喜欢的课程就是数学(尽管很多人都觉得它很枯燥),因为一套非常复杂的理论,往往仅仅建立在几条显而易见的公理之上,而其它的东西只不过是一些推论,那时我最喜欢做的事就是尽力把各种定理推导出来,从中我不仅学到了很多在书本中没有的数学知识,更重要的是让我养成了刨根问底的习惯,凡事都想弄清楚为什麽,而不仅仅是知道;
学习程序设计是在上大学後(在此以前我还没有接触过计算机),刚开始学C语言,我最感兴趣的就是那些标准程序库是如何设计出来,当时我还用Taylor定理实现了标准数学函数(虽然比原来的慢了许多,但还是学到了不少东西),接 我开始看标准库的源代码,很快我就发现光会C还不够,因为里面充满了太多的汇编代码,於是我开始学习x86汇编和计算机硬件结构,前後用了半年多的时间,在学习过程中进一步加深了对C语言的理解(象叁数调用规则,变长叁数的实现,局部变量和全局变量的区别,程序的内存布局...都是在这段时间才有了质的认识),直到99年才开始学C++,目的就是为了学MFC,在学完C++语法後,我开始试 看MFC的源代码,结果自然是以失败告终(一个刚学了一点C++语法,没有一点Windows程序设计经验的人,想看懂MFC源代码,当然不可能),
但是我没有放弃,我开始制定了一个长期的学习计划(三年左右,现在应该快要实现了),第一步阅读在Dos下一个较大的C++类库源代码,当时我选择了Turbo Vision,目的主要是学习用C++设计大型系统的知识,当然最後的收获不仅至於此,我还掌握了消息驱动的精髓,管理视窗系统的数据结构,内存管理的技巧,汇编语言与C++的结合...一直以来我都觉得TV是一个很好的学习范例,一来是它要求的起点比较低,只要会C++ 汇编和熟悉Dos环境,二来大小比较合适总共只有1MB源代码,最後它也足够复杂基本上实现了一个小型的Windows系统(主要是消息驱动机制和视窗管理)并展现了许多重要的C++课题(象持久性),为Windows和MFC的学习打下了很好的基础,
第二步学习用C语言写Windows程序设计,这一步是我在看了您的《深入浅出MFC 1st》後临时加上去的,很感谢您你在附录里的书评,基本上我就是按照您的建议先後阅读了《Programming Widnows 5th》 《Advanced Windows 3rd》 《Windows 95 : A Developer's Guide》 《Windows 95 System Programming SECRETS》(後两本书是从老师的网站上下载的,在这我还要再次感谢老师无偿地提供这两本非常经典的书籍 ),为了理解Windows虚拟内存管理,我还专门阅读了386保护模式的书籍,同时还看了老师的《Inside C++ Object Model》和《Effective C++ 2nd》(很可惜没能在99年学习Turbo Vision的时候买到这两本书,否则我对C++和TV的理解肯定会更进一个档次),
第三步就是阅读MFC源代码,同样老师的书评和书籍再次给了学生很大的帮助,现在这一步正在进行中,有了上两步的准备,我想一定能完成。
上面基本上是我大学四年主要学习过程。下面是我今明两年的学习计划:
1 加强数学基础,重新深入学习离散数学,组合理论,图论等知识
2 学习常用的算法,如图形 编译 数值计算
3 加强对C++的理解,像老师的泛型设计系列
4 UNIX平台程序设计
5 网络编程和TCP/IP协议
6 COM和CORBA
光有计划不行,还得落实到行动中,我会尽量争取业余时间,努力提高自己的水平
记得去年三月份第一次上老师的网站,立即就被里面的内容所深深吸引(我把整个网站download下来,刻成了一张光盘) 无论是书评 散文还是书籍,都充满了真知灼见 现在粗制滥造的"作家"实在是太多了,像老师这样对读者负责的技术作家,怎能不让人耳目一新呢 对老师关於书籍和书价的观点,我非常赞同 对於一本真正的好书,贵上一两倍也是应该,只有这样才能在作(译)者 出版社 读者之间产生良性循环,产生更多更好的书
现在,我在北京□□工作,一家主要从事集成电路设计和中间件开发的合资企业。目前,我所在的项目组主要从事...。按照安排,再过一个月就到我们小组给其它组做技术讲座了,我们选的题目就是泛型设计和STL,真希望能在此前买到老师关於泛型设计的简体中文版书籍。
寄件者: xioax
传送日期: 2002年1月28日 PM 07:23
就像老师说的最好的办法就是在退出之前对每个文档框架窗口发出WM_CLOSE命令,一来可以提示用户存盘,二来可以将其删除。不过也可以把下面这句给注释起来,就可以避免两次删除的问题(只由CDocument的析构函数删除一次)。
CMultiDocTemplate::~CMultiDocTemplate()
{
POSITION pos = m_docList.GetHeadPosition();
while (pos != NULL)
{
POSITION posDoc = pos;
CDocument* pDoc = (CDocument*)m_docList.GetNext(pos);
// m_docList.RemoveAt(posDoc); //
delete (CDocument*)pDoc; //
}
}
●侯捷回覆:在这个地方修补,有点东补西凑的味道,恐怕不是很好。anyway,我已经将整个 window-close 系统放上去,见稍後说明。真是大工程呀。累死我了。
对了,在AfxWinInit中打开resource.res用的是绝对路径,如果目录换了,就打不开,所以最好改成相对路径。
void AfxWinInit(void) { // ref. appinit.cpp
...
// ifstream ifs("d:\\pic2\\mfclite\\resource.res");
ifstream ifs("resource.res"); // change to relative directory
●侯捷回覆:同意,并修改为「万一找不到档案,提示使用者」。使用相对路径,那麽程式发展过程中每次修改 .rc 时,必须将新制造的 .res 复制到:
\pic2\mfclite\vc6ide 目录和
\pic2\mfclite\vc5ide\debug 目录和
\pic2\mfclite\vc5ide\release 目录中,
第一个给IDE环境中执行程式时使用,第二个给IDE环境编译出来的DEBUG版本但於console环境中执行时使用,第三个给IDE环境编译出来的RELEASE版本但於console环境中执行时使用。已於 makefile 中撰写这些拷贝动作。
寄件者: xioax
传送日期: 2002年1月29日 AM 10:58
候老师:
好 今天意外地发现一个问题,是关於text mode和binary mode的。由於在打开文件时您用的是"wt"和"rt",所以会影响文件的读写:
1 在写入时,'\n'(0x0a)转化成'\r''\n'(0x0d 0x0a),在读入时,'\r''\n'转化成'\n',这个影响不算太大,毕竟写入读出的转化是对称的。
2 在text mode下,CTRL+Z(0x1a)被认为是文件结束的标志,这就会产生大麻烦。下面是测试代码:
...
{
char c = 0x1a ;
CFile write ("test.tmp", CFile::modeWrite) ;
CArchive store (&write, CArchive::store) ;
store << c ;
}
{
char c ;
CFile read ("test.tmp", CFile::modeRead) ;
CArchive load (&read, CArchive::load) ;
load >> c ; // here, an assert failure
}
...
由於写入的是0x1a,导致在读出时会被认为到了文件尾,fread失败
所以应该修改CFile::Open,用binary mode打开:
BOOL CFile::Open(const char* lpszFileName, UINT nOpenFlags)
{
// TRACE1("filename=%s %s\n", lpszFileName,
// (nOpenFlags == modeRead ? "modeRead" : "modeWrite"));
switch (nOpenFlags) {
case modeRead: //
m_hFile = ::fopen(lpszFileName, "rb"); //
break;
case modeWrite: //
m_hFile = ::fopen(lpszFileName, "wb"); //
break;
default:
assert(FALSE); //
}
assert(m_hFile != NULL);
return TRUE;
}
下面是对text mdoe和binary mode的一段描述(摘自MSDN):
Open in text (translated) mode. In this mode, CTRL+Z is interpreted as an end-of-file character on input. In files opened for reading/writing with "a+", fopen checks for a CTRL+Z at the end of the file and removes it, if possible. This is done because using fseek and ftell to move within a file that ends with a CTRL+Z, may cause fseek to behave improperly near the end of the file.
Also, in text mode, carriage return-linefeed combinations are translated into single linefeeds on input, and linefeed characters are translated to carriage return-linefeed combinations on output. When a Unicode stream-I/O function operates in text mode (the default), the source or destination stream is assumed to be a sequence of multibyte characters. Therefore, the Unicode stream-input functions convert multibyte characters to wide characters (as if by a call to the mbtowc function). For the same reason, the Unicode stream-output functions convert wide characters to multibyte characters (as if by a call to the wctomb function).
Open in binary (untranslated) mode; translations involving carriage-return and linefeed characters are suppressed.
●侯捷回覆:同意,已修改。这个错误你也找得出来,佩服。
你的提问方式,以及对 MFCLite 的深刻理解,对我的写作很有帮助。感谢你的用心。
传送日期: 2002年1月28日 AM 08:10
侯老师,您好,我发现在没有安装Cygwin的机器上无法运行mfclite3中的mfclappg.exe,建议您在下载文件中提供cygwin1.dll(一般在"/cygwin/bin/"可以找到),并使之与mfclappg.exe位於同一目录。
Solstice
●侯捷回覆:好的。