TCHAR字符串和Win32 API函数的'A'或'W'版本是什么?

3

TCHAR 字符串是什么,比如 LPTSTRLPCTSTR,我该如何处理这些字符串?当我在 Visual Studio 中创建一个新项目时,它会为我创建以下代码:

#include <tchar.h>

int _tmain(int argc, _TCHAR* argv[])
{
   return 0;
}

例如,我如何连接所有的命令行参数?

如果我想要打开一个以第一个命令行参数给定的名称命名的文件,我该怎么做呢?Windows API定义了许多函数的'A'和'W'版本,例如CreateFileCreateFileACreateFileW;那么这些有什么不同,我应该使用哪个呢?


1
我发现自己经常在问题中写下这个问答集的要点。当这种需要出现时,我想开始使用它作为我的标准参考。欢迎任何改进;请随意编辑或添加您自己的答案。 - MicroVirus
@AdrianMcCarthy 很好的参考资料!我在搜索中没有找到过这个。 - MicroVirus
我对是否称之为重复犹豫不决。虽然它涵盖了很多相同的内容,但这个问题将更容易被许多对此感兴趣的人找到,并且答案非常出色。 - Adrian McCarthy
@AdrianMcCarthy 我不介意它是一个重复的问题。仅仅因为它是一个重复的问题,并不意味着两个问题都不好。现在这两个问题已经链接在一起,人们可以阅读两个问题并决定他们喜欢哪一个。我认为我的问题略有不同,但你的答案也很好。我尽可能地让我的问题适合新手。如果我早些时候看到那个Q&A,我会把我的答案添加到那个问题中的。 - MicroVirus
将它们链接起来更好。读者可以同时获得两者的内容。 - David Heffernan
1个回答

7

首先,我要说的是,在新的Windows项目中最好不要使用TCHAR,而应直接使用Unicode。接下来是实际答案:

字符集

我们需要了解的第一件事是在Visual Studio中字符集的工作方式。 项目属性页面有一个选项来选择所使用的字符集:

  • 未设置
  • 使用Unicode字符集
  • 使用多字节字符集

Project Property Page - Character Set

根据您选择的三个选项之一,很多定义都会改变以适应所选字符集。有三个主要类别:字符串、来自tchar.h的字符串例程和API函数:
  • '未设置'对应于使用ANSI编码的TCHAR = char,其中您使用系统的标准8位代码页来处理字符串。所有tchar.h字符串例程都使用基本的char版本。所有与字符串相关的API函数将使用API函数的'A'版本。
  • 'Unicode'对应于使用UTF-16编码的TCHAR = wchar_t。所有tchar.h字符串例程都使用wchar_t版本。所有与字符串相关的API函数将使用API函数的'W'版本。
  • '多字节'对应于使用某些多字节编码方案的TCHAR = char。所有tchar.h字符串例程都使用多字节字符集版本。所有与字符串相关的API函数将使用API函数的'A'版本。

相关阅读:关于Visual Studio 2010中的“字符集”选项

TCHAR.h头文件

tchar.h头文件是使用通用名称对字符串进行C字符串操作的帮助程序,它会根据给定的字符集切换到正确的函数。例如,_tcscat将切换到strcat(未设置),wcscat(Unicode)或_mbscat(MBCS)。_tcslen将切换到strlen(未设置),wcslen(Unicode)或strlen(MBCS)。

通过将所有_txxx符号定义为宏,可以切换到根据编译器开关评估为正确函数的函数。

其背后的思想是,您可以使用编码不可知类型 TCHAR(或 _TCHAR)和作用于它们的编码不可知函数(来自 tchar.h),而不是来自 string.h 的常规字符串函数。类似地,_tmain 被定义为 mainwmain。有关详细信息,请参见:在 C++ 中 _tmain() 和 main() 有什么区别? 还定义了一个辅助宏 _T(..),用于获取正确类型的字符串字面值,即 "regular literals"L"wchar_t literals"
请参见此处提到的注意事项:TCHAR 是否仍然相关?-- dan04 的答案 _tmain 示例
对于问题中的 main 示例,以下代码将所有作为命令行参数传递的字符串连接成一个字符串。
int _tmain(int argc, _TCHAR *argv[])
{
   TCHAR szCommandLine[1024];

   if (argc < 2) return 0;

   _tcscpy(szCommandLine, argv[1]);
   for (int i = 2; i < argc; ++i)
   {
       _tcscat(szCommandLine, _T(" "));
       _tcscat(szCommandLine, argv[i]);
   }

   /* szCommandLine now contains the command line arguments */

   return 0;
}

(省略错误检查)这段代码适用于字符集的所有三种情况,因为我们在任何地方都使用了 TCHARtchar.h 字符串函数和字符串字面值的 _T。忘记用 _T(..) 包围您的字符串字面量是编写此类 TCHAR 程序时常见的编译器错误来源。 如果我们没有做到这些事情,那么切换字符集将导致代码要么无法编译,要么更糟糕的是在运行时表现不当。

