善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。
显示标签为“VC”的博文。显示所有博文
显示标签为“VC”的博文。显示所有博文

2025-11-11

你可能从未注意过的 MFC 陷阱:模态对话框禁用机制的局限性

在最近的一个软件开发案例中,遇到了不容易处理的情况。

在我们的软件中,有那种一直浮在界面上的非模态对话框,例如那种浮动的工具面板。但是,这种窗口,貌似不会在主窗口弹出模态对话框的时候被 Disable。如果模态对话框是在这个非模态对话框中弹出的,那没有问题。

用 VS2013 和升级到最新的 VS2022 各写了一个测试程序,发现这就是 MFC 的默认逻辑。
在主窗口上放了两个点击后各自会弹出非模态对话框和模态对话框的按钮。先弹出非模态对话框,然后再去弹出模态对话框。此时主窗口被 Disable,无法响应鼠标、键盘消息。但非模态对话框不受影响。

本想靠调整产品设计「容忍」过去。但问题在于,如果在保持模态对话框弹出的情况下,去把非模态对话框先关闭了,那主窗口会被 Enable。此时它跟模态对话框之间都能响应鼠标、键盘消息,效果就好像之前弹出的模态对话框变成了非模态对话框一样。这个时候就会有「后果」了,产品设计再怎么调整,也没法让软件在这种乱了套的情况下还能工作正常。


在网上 Google 了半天,不要说解决方案,连问题都没人提到。讲解模态 / 非模态对话框的文章有一些,但都是很入门的介绍。只是从「使用者」的角度去讲用法谈区别,并没有涉及我遇到的问题。很少从原理角度进行说明,更没有去分析源代码。大概 MFC 现在用的人真的不多了吧?

图片由 Google Gemini 生成

还是得自己动手,在 DoModal() 处下了一个断点,单步跟踪进到 MFC 的代码里面看了一下,就明白了。以下代码来自 VS2013,VS2022 我貌似没有安装 MFC 源代码,但从表现上看二者在这个逻辑上应该相差无几。我们一起来看看 CDialog::DoModal() 到底干了些什么事情吧:

INT_PTR CDialog::DoModal()
{
	// can be constructed with a resource template or InitModalIndirect
	ASSERT(m_lpszTemplateName != NULL || m_hDialogTemplate != NULL ||
		m_lpDialogTemplate != NULL);

	// load resource as necessary
	LPCDLGTEMPLATE lpDialogTemplate = m_lpDialogTemplate;
	HGLOBAL hDialogTemplate = m_hDialogTemplate;
	HINSTANCE hInst = AfxGetResourceHandle();
	if (m_lpszTemplateName != NULL)
	{
		hInst = AfxFindResourceHandle(m_lpszTemplateName, RT_DIALOG);
		HRSRC hResource = ::FindResource(hInst, m_lpszTemplateName, RT_DIALOG);
		hDialogTemplate = LoadResource(hInst, hResource);
	}
	if (hDialogTemplate != NULL)
		lpDialogTemplate = (LPCDLGTEMPLATE)LockResource(hDialogTemplate);

	// return -1 in case of failure to load the dialog template resource
	if (lpDialogTemplate == NULL)
		return -1;

	// disable parent (before creating dialog)
	HWND hWndParent = PreModal();
	AfxUnhookWindowCreate();
	BOOL bEnableParent = FALSE;
	CWnd* pMainWnd = NULL;
	BOOL bEnableMainWnd = FALSE;
	if (hWndParent && hWndParent != ::GetDesktopWindow() && ::IsWindowEnabled(hWndParent))
	{
		::EnableWindow(hWndParent, FALSE);
		bEnableParent = TRUE;
		pMainWnd = AfxGetMainWnd();
		if (pMainWnd && pMainWnd->IsFrameWnd() && pMainWnd->IsWindowEnabled())
		{
			//
			// We are hosted by non-MFC container
			// 
			pMainWnd->EnableWindow(FALSE);
			bEnableMainWnd = TRUE;
		}
	}

	TRY
	{
		// create modeless dialog
		AfxHookWindowCreate(this);
		if (!CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), hInst) && !m_bClosedByEndDialog)
		{
			// If the resource handle is a resource-only DLL, the dialog may fail to launch. Use the
			// module instance handle as the fallback dialog creator instance handle if necessary.
			CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), AfxGetInstanceHandle());
		}

		m_bClosedByEndDialog = FALSE;
	}
	CATCH_ALL(e)
	{
		TRACE(traceAppMsg, 0, "Warning: dialog creation failed.\n");
		DELETE_EXCEPTION(e);
		m_nModalResult = -1;
	}
	END_CATCH_ALL

	if (bEnableMainWnd)
		pMainWnd->EnableWindow(TRUE);
	if (bEnableParent)
		::EnableWindow(hWndParent, TRUE);
	if (hWndParent != NULL && ::GetActiveWindow() == m_hWnd)
		::SetActiveWindow(hWndParent);

	// destroy modal window
	DestroyWindow();
	PostModal();

	// unlock/free resources as necessary
	if (m_lpszTemplateName != NULL || m_hDialogTemplate != NULL)
		UnlockResource(hDialogTemplate);
	if (m_lpszTemplateName != NULL)
		FreeResource(hDialogTemplate);

	return m_nModalResult;
}

