STL 字符串类与 UNICODE

C++ Q&A 专栏...

原著:Paul DiLascia
翻译:James Liu

原文出处:MSDN Magazine Aug 2004 (C++ Q&A)
原代码下载: CQA0408.exe (234KB)

  1. GetKeyState 使用示例
  2. STL 字符串类与 UNICODE
  3. 如何向C#或.NET框架暴露 C++ 对象?
  4. 如何获取专用文件夹的路径名?

我想让用户双击程序图标时按住 Control 键,以一种特殊的方式来启动程序。 但::GetCommandLine 和__argc 均没有任何反应,用 MFC 中的 CCommandLineInfo 似乎也是如此。有没有一种方法可以解决这个问题呢?

Dean Jones
有,非常简单。你所要做的就是调用 GetKeyState。当你正在处理的当前消息被发送时, 该函数返回虚拟键的状态。这个状态可能是弹起,按下,或者套索钉。套索钉用于大写锁定( Caps)和转换锁(Shift Lock),它们可以转换状态。对于一般的 键,如控制键(VK_CONTROL),如果键被按下,则其状态的高位标识位为 1。
  许多的应用程序使用 Control+F8 作为特殊键来启动恢复模式。比如,如果应用程序允许用户 定制工作间,那么Control+F8就可以将其恢复到初始的默认设置,只是在恢复之前一定要让用户进行确认。做的更好一点的话,你可以在单独的INI文件中保存用户的设置,这样用户 有机会恢复它们。不管怎样,要想在程序启动时检查 Control 键,你可以像下面这样写:
	if (GetKeyState(VK_CONTROL)<0)
	{
	  // enter special mode
	}
Figure 1 给出了一个基于 MFC 的示例程序代码段,你可以通过本文顶端的链接进行下载,如果用户在启动程序的时候按下Ctrl+F8,它将显示一个消息框,并且 发出蜂鸣声。如果你只是想检查 Control键,可以忽略对 VK_F8 键的测试。

我经常在 C++ 程序中使用标准模板库(STL)的 std::string 类,但在 使用 Unicode 时碰到了问题。在使用常规 C 风格的字符串时,我可以使用 TCHAR 和 _T 宏,这样针对 Unicode 或 ASCII 均可以进行编译,但我 总是发现这种ASCII/Unicode的结合很难与 STL 的 string 类一起使用。你有什么好的建议吗?
Naren J.
是的,一旦知道 TCHAR 和_T 是如何工作的,那么这个问题很简单。基本思想是 TCHAR 要么是char,要么是 wchar_t,这取决于 _UNICODE 的值:
	// abridged from tchar.h
	#ifdef  _UNICODE
	typedef wchar_t TCHAR;
	#define __T(x) L ## x
	#else
	typedef char TCHAR;
	#define __T(x) x
	#endif
  当你在工程设置中选择 Unicode 字符集时,编译器会用 _UNICODE 定义进行编译。如果你选择MBCS(多字节字符集),则编译器将不会带 _UNICODE 定义 。一切取决于_UNICODE 的值。同样,每一个使用字符指针的 Windows API 函数会有一个 A(ASCII) 和一个 W(Wide/Unicode) 版本,这些版本的 实际定义也是根据 _UNICODE 的值来决定:
	#ifdef UNICODE
	#define CreateFile CreateFileW
	#else
	#define CreateFile CreateFileA
	#endif
  同样,_tprintf 和 _tscanf 对应于 printf 和 scanf。所有带"t"的版本使用 TCHARs 取代了chars。那么怎样把以上的这些应用到 std::string 上呢?很简单。STL已经有一个使用宽字符定义的wstring类 (在 xstring 头文件中定义)。string 和 wstring 均是使用 typedef 定义的模板类,基于 basic_string, 用它可以创建任何字符类型的字符串类。以下就是 STL 定义的 string 和 wstring:
	// (from include/xstring)
	typedef basic_string< char, 
	  char_traits< char >, allocator< char > >
	  string;
	typedef basic_string< wchar_t, 
	  char_traits< wchar_t >, allocator< wchar_t > > 
	  wstring;
  模板被潜在的字符类型(char 或 wchar_t)参数化,因此,对于 TCHAR 版本,所要做的就是使用 TCHAR 来模仿定义。
	typedef basic_string< TCHAR, 
	  char_traits< TCHAR >, 
	  allocator< TCHAR > > 
	  tstring;
  现在便有了一个 tstring,它基于 TCHAR——也就是说,它要么是 char,要么是 wchar_t,这取决于 _UNICODE 的值。 以上示范并指出了 STL 是怎样使用 basic_string 来实现基于任何类型的字符串的。定义一个新的 typedef 并不是解决此问题最有效的方法。一个更好的方法是基于 string 和wstring 来简单 地定义 tstring,如下:
	#ifdef _UNICODE
	#define tstring wstring
	#else
	#define tstring string
	#endif
  这个方法之所以更好,是因为 STL 中已经定义了 string 和 wstring,那为什么还要使用模板来定义一个新的和其中之一一样的字符串类呢? 暂且叫它 tstring。可以用 #define 将 tstring 定义为 string 和 wstring,这样可以避免创建另外一个模板类( 虽然当今的编译器非常智能,如果它把该副本类丢弃,我一点也不奇怪)。[编辑更新-2004/07/30:typedef 不创建新类,只是为某个类型引入限定范围的名称,typedef 决不会定义一个新的类型]。不管怎样,一旦定义了 tstring,便可以像下面这样编码:
	tstring s = _T("Hello, world");
	_tprintf(_T("s =%s/n"), s.c_str());
  basic_string::c_str 方法返回一个指向潜在字符类型的常量指针;在这里,该字符类型要么是const char*,要么是 const wchar_t*。
  Figure 2 是一个简单的示范程序,举例说明了 tstring 的用法。它将“Hello,world”写入一个文件,并报告写了多少个字节。我对 工程进行了设置,以便用 Unicode 生成 debug 版本,用 MBCS 生成 Release 版本。你可以分别进行编译/生成并运行程序,然后比较结果。Figure 3 显示了例子的运行情况。