Windows API 函数

在字符串上工作的 Windows API 函数,如 CreateFileGetCurrentDirectory,在 Windows 头文件中实现为宏,就像 tchar.h 的宏一样,会切换到 'A' 版本或 'W' 版本。例如,CreateFile 是一个宏,被定义为 ANSI 和 MBCS 的 CreateFileA,以及对于 Unicode 的 CreateFileW

每当你在代码中使用平台(没有'A'或'W')时,实际调用的函数将根据所选字符集而变化。你可以通过使用显式的"A"或"W"名称来强制使用特定版本。
结论是,除非你想要始终引用特定版本(独立于字符集选项),否则应始终使用未经限定的名称。
对于问题中的示例,我们想打开第一个参数给出的文件:
int _tmain(int argc, _TCHAR *argv[])
{  
   if (argc < 2) return 1;

   HANDLE hFile = CreateFile(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

   /* Read from file and do other stuff */
   ...

   CloseHandle(hFile);

   return 0;
}

(错误检查被省略)请注意,在这个例子中,我们不需要使用任何特定于 TCHAR 的东西,因为宏定义已经为我们考虑了这一点。
利用 C++ 字符串
我们已经看到了如何使用 tchar.h 例程来使用 C 风格的字符串操作来处理 TCHAR,但如果我们可以利用 C++ 的 string 来处理这个问题就更好了。
我的建议首先是不要使用 TCHAR,而是直接使用 Unicode,请参见结论部分,但如果您想使用 TCHAR,可以按照以下步骤进行操作。
要使用 TCHAR,我们希望得到一个使用 TCHARstd::basic_string 实例。您可以通过 typedef 自己的 tstring 来实现这一点:
typedef std::basic_string<TCHAR> tstring;

对于字符串字面值,请不要忘记使用_T
您还需要使用正确版本的cincout。 您可以使用引用实现tcintcout
#if defined(_UNICODE)
std::wistream &tcin = wcin;
std::wostream &tcout = wcout;
#else
std::istream &tcin = cin;
std::ostream &tcout = cout;
#end

这将使您能够几乎做任何事情。可能会有偶尔的例外,例如 std::to_stringstd::to_wstring,对于这些例外,您可以找到类似的解决方法。
结论
本答案(希望)详细介绍了 TCHAR 是什么以及它如何与 Visual Studio 和 Windows 标头交织在一起。然而,我们也应该考虑是否要使用它。 我的建议是直接为所有新的 Windows 程序使用 Unicode,根本不要使用 TCHAR 其他人也给出了同样的建议:Is TCHAR still relevant? 要在创建新项目后使用 Unicode,请首先确保字符集设置为 Unicode。然后,从源文件(或者从 stdafx.h)中删除 #include <tchar.h>。将任何 TCHAR_TCHAR 更改为 wchar_t,将 _tmain 更改为 wmain
int wmain(int argc, wchar_t *argv[])

非控制台项目的Windows应用程序入口点是WinMain,在TCHAR术语中会显示为:
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR    lpCmdLine, int nCmdShow)

应该变成

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR    lpCmdLine, int nCmdShow)

在此之后,只使用wchar_t字符串和/或std::wstring

进一步的注意事项

  • 在使用TCHAR数组(字符串)时,编写sizeof(szMyString)时要小心,因为对于ANSI,这是字符和字节的大小,对于Unicode,这仅是字节大小,字符数最多为一半,对于MBCS,这是字节大小,字符数可能相等也可能不相等。Unicode和MBCS都可以使用多个TCHAR来编码单个字符。
  • 混合使用TCHAR和固定的charwchar_t非常麻烦;您必须将字符串从一个转换为另一个,使用正确的代码页!简单的复制在一般情况下不起作用。
  • _UNICODEUNICODE之间存在轻微差异,如果您想有条件地定义自己的函数,则相关。请参见为什么同时存在UNICODE和_UNICODE?

一篇非常好的、补充性的答案是:Windows上MBCS和UTF-8之间的区别

完全不同意Unicode作为默认选择。这是20世纪的做法。每个人都应该选择UTF-8。 - SergeyA
3
@SergeyA 嗯,如果不考虑Windows不支持UTF-8的话,那就没问题了。 - MicroVirus
4
整个Windows API不支持UTF8编码。浏览器可以自由实现从网络上获取的UTF8数据到UTF16的转换(并且可以选择一个支持UTF8的GUI库/渲染引擎)。 - deviantfan
1
@SergeyA 因此,Unicode建议(它直接来自微软):您可以使用代码页,或者如果您想要Unicode支持,则必须在API级别上选择UTF-16。 - MicroVirus
我想补充的一件事是,有时候TCHAR/tstrings可以用于跨平台开发。如果你想在所有平台上支持本地字符串编码,TCHAR是一种方法,在Windows上支持UTF-16,同时在Mac和Linux上支持UTF-8。虽然这不是它被“预期”使用的方式,但我已经看到过它在实际应用中被这样使用。 - MrEricSir
显示剩余14条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接