看到高亮的代码应该就能明白了。MFC 在弹出模态对话框的时候,只是把 hWndParent 给 Disable 了。然后如果发现主窗口还没被 Disable,再补上一刀。后面这个逻辑应该就是为了应对在非模态对话框中弹出模态对话框的情况。

看到了这些逻辑,就可以明白本文最开始讲到的情况是情理之中的。模态对话框只管住了大 BOSS 和父亲,对于兄弟和叔伯之类,都没有去管。如果情况复杂一点,例如非模态对话框中又弹了第二层的对话框,那么不管这个对话框是模态还是非模态,接下来都有机会整出点问题。


知道了问题的原因,接下来就要寻找靠谱的解决方案了。肯定有人解决过这类问题,因为有些软件没有这种问题。但不知道是因为觉得问题太简单了不值得说,还是想藏着掖着?

希望是前者。因为真正的解决方案确实也很简单。

之前有同事尝试解决这个问题,办法是让模态对话框去通知所有的非模态对话框(或者说需要被 Disable 的窗口)自行 Disable,用了我们软件内部与 Windows 消息相互独立的另外一套通讯机制。

但这个解决方案有一些问题:模态对话框除了自己开发的那些,还有系统提供的 MessageBox、文件打开对话框等,另外还包括第三方库中的那些,都是我们无力触达的。所以虽然它花了很多力气让对话框们去多重继承一个基类,但还是没法彻底解决这类问题。

回头想想,主窗口不是总是会被 Disable 的么?那么监听主窗口的 WM_ENABLE 消息,在事件处理函数中把那些需要额外 Disable 掉的窗口集中处理掉,不是就可以了?

实际做起来也很简单,基本上就是在主窗口的 OnEnable() 中用 EnableWindow(bEnable) 把状态传递过去。唯一需要动点脑筋的,就是让那些需要额外 Disable 掉的窗口把自己的 HWND 注册到一个主窗口拿得到的地方,也就是说待处理的窗口列表需要管理起来。

具体的实现代码我就不贴了。重要的是解决问题的思路,具体如何实现,可以有很多种办法,选择适合自己的哪一种就好。相信读到这里的,都是合格的 Win32 / MFC 程序员。

2017-05-25

将 C++11 新特性用于代码优化

关于 C++11 的科普,在这里就不详细进行了,可以参考 维基百科 页面。即使是中文页面,我认为写得足够详细和系统了。