Figure 3 运行中的 tstring

  顺便说一下,MFC 和 ATL 现在已经联姻,以便都使用相同的字符串实现。结合后的实现使用一个叫做 CStringT 的模板类,这在某种意义上 ,其机制类似 STL 的 basic_string,用它可以根据任何潜在的字符类型来创建 CString 类。在 MFC 包含文件 afxstr.h 中定义了三种字符 串类型,如下:
	typedef ATL::CStringT< wchar_t, 
	  StrTraitMFC< wchar_t > > CStringW;
	typedef ATL::CStringT< char, 
	  StrTraitMFC< char > > CStringA;
	typedef ATL::CStringT< TCHAR, 
	  StrTraitMFC< TCHAR > > CString;
CStringW,CStringA 和 CString 正是你所期望的:CString 的宽字符,ASCII 和 TCHAR 版本。
  那么,哪一个更好,STL 还是 CStirng?两者都很好,你可以选择你最喜欢的一个。但有一个问题要考虑到:就是你想要链接哪个库,以及你是否已经在使用 MFC/ATL。从编码 的角度来看,我更喜欢 CString 的两个特性:
  其一是无论使用宽字符还是char,都可以很方便地对 CString 进行初始化。
	CString s1 = "foo";
	CString s2 = _T("bar");	
  这两个初始化都正常工作,因为 CString 自己进行了所有必要的转换。使用 STL 字符串,你必须使用_T()对 tstring 进行初始化,因为你 无法通过一个char*初始化一个wstring,反之亦然。
  其二是 CString 对 LPCTSTR 的自动转换操作,你可以像下面这样编码:
	CString s;
	LPCTSTR lpsz = s;
  另一方面,使用 STL 必须显式调用 c_str 来完成这种转换。这确实有点挑剔,某些人会争辩说,这样能更好地了解何时进行转换。比如, 在C风格可变参数的函数中使用 CString 可能会有麻烦,像 printf:
	printf("s=%s/n", s); // 错误
	printf("s=%s/n", (LPCTSTR)s); // 必需的	
  没有强制类型转换的话,得到的是一些垃圾结果,因为 printf 希望 s 是 char*。我敢肯定很多读者都犯过这种错误。防止这种灾祸是 STL 设计者不提供转换操作符的一个毋庸置疑的理由。而是坚持要你调用 c_str。一般来讲,喜欢使用 STL 家伙趋向于理论和学究气,而 Redmontonians(译者:指微软)的大佬们则更注重实用和散漫。嘿,不管怎样,std::string 和 CString 之间的实用差别是微不足道的。

我正在试图用托管扩展和互用性(interop)向 C# 和 .Net 框架暴露我的 C++ 库。我的一个类中含有一个联合(union)类型,但 .Net 似乎并不支持这种类型:
	class MyClass
	{
		union
		{
			int i;
			double d;
		};
	};
  使用联合旨在节约空间,因为我知道int和double是绝对不可能同时使用的。同时,我的很多代码都引用了此联合类型,而且我不想更改它们。请问我怎样把这个类暴露给.Net呢?我是不是必须把联合中的每个值分别定义为成员变量,或者使用一个成员方法?
