Chinaunix首页 | 论坛 | 博客
  • 博客访问: 6062946
  • 博文数量: 1227
  • 博客积分: 10026
  • 博客等级: 上将
  • 技术积分: 20273
  • 用 户 组: 普通用户
  • 注册时间: 2008-01-16 12:40
文章分类

全部博文(1227)

文章存档

2010年(1)

2008年(1226)

我的朋友

分类: C/C++

2008-03-17 11:18:48

下载本文示例代码

许多 C++ 爱好者已经对我最近的专栏中渗入了太多关于C#的内容表示关注。我承认这一点!我唯一的辩解是:由于 Microsoft® .NET Framework 已经获得广泛的认同,给我发送关于C#问题的读者越来越多,同时因为C# 和 C++ 如此类似,所以我就回答了一部分他们的问题。这不是我有意疏远 C++ 爱好者——上帝知道,我就是他们中的一员啊!不管怎样,为了突出重点,从这个月开始的 C++ 专栏将更多地专注于 C++ 的内容,包括托管扩展以及 MFC 这样的传统内容。因此提出你的 C++ 问题吧!我特别鼓励你提出关于托管 C++ 的问题。你使用它的时候有些什么体会?

在你 2004 年 3 月 的专栏中,你通过重定义 WM_USER+1 实现 了 CMyOpenDlg 的初始化。我认为在通常意义上你误用了 WM_USER 的范围(它是保留给所有 RegisterClass 使用者的),此外还错在 WM_USER+1 已经 是一个预定义的对话框消息 DM_SETDEFID。你不应该再对这个消息用不同的值了吧?

Jeff Partch

你说得完全正确!WM_USER 是为所有实现窗口类的人保留的——无论是你,还是友好的 Redmondtonians(译注:Microsoft), 仰或是 Gleepoid 行星上的叛逆者。Figure 1 展示了正式的 Windows 消息值的细目分类,对此每个人都应该至少每十年复习一次。WM_USER 到 0x7FFF 是为私有窗口类保留的。你可以将这个范围 认为是在特定的窗体类中有意义的专用消息。举个例子,状态栏控件的 SB_SETTEXTA 使用 WM_USER+1。同时正如你所指出的一样, 对话框的 DM_GETDEFID 和 DM_SETDEFID 使用 WM_USER+0 和 WM_USER+1。我在 2004 年 3 月的专栏中使用 WM_USER+1 是与 DM_GETDEFID 相冲突的。
  想要定义其自己消息的应用程序应该使用 WM_APP。WM_APP 是确保不会与系统(WM_CREATE 等等)或类/特定控件消息如 DM_GETDEFID 相冲突的。Windows 定义 的 WM_APP 如下:

#if (WINVER >= 0x0400)
#define WM_APP 0x8000
#endif  

  正如每个 Windows 极客(Geek)所知道的那样,WINVER 0x0400 是指 Windows 95、Windows 98 和 Windows NT。所以 WM_APP 的使用还不到十年,这解释了为什么我没注意到它——在 2005 年之前,我不应该对下一个十年的消息范围妄加评论!
  但是我真的要为 CMyOpenDlg 使用 WM_APP 吗?CMyOpenDlg 介于 Windows 和应用程序之间。我把它写成一个扩展 ,以便其他程序员可以在其应用程序中使用。如果某个程序员已经在使用 WM_APP,并且是用 CMyOpenDlg,那么是否有冲突呢?在文件打开对话情况中这是不太可能的——但如果我正在写一个自定义控件 ,比如工具栏或进度条并需要定义自己的消息时该怎么办呢?我可以让他们基于 WM_APP 来避免和基本控件冲突(但是这时存在与应用程序冲突的风险)或者我可以选一个 消息号如:WM_USER+400,它远远超出 Windows 控件消息范围。不幸的是,这里没有正确的答案。这是一个自 Windows 发布以来就一直 折磨着控件和类库设计者的问题。简直就没有一个十全十美方法来划分消息空间而保证绝不导致冲突。你每次都必须根据经验来采取决定。对于 CMyOpenDialog,我 将选择 WM_USER+10 这样的消息,它不会与 DM_XXX 消息冲突。用注册消息可能又太夸张了。
  说到注册消息,WM_APP 取值范围和已注册消息取值范围(0xC000 - 0xFFFF)有什么不同,什么时候应该选用其中一个而不是另一个? 如果你只是在对自己说——就是,如果你打算仅仅将发送消息到自己应用程序的窗口中,你可以使用 WM_APP 取值范围。注册消 息则是全局消息,用于发送到别的程序员写的其它应用程序。举个例子,如果你正在写一个协作-管理应用程序,它自己能通过发送一个特定的消息来 确定具有相似意向的程序,这时你应该使用注册消息:

UINT WM_SAYHELLO = RegisterWindowMessage(_T("WmSayHello"));

  这时其它协作应用程序可以用这个特定的名称“WmSayHello”注册并获得相同的值,于是他们都可以互相通信。每个 Windows 会话实际的 WM_SAYHELLO 值 将会各不相同,但在某一个对话中,注册它的所有应用程序将具有相同的值。当然,该值将总在 0xC000 - 0xFFFF 范围之内。糟了——MSDN® 杂志有最精明的读者!

我正用托管 C++ 写一个应用程序并且碰到一个关于字符串 文字量的问题。我知道我能用_T("sample string") 或 S"Sample String" 创建一个字符串。我也知道 S 字符串文字量更有效率,但好象很少有人知道它为什么有效以及效率高在哪。两种类型的字符串 有何差别?托管字符串文字量确切含义是什么,以及为什么它更好?

Randy Goodwin

理解托管字符串文字量和普通C/C++ 字符串文字量之间差别的最好方法是用它们写相同的代码并考察其编译结果。我写了一个简单的托管 C++ 程序,StrLit.cpp,这个程序用C++和.NET风格创建了一些 String 文字量,代码如 Figure 2 所示。Figure 3 和 Figure 4 展示了用 ILDASM 反汇编的 Microsoft 中间语言(MSIL)代码。正如你可能知道的许多事情一样,其首要差别就是当你使用 C++ 文字量时,编译器产生一个对相应 String 构造函数的调用:

// String* c2 = new String(_T("This is a TCHAR string"));
ldsflda valuetype $ArrayType$0x11197cc2 
   modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) 
   ''?A0x1f1e2151.unnamed-global-0''
newobj instance void [mscorlib]System.String::.ctor(int8*)      
  我会马上解释这段冗长而令人费解的文字,但其基本思路是编译器加载了 C++ 字符串文字量的地址,然后产生一个 newobj 指令,该指令将文字量地址到 String 的构造函数。另外,编译器为非托管字符串文字量创建了一个专门的 Array 类型——这就是前面代码段中样子奇怪的 $ArrayType$0x11197cc2,还有一个 被命名为?A0x1f1e2151.unnamed-global-0 的静态实例,如图 Figure 3 所示:


Figure 3 Disassembler 中的 StrLit

  如果你花点时间看看 MSIL,你将发现 modopts 无处不在。一个 modopt 的完全描述已超出了本栏目范围,但是其基本思路是编译器 能用 modopt 提供有关公共语言运行时无法理解的类型的附加信息,但是该程序集的其它使用者可能理解——在这种情况下,该信息就是全局常量。CLR本身没有常量的概念,但是 程序集的其它使用者可能知道。如果该程序集被引入 C++,则 C++ 编译器会知道该变量是个常量;但是如果该程序集被 C# 使用,C# 编译器将会忽略 modopt 常量,因为 C# 没有常量的概念,正如你所能看到的,当你在托管代码中使用原生类型时有许多奇怪或迷惑发生。
  当你用 S 语法使用托管文字量时,不管怎样,一切都很简单:

// String* s1 = new String(S"This is a String literal");
ldstr "This is a String literal"

  这里没有函数调用,仅有一个 ldstr 指令。这个指令将引用某个字符串文字量的新对象压入,储存在程序的元数据中。无论是用文字量创建 String 对象还是直接将它传递给需要字符串的 某些函数,都是如此:

// Console::WriteLine(S"World");
ldstr "World"
call void [mscorlib]System.Console::WriteLine(string)

  了解所发生的事情的另外一种方法是用/FAs编译程序来生成一个程序集列表。(是的,它也可以用于 MSIL)Figure 5 展示了用/FAs编译 StrLit 后经过编辑的结果,。这里有趣的是你可以看到编译器为 TCHAR字符串 "This is a TCHAR string" 生成了两个字符串($SG1883 and $SG2266),即使它们有相同的内容。然而,此处针对 S"This is a String literal" 仅有一个托管字符串 文字量 ($StringLiteralTok$70000001$),它被使用了两次。大多数程序员调用 "uniquification” 进程,但是 出于某种原因微软的家伙们(Redmondtonians)称之为“字符串暂留”(string interning)”。正如说明文档所描述的:“公共语言基础架构(CLI)确保涉及具有相同字符序列的两个元数据符号的两个 ldstr 指令的结果精确地返回相同字符串对象。”
  所有这些要点是托管文字量更快。为了弄清楚它到底有多快,我写了另一个托管 C++ 程序,StrTime,它分配了一大批字符串并报告花了多长时间。我 使用了我在 2004年6月专栏中的 ShowTime 类:

_stprintf(msg, _T("Test 1: Allocate %d strings using LPCTSTR"),num);
ShowTime st(msg);
for (int i=0; iStrTime 则使用托管 S 文字量做同一件事。下面是结果: 

C:\>StrTime 50000000
Test 1: Allocate 50000000 strings using LPCTSTR: 32797 msec
Test 2: Allocate 50000000 strings using .NET S literal: 100 msec

  Wow!正如你所看到的,S 字符串快得多。但是,嗨——等一下,这里到底发生了什么?ldstr 真的快327倍吗?这个测试值得可疑有两个原因。首先,函数调用不应该如此 昂贵;其次,我的机器只有 785MB,我真的分配了5000万个字符串吗?当然不是!因为变量s在循环内部存活,每次反复它都超出范围——这使得它满足垃圾收集 器的条件。事实上,你可以写如下的程序并且它永远循环而不会耗尽内存:

while (1) {
 String* s = new String(L"foo");
}

  在这个测试中,S 文字量表现得如此之快的主要原因是 C++ 文字量调用 newobj 来创建新的对象,尽管 S 文字量生成 ldstr,它只简单地将相同 的对象引用一次又一次地压入栈中,5000万次(记住“字符串暂留”)为了证明这一点,我修改了 StrTime (StrTime2)以显示托管 String 的实际地址。为此,你需要使用 GCHandle:

String* s = new String(S"foo");
GCHandle h = GCHandle::Alloc(s, GCHandleType::Pinned);
IntPtr ptr = h.AddrOfPinnedObject();
_tprintf(_T(" string at %p\n"), ptr.ToInt32());
h.Free();

  这创建了固定句柄,于是可以调用 GCHandle::AddrOfPinnedObject 获得固定对象的地址。下面是运行新程序的测试结果:

C:\>StrTime2 5
Test 1: Allocate 5 strings using LPCTSTR
string at 04AC21E0
string at 04AC22D8
string at 04AC2318
string at 04AC2358
string at 04AC2398
Test 2: Allocate 5 strings using .NET S literal
string at 04AC218C
string at 04AC218C
string at 04AC218C
string at 04AC218C
string at 04AC218C

  无需分配5000万个字符串;5个就足以搞定了。正如你所看到的,每次调用新的 String(_T("foo")) 5次或无 穷次来生成一个新的对象,而调用新的 String(S"foo") 加载相同的对象。因此用 C++ 文字量进行5000万字符串测试花费如此之长时间的真正原因是运行时不得不分配大量的内存并执行垃圾收集。
  所有这些可能已经比你期望知道的更多了,但是这是为了好玩我还写做另一个修改,StrTime3,它在分配字符串之前强制进行垃圾收集,这时则显示“认为被分配的”内存数量 (正如 GC::GetTotalMemory 文档里所描述的那样):

GC::Collect();
GC::WaitForPendingFinalizers();
// allocate strings
printf("TotalMemory = %d\n", GC::GetTotalMemory(false));

而这里是结果:

C:\>StrTime3 50000000
Test 1: Allocate 50000000 strings using LPCTSTR
TotalMemory = 190952, time: 33147 msec
Test 2: Allocate 50000000 strings using .NET S literal
TotalMemory = 43484, time: 110 msec

  正如你所看到的,5000万个托管 String 只占有了非常少的空间——特别是当真正只有一个时!尽管用TCHARs,垃圾收集器仅用了 190,952 字节。高等数学推理会得出5000万字符串 无法装入 191 KB,因此可以肯定地说:Framework 在垃圾收集装置里安装了一些机关。
  最后我担心我的所有测试程序都没有回答你的问题——使用托管 String 文字量到底有多快?没有微妙或纳秒粒度的计时器就不可能说清楚这个问题。但是至少我的研究可以帮助你理解表象下面 所发生的一切,从而你可以明白为什么 ldstr 比 newobj 有更加显著的效率。
  因此,你要使用哪种类型的文字量呢?事实是:用户可能绝不会注意到其中的差别。但是无论何处你调用一个使用 String 的函数最好是用 S 类型文字量。注意在 MFC 中,因为可以交替使用 LPCTSTR 和 CString,你不能将传递 String 给需要 TCHARs 的C/C++函数。那是因为托管字符串总是 Unicode (宽字符),并且从 String 到 wchar_t 没有自动转换机制。因此,你需要在 vcclr.h 中提供的 PtrToStringChars函数。下一个问题示范了一个这方面的例子。
  关于托管的和原生的字符串文字量最后要注意的一个小问题是:如果你检查在 Figure 3 和 Figure 4 中的ILDASM 代码,你会看到虽然文本 "This is a String literal" 在反汇编代码中清晰可见,而表示 C++ 文字量 "This is a TCHAR string" 的文本却无处可寻。它被隐藏在一个神秘变量中了 A0x1f1e2151.unnamed-global-0。这是否意味着 C++ 文字量提供了更大迷惑性——就是说,让你可以在众目睽睽之下隐藏你的字符串?实际并不如此,Figure 3 中的 MSIL 代码报告了神秘的 "unnamed-global-0" 是 在 "D_0000B14C."。出于好奇,我在一个十六进制查看程序中查看了 StrLit.exe,果真有这个串,确切位置在 0xB14C。Figure 6 展示了十六进制程序显示多所有东西:


Figure 6 十六进制程序显示的信息

我遇到了一个在即将出品的 C# 应用软件中保护我们的知识产权的问题。我知道所有的代码都可以被 ILDASM 反汇编,这将使得别人可以很 轻松地获得我们的数学公式。为了解决这个问题,我可以写一个 C++ DLL并使用 DllImport 在 C# 中导入函数,我更喜欢使用托管 C++ 写一个托管 __gc 类,这样我便能同时暴露属性和方法。当我 这样做并编译 DLL 时,我仍然可以使用 ILDASM 对之进行反汇编,这样并没有实现我试图做的事情。我认为 C++ 编译器会生成原生机器代码,而不是 MSIL。是 不是这样呢?

Matt Hess

当你用/clr编译 C++ 程序时,编译器将所有函数视作托管的,并默认将它们编译成 MSIL(少数情况除外,这我会在稍后讨论)。结构和类可以是托管的,也可以是非托管的,这取决于你的是否使用 __gc。如果你想将一个特殊函数编译成为原生代码(可能为了隐藏它的实现,或出于性能的原因),你可以使用 #pragma unmanaged:

   #pragma unmanaged
   // this fn and all subsequent ones are compiled 
   // to native code
   void func(/* args */)
   {
      ...
   }
   #pragma managed

  #pragma managed 指示开关返回到托管模式。Figure 7 展示了我写的一个简单的 C++程序,它有两个函数,PrintFive1 和 PrintFive2,每个 函数打印字符串 5 次。第一个函数是原生的,第二个是托管的。如果你用/FAs编译并查看所生成的 .asm 文件,你将看到在 PrintFive1 中的 for 循环编译成如下的东东:

; for (int i=0; i<5; i++) {
   mov DWORD PTR _i$1662[ebp], 0
   jmp SHORT $L1663
   ...
; etc... (more native assembly)
而 PrintFive2 中相同的 for 循环编译却是下面这样的:
; for (int i=0; i<5; i++) {
   ldc.i.0 0 ; i32 0x0
   stloc.0   ; _i$1669
   ...
; etc... (more MSIL)

  我说过当你使用/clr时,所有函数默认是托管的,但是这并不完全正确。虽然 MSIL 通常足以用来表示大多数 C/C++ 构造,但它不能表示全部 的东西。少数情况下,它强制某个函数被编译成原生代码,即便没有 #pragma unmanaged 也一样。这种情况很容易看出来:比如,如果你的函数有__asm 块,setjmp/longjmp 指令,或一个可变参数列表(varargs)。 因为 MSIL 无法处理这些结构,编译器便自动将这些函数编译为原生代码。
  你可以只用 #pragma unmanaged 来编译纯 C++ 函数——也就是说,你不使用托管类型。你不能用 #pragma unmanaged 来编译托管 __gc 类的方法(那样做没有意义)。因此,如果你想用 C++ 来隐藏你的算法,你必须用原生的 C++ 类型将他们实现成纯粹的 C++ 函数。你的 __gc 类方法可以调用这些函数,在调用你的实现之前,你必须将任何托管参数转换成非托管类型:

__gc class Widget {
public:
  MyMethod(String *s) 
  { 
    // convert managed types to C++ types
    const WCHAR __pin* pstr = PtrToStringChars(s);
    // call unmanaged C++ function (in #pragma unmanaged block)
    DoSecretAlgorithm(pstr); 
  }
};  

  托管 C++ 是微软唯一允许你在相文件中组合使用原生代码和 MSIL 代码的语言,这就是它如此强大的原因之一!另一种使用托管 C++ 混合代码的方法是通过链接。比如,无论何时你调用如 printf 或 memset 这样的 C 库函数,它们从相应的库中获得静态链接。当你使用/clr,它自动 暗示使用/MT来确保你的程序与 C 运行时库的多线程版本进行链接。CLR 需要它们,因为垃圾收集器在一个独立的线程里运行终结例程(finalizers)。

译注
  关于此问题可参考李建中译《Microsoft .NET框架程序设计(修订版)》P12“IL与知识产权的保护”小节)

给Paul发送你的问题和评论请发到 cppqa@microsoft.com

作者简介
  Paul DiLascia 是自由作者,顾问,和 Web/UI整体设计师。他是 Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992)一书的作者。你可以通过 paul 取得联系。
下载本文示例代码
阅读(1371) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~