总之,C++11 对原始的 C/C++ 作出了在我看来是不算小的改动。有一些概念,放在以前的时代是绝对真理,在 C++11 推出之后,可能需要重新了解一下了。VS2013 对 C++11 的支持并不算「完美」,不过大部分「有用」的特性还是到位了。这里就以它为例,来谈谈如何把 C++11 的新特性应用到你的软件开发工作中来提升性能和开发效率。

本文提到的 C++11 的这些新特性,我大致把它们分为两类:一类是可以直接提升代码的性能表现的,我列在「性能优化」部分;另一类虽然不能直接提升代码的性能,但可以提升开发效率,便于更快地开发出可维护性更好的代码,我列在「非性能优化部分」。

另外,受作者水平所限,本文并不是对 C++11 在这些方面的完整的参考内容,仅仅作为一个引导来阅读吧。


性能优化部分

右值引用和 move 语义
C++11 引入了右值引用,支持了 move 语义。在我看来,这个变化的意义可能是 C++11 里面最大的一个。右值引用和 move 语义是什么,这里不展开。通俗一点地讲,这个特性使得程序员可以在必要的时候自行决定到底是深拷贝还是浅拷贝。对于大量的数据「搬运」操作,可以节省下不少时间。对于性能优化来说,意义重大。

其实就算没有右值引用,在 C++11 之前的时代也可以做类似的优化。C++ 程序员只要对于自己的资源管理类显式地提供深 / 浅拷贝版本的函数即可。不过这样一来代码工作量会比较大,程序会变得比较复杂,并且始终不是一个规范。现在这一切都不是问题了。

对于 STL 自己的类/容器,VS2013 已经做了足够的优化。例如,你可以通过:
string strA = std::move(strB);
来把 strB 的字符串动态内存部分直接给到 strA,速度比简单的赋值要快不少。当然,strB 就不再具有有意义的值了(这里例子中会变成空字符串)。当你 push_back 或 insert 一个 string 到容器里面的时候,如果 string 其实是一个临时变量,那么用 move 语义你也可以得到相当明显的性能提升。

如果例子里面 string 换成一个 map<string, string>,那么提升会更明显。总之,内存的分配和释放,以及 memcpy 操作统统被避免了。所以,理论上需要传递的东西越多,你得到的性能提升就会越显著。

就地部署(emplace)
C++11 对于常见的 STL 容器,都提供了一种能提升性能的数据置入方法,称之为「就地部署」。通过用就地部署取代原来的 push_back 或 insert 之类的操作,不再需要先构造再传递,而是由容器直接调用目标对象的构造函数来完成数据填充。

在某些情况下(T 提供了对应的构造函数时),这样可以避免一次拷贝构造的开销。而最差的情况(T 没有提供对应的构造函数),也最多不过就是与 push_back 和 insert 效果一模一样而已。所以我建议所有能用上就地部署的地方,都统统用上,无需太多考虑。

并且,就地部署与 move 语义相互并不冲突,而且是互有补充。move 语义解决深拷贝慢的问题,就地部署试图减少哪怕是浅拷贝的执行次数。两者配合起来效果更加完美。

散列表
在 C++11 里,不再需要通过第三方库来引入散列表(或者叫哈希表)了。STL 正式支持了四种散列表的实现,全部都冠以「unordered_」的前缀,以便与一些第三方实现相区别。

对于大多数用 map / set 实现的代码,只要简单替换容器就可以得到性能上的提升。map / set 基于红黑树(自平衡二叉树),时间复杂度至少是 log(N)。散列表版本的 map / set 提供常数级的时间复杂度,随着数据量的增大,无论是写入还是读取的性能都超过了红黑树版本的 map / set。

我个人的测试结论是:同是 set<string>,即使是小数据量,散列表版读取代价也只是红黑树版的约 60%;小数据量下,红黑树版写入略快,但在容器内数据量达到「万」级别的时候,散列表的写入速度也开始超越红黑树版(此为 Release 版测试结论,Debug 版在「百」级别即发生超越现象)。