John Bunt
你可以使用这些方法中的任意一个,但不一定非要这么做。在 .Net 的互用性机制中,总有 办法来很好地暴露C++对象——这么说吧,几乎总有办法。公共语言运行时(CLR)不能理解联合类型,但可以用某些特殊技巧的常规 __value struct 来告诉 它成员在哪里。这个神奇的属性就是 StructLayout 和FieldOffset。在托管 C++ 中,它看起来象这样:
	[StructLayout(LayoutKind::Explicit)]
	public __value struct MyUnion {
	  [FieldOffset(0)] int i;
	  [FieldOffset(0)] double d;
	};
  这段代码告诉 CLR,整数i和浮点数d均处于结构的零偏移处(也就是说是第一项),这样便使它们交迭,其效果就是把一个结构变成了联合。这样便可以在 __gc 类中 使用 MyUnion,像这样:
	public __gc class CUnionClass {
	public:
	  // 可以直接存取,因为它是 public 类型
	  MyUnion uval;
	};
  有了 CUnionClass 的定义,便可以在任何 .Net 语言中通过 uval 直接存取成员i和d。在C#中,它看起来像下面这样:
	CUnionClass obj = new CUnionClass();
	obj.uval.i = 17;
	obj.uval.d = 3.14159	
  我写了一个名为 MCUnion 的小程序,它实现了一个托管C++库,它包含前面所示的 CUnionClass,还有一个用于测试这个C++库的 C# 程序 utest(参见 Figure 4和 Figure 5)。CUnionClass 示范了如何为联合成员添加属性,这样你就可以通过 obj.i 和 obj.d,而不是 obj.uval.i 和 obj.uval.d 来 存取值。依照你的设计,这可能是,也可能不是你所想要的结果。如果你愿意,你可以将 uval 设置为 private 或者protected 类型,这样客户端就必须使用属性。这将完全隐藏 uval 的联合 实质特性。测试程序通过联合本身和属性 i 和 d 两种方式都可以存取 i 和 d。

我正在写一个 DirectX 屏保,需要在用户进行屏保设置之前,将从用户 My Pictures 目录下获得JPG,BMP,GIF 和 TGA 文件列表 作为一种默认设置并自行加载它们。将图像纹理设置到 DirectX 中没有什么问题,但我有点担心的就是不同的用户其 My Pictures 目录可能会不 同。在我的机器上,这个路径为“C:/Documents and Settings/Administrator/My Documents/My Pictures”。有没有一个简单的方法获得 My Pictures 的位置呢?

是的,有一个简单的方法。你需要的函数是 SHGetSpecialFolderPath,它可以通过一个预定义的ID,如 CSIDL_MYPICTURES 来找到 对应的专用文件夹,该函数被定义在 ShlObj.h中,其中还定义了很多其它的外壳元素。比如:
	TCHAR buf[MAX_PATH];
	SHGetSpecialFolderPath(NULL, // HWND
		buf,
		CSIDL_MYPICTURES,
		NULL); // don''t create
	
  应该总是使用 SHGetSpecialFolderPath 获得专用文件夹的名称(而不是直接搜寻注册表),因为它保证可以在所有版本的 Windows 系统中工作, 包括未来的版本,即便微软的大佬们修改存储专用文件夹路径的注册表键值。对于 Windows 2000 和 Windows XP来说,SHGetSpecialFolderPath 在shell32.dll中。而 Windows 9x 和 Windows NT 等较旧版本不含 SHGetSpecialFolderPath,但Microsoft 通过一个专门的 DLL 提供——SHFOLDER.DLL,你可以 随自己的应用程序免费分发这个DLL文件。
  事实上,来自 Redmond 的官方文档如是说:“鼓励软件供应商尽可能多地重新分发 SHFOLDER.DLL 文件,以便支持 Windows 2000 以前 各个版本。”唯一需要注意的是:如果你的应用程序是面向旧版本的 Windows 操作系统,但是在 Windows 2000 或 Windows XP上生成的, 那么你必须显式的链接 SHFOLDER.DLL;否则链接器将从 Shell32.dll 中得到 SHGetSpecialFolderPath。
  既然这是一个 C++ 专栏,所以我写了一个叫做 CSpecialFolder 的小类(参见 Figure 6),它从 CString 派生,并会自动调用 SHGetSpecialFolderPath。 其使用方法如下:
	CSpecialFolder mypics(CSIDL_MYPICTURES);
	LPCTSTR lpszPath = mypics;
	
  这样赋值是行得通的,因为 CSpecialFolder 从 CString 派生而来,它含有一个隐式的到 LPCTSTR 的转换操作符。CSpecialFolder 可以 从下载包中得到,附带有一个测试程序,它可以显示所有在 ShlObj.h 文件中有 CSIDL_XXX 定义的专用文件夹路径名称。其中包含大家熟悉的文件夹,如 :Favorites(收藏夹),Fonts(字体),Programs(程序),History(历史),AppData(应用程序数据)——以及一些 奇怪的文件夹,比如:CSIDL_BITBUCKET(回收站),CSIDL_INTERNET(我想是指 Microsoft IE图标的路径),还有 CSIDL_SYSTEMX86(在 RISC/Alpha For Windows 2000 上,x86 的系统目录)。

向 Paul 提问和评论请发到 [email protected].
 

作者简介
  Paul DiLascia 是一名自由作家,顾问和 Web/UI 设计者。他是《Writing Reusable Windows Code in C++》书(Addison-Wesley, 1992)的作者。通过 http://www.dilascia.com 可以获得更多了解。  
本文出自 MSDN Magazine 的 August 2004 期刊,可通过当地报摊获得,或者最好是 订阅

本文由 VCKBASE MTT 翻译  

你可能感兴趣的:(C++,windows,String,Microsoft,basic,mfc)