所以我认为,只有在数据量很小,并且写入与读取的概率大致相当时,使用红黑树版 map / set 才在性能上可能有明显收益。其余情况,都建议采用散列表版本 map / set。当然,如果 T 是自定义类,并且你不愿意为它写散列函数,那就算了。


非性能优化部分

完美转发
C++11 中所谓「完美转发」的特性,其实是配合右值引用来使用的。如果为了支持右值引用,而不得不让自己的代码量变大一倍,那有些人可能就要望而却步了。完美转发其实是借用了模板技术,使得你可以只写一份代码,就可以兼顾(常量)左值引用与右值引用的情况。工作量更少,代码更简洁,出错的概率也就更低。

不过,采用模板技术的缺点就是:编译期展开。这一方面降低了编译器的效率,另一方面会导致头文件的包含关系变得不太容易整理。除此之外,还有一种我称之为「不完美转发」的替代解决方案,本质上是在性能上作出一定程度上还算可以接受牺牲,来换取代码简洁性,取得一个还算 OK 的平衡。我会另外写一篇 Blog 来介绍一下它。

类型推导
「类型推导」也就是所谓的 auto 类型。这个东西使用起来基本没有门槛。很多人可能最开始接触 C++11 就是通过它了。

这个的确是一个好东西,用来写 STL 的 iterator 类型再合适不过了。因为我们本来也不怎么关心 iterator 的具体类型。不过,仍然不建议滥用。如果到处都是 auto,阅读你代码的人会经常性地需要回顾才能知道一个变量的类型,特别是在你没有用匈牙利命名法的时候更是如此。

所以,我的建议是:当你觉得一个变量的类型写起来很麻烦,而你其实并不关心它的时候,放心地用 auto。并且,auto 变量的作用域不要太大,if / for / while 循环内的局部变量用它是最合适的。

基于范围的 for 循环
很多语言早就可以这样写了。而 C++11 现在也可以这样写了:
for (auto& stk : stocklist)
相比起:
for (auto pIter = stocklist.begin(); pIter != stocklist.end(); ++pIter)
孰优孰劣一目了然。何况后者通常还需要跟一句:
auto stk = (*pIter);
不过,如果是一个 map,你可能经常要取 pIter->first / second 之类。或者你打算在循环里面对 pIter 做 erase 操作,那还是用传统方式比较好。

空指针
用 nullptr 取代 NULL。我觉得最大的好处就是 nullptr 的颜色没有 NULL 扎眼。不过,由于 NULL 也表示 0,有的时候也表示无效句柄。我觉得对于所有指针类型的 NULL,置换成 nullptr 可能会对阅读代码有一定帮助。

角括号
C++11 的编译器现在可以识别 >> 到底是两个模板类的嵌套,还是 >> 运算符。因此写代码的时候就不特意空上一格,写多层模板类嵌套的时候就更美观一点。

不过,多层模板类嵌套,本来就不可能「美观」到哪里去。起码我是不建议太多此类的代码实践的。

初始化列表
vector 可用这样的方式来进行初始化:
vector<int> vecX = { 1, 2, 3, 4 };
的确是比以前省事了。也就是说,C-Style 数组的存在意义又少了一层。

统一初始化
struct 可以被这样初始化:
struct C
{
    int a;
    int b;
    int c;
};
C c{1, 2, 3};
class 的 public 成员也可以。
在某些喜欢使用各种结构体的代码中,这个特性可以让你少写一大堆构造函数。

通用智能指针
std::shared_ptr<T>,强在可以指向任意对象,缺点也由此而生:由于引用计数保存在 shared_ptr 中,因此对智能指针的赋值操作是线程不安全的。这个问题,有一篇Blog论述,我觉得写得不错,就直接引用不细讲了。从原理和测试数据来看,我认为这篇 Blog 是靠谱的。

所以,虽然 shared_ptr 很强大,但使用场合需要注意:单线程随便用。多线程下,赋值过程要注意。单对单没啥问题,最好不要出现左值右值交叉的情况(一个线程在 A = B,另一个线程在 B = C)。若因业务需求无法避免的话,要考虑当作临界资源加锁保护。实在不行,就写一个专用智能指针,把引用计数放在 T 里面,加锁保护,就不会有问题了。

正则表达式
与散列表类似,不再需要第三方实现,现在 C++11 也直接支持正则表达式了。我以前要找一个 Unicode 支持得好的 Regex 库真的是苦水一堆,现在有了官方支持真的是太好了。

2017-05-20

SDL 检查不报错事件调查报告

Visual Studio 2013 VC 项目默认是启用 SDL 检查的。通常而言,这会使得一些「过期」函数在编译时被报告 Error。比如 strcpy 和 inet_addr 之类都会遇到这个问题。

理论上讲,这些过期函数的确不安全,或者说容易被不安全地调用。微软也很「贴心」地在编译器的报错信息中给出了解决方案,比如用 strcpy_s 和 InetPton 来替换,都不用你去搜解决办法了。所以按照我的习惯,一般是就地解决掉这些问题再往下走。

不过呢,可能有的人性子比较急,也有的时候是从旧项目移植,想快点编译完先跑一下看看效果。改代码毕竟要时间,从 strcpy 改成 strcpy_s 可能还好,而从 inet_addr 改到 InetPton 就真的没有想象中那么轻松。所以编译器也给出了另一种建议,你可以设上几个 Macro,SDL 也是可以被忽略的。

到此为止都还比较和谐,大家都是有商有量地做事情。然而当我腾出时间准备把过期函数扫扫干净,把同事临时加的 Macro 去掉之后一编译,问题来了——SDL 这次怎么不报错了?

再三确认过 vcxproj 中已经没有了相关的 Macro,并且 SDL 的确是打开了。但这次编译就是不会报错,似乎对我面前的 strcpy 视而不见。为什么呢?

Google 上搜了一大圈也没有方向。最后还是被我排除法硬试出来的——平台工具集如果选了 v120_xp,那么 SDL 即使打开,有些过期函数也不会报错。是不是 SDL 就此失效,我不清楚,因为我没法去覆盖所有的过期函数。我估计,微软是这样想的:你既然打算让这个程序跑在过期的 OS 上,那函数过期不过期已经不重要了。

其实还有一个更重要的原因:strcpy_s 还好,但要是把 inet_addr 真的换成了 InetPton,你就会发现在 WinXP 下你的程序根本就跑不起来。实际上,WinXP 就不支持 InetPton。MSDN 上的信息表明,最低也要 Vista 才可以。

我们的程序暂时还不能抛弃 WinXP 用户,但若要完全不知道用了哪些过期函数我又心有不甘,于是我打算做一个 #if……#else……#endif 来解决这个过期 OS 兼容的问题。搜了一下,正确的姿势应该是:
#if (_WIN32_WINNT >= 0x602)
#else
#endif
最后我是在 Debug 版本上用了平台工具集 v120,在 Release 时还是用了 v120_xp。过期函数只会局限在以上 Macro 范围内,有限度地使用。

另外,WinXP 也不支持条件变量 CONDITION_VARIABLE,所以这个服役期超长的操作系统是真的应该淘汰了。我不得不说一句:WannaCry,干得好!

2017-03-29

对 VS2013 下 C++11 的精准转发与通用引用的一点研究

在 C++11 中,允许用以下方式编写模板函数:
#include <iostream>
#include <string>
class A
{
public:
    template <typename T>
    void foo(T&& t)
    {
        T _t = std::forward<T>(t);
    }
};
int main()
{
    std::string s1 = "test";
    A a;
    a.foo(s1);
    std::cout << "1:" << s1 << std::endl;
    a.foo(std::move(s1));
    std::cout << "2:" << s1 << std::endl;
    a.foo("ok");
}
以上代码在 Visual Studio 2013 上测试通过。输出是:
1:test
2:
可以看到,模板函数 A.foo 只有一个声明和实现,但既可以接受左值,也可以接受右值。并且当 S1 被当作右值引用传入的时候,其值是确确实实被「丢弃」了。这就是所谓的精准转发(把 T 的类型准确地传递到使用者),以及通用引用(用一个 T&& 就可以表示所有的情况)。对于要写库的程序员来说,可谓是一个福音了。
然而,对于模板类,下面的写法看上去很好,但是编译会报错的:
template <typename T>
class A
{
    T _t;
    public:
    void foo(T&& t)
    {
        _t = std::forward<T>(t);
    }
};
int main()
{
    std::string s1 = "test";
    A<std::string> a;
    a.foo(s1);
    std::cout << "1:" << s1 << std::endl;
    a.foo(std::move(s1));
    std::cout << "2:" << s1 << std::endl;
    a.foo("ok");
}
编译后报错:
error C2664: “void A<std::string>::foo(T&&)”: 无法将参数 1 从“std::string”转换为“std::string&&”
with
[
    T=std::string
]
无法将左值绑定到右值引用
这大概是因为,T 的类型在 A<std::string> 的时候就确定了,因此编译器无法进行更多的类型推导。
那么怎么办呢?其实也不难,foo  函数像下面这样写就可以了:
template <typename T>
class A
{
    T _t;
    public:
    template <typename X>
    void foo(X&& t)
    {
        _t = std::forward<X>(t);
    }
};
在函数模板中用一个新类型就可以了。如果 T 跟 X 不一致,那么编译器反正会检查出来的。不用担心。
还有一个问题:有的时候我们会把 foo 的实现写在 class 的外面。那这个时候怎么办呢?
我本来想抛题目给大家去做。不过都到最后了卖关子也没什么意思,还是直说吧:
template <typename T>
class A
{
    T _t;
    public:
    template <typename X>
    void foo(X&& t);
};
template <typename T>
template <typename X>
void A<T>::foo(X&& t)
{
    _t = std::forward<X>(t);
}
标红的两行,只能是这个顺序。类的模板定义在上面,函数的模板定义在下面。颠倒过来,报错。要写在一行也可以,先左后右就行。但是要想把尖括号打开强行并成一句,报错。

2013-01-25

VC6 最佳拍档,Platform SDK Feb 2003

很长一段时间内,一直用 Platform SDK Server 2003 SP1 来搭配 VC6 进行开发工作。但这个组合其实并不是很合适。这个版本的 Platform SDK 里面,有一些东西是 VC6 的 CRT 已经无法支持的,比如对 __time64_t 进行操作的那些时间函数。另外在链接到 shell32.lib 之类的库的时候,也会报错说调试信息的格式已经不匹配了。

最后一个支持 VC6 的 Platform SDK,是 Platform SDK Feb 2003,比我之前用的 SDK 版本要更旧一点。微软官网已经不提供下载了,目前给出的所有链接都是失效的。好在有网友通过 独立Blog 提供了 cab 包方式的下载。我把这些 cab 包中打包的文件提取出来后制作成了 ISO 光盘镜像,便于刻盘保存,也更方便下载。

以下下载链接来自于 Mega 网盘。下载无需注册,但国内下载速度不太好说,有快有慢,并且(从 Mega 的性质看来)随时可能被墙。可以尝试一下 ping mega.co.nz,如果 ping 值高于 600ms,建议通过 VPN 下载(GoAgent对于Mega不顶用),或者另寻它途。
Platform SDK Feb 2003.iso (346.8 MB)

值得一提的是,这个版本的 SDK 需要你的默认浏览器是 IE 才行。貌似通过 ActiveX 控件实现的安装向导,以后的版本没有继续采用,明显是一个失败的设计。

2012-10-19

一个关于浮点运算的坑

这两天遇到一个很奇怪的问题,后来通过排除法终于把故障点缩小到一句代码,但还是很奇怪:

某个同事以前用 VC 写了一个 DLL 用于提供某类通用计算,相当于一个计算模块,由我这边写 BCB 程序来调用。不过在这次的问题中发现,一旦线程调用过 pow() 这个 C 库函数来计算过一个非整数指数的幂,那么这个计算模块接下来同样的参数再次用就会得出不一样的结果。调用过 pow() 之前是一种结果,调用之后是另一种结果,现象相当稳定。

诡异就诡异在 pow() 的返回值既没有错,也没有被采用过。比如仅仅一句:
pow(2.0, 3.1);
函数返回值根本没有用来干过任何事情,相当于直接丢掉了。那么按道理来说这行代码应该不会对之前或之后的代码造成任何影响。但它确确实实影响了。
然而指数如果是整数,就不会,比如:
pow(2.0, 3.0);

那个写 DLL 的同事对此也是一头雾水。怀疑点一度被放在 VC / BCB 身上,因为它们的 CRT 不一样。但因为正好有一个测试工具,稍加改造便可以针对计算过程输出详细的结果报表,因此用来比较了一下,发现了问题:出问题的地方,有两个本来应该用来比较的 double 型输入数据正好是相等的。在 pow() 调用之前,它们的确被判断为相等。但调用 pow() 之后,计算结果显示它们被判断为不相等了。

这种事情对于常写浮点运算相关代码的程序员而言是很容易引起警惕的。浮点数不能直接用 == 之类来比较,必须去判断两数相减的绝对值是否小于某个精度。所以将这个测试结果提交给写 DLL 的同事之后,很快就定位并解决了问题。
但这里我更关注的是以下几个问题:

  1. pow(2.0, 3.0) 和 pow(2.0, 3.1) 的不同,使我相信 CRT 肯定对前者作了优化。这种事情,想得通,但后果可能是个坑。
  2. 很明显,CRT 在 pow() 被调用之后,处理浮点数时的行为模式改变了。通过观察 _statusfp() 的返回值,我发现调用前为 0,调用后变成了 0x20。但这个 0x20 是一个 Undocumented 的东西,哪怕是最新的 MSDN 上也找不到。并且我通过 _fpreset() 将状态字变回了 0,但计算模块仍然会出错,说明应该还有别的东西也被改了。这个坑是不是 VC 和 BCB 联合挖的,目前还不知道。
  3. 如果不知道有这个坑,就可能导致一些大麻烦。在特定的代码逻辑中,这个问题很难通过黑盒测试发现。它可能在很长一段时间内都能工作得很好,直到某一次有个用户算了一个指数带小数点的幂,然后……一切就不一样了。这简直就是逻辑炸弹嘛!让我想起了《深渊上的火》里面可怜的蓝荚和绿茎。
  4. MSDN 真心不是完全靠得住的。

在本文最后,还要对提供过重要帮助的 Libin Yan 表示感谢!并感谢所有关注和评论过这个 PO 的 G+ 网友!

2011-03-29

CWebBrowser2 打开 PDF 后退出时崩溃问题的解决

用嵌入在对话框中的 CWebBrowser2 控件打开 PDF 文档后,主程序退出时抛了 Access Violation。扔异常的是 ACRORD32.DLL,ADOBE 自己的玩意儿,调用栈中也看不出什么,主程序看来都快退完了,显然和什么东西没关干净有关。

照例,国产没货。最后在这里找到了:
以上方法,在 VC6 sp6 / WinXP 的 Debug 和 Release 编译上都试过。除此之外就不知道了。
据说,问题和 Adobe Reader 9 有关。V8 没有这个问题,因为 V8 是单一实例,而 V9 不是。照此一来,内存泄漏可能也是难免的。