TCHAR、WCHAR、LPSTR、LPWSTR、LPCTSTR(等等)是什么?






4.87/5 (255投票s)
许多 Windows C++ 程序员对 TCHAR 和 LPCTSTR 等奇特的 TCHAR 数据类型感到困惑。在这里,我将简要地尝试驱散迷雾。
许多 C++ Windows 程序员对 TCHAR
、LPCTSTR
等奇特的标识符感到困惑。在本文中,我将尽最大努力来驱散迷雾。
一般来说,一个字符可以由 1 个字节或 2 个字节表示。我们称 1 字节字符为 ANSI 字符——所有英文字符都通过这种编码表示。我们称 2 字节字符为 Unicode,它可以表示世界上所有的语言。
Visual C++ 编译器支持 char
和 wchar_t
作为 ANSI 和 Unicode 字符的原生数据类型。尽管 Unicode 有更具体的定义,但为了便于理解,可以将其视为 Windows 操作系统用于多语言支持的双字节字符。
如果您希望您的 C/C++ 代码独立于所使用的字符编码/模式,该怎么办?
建议:使用通用的数据类型和名称来表示字符和字符串。
例如,而不是替换
char cResponse; // 'Y' or 'N'
char sUsername[64];
// str* functions
用
wchar_t cResponse; // 'Y' or 'N'
wchar_t sUsername[64];
// wcs* functions
为了在您的语言中支持多语言(即 Unicode),您可以简单地以更通用的方式进行编码。
#include<TCHAR.H> // Implicit or explicit include
TCHAR cResponse; // 'Y' or 'N'
TCHAR sUsername[64];
// _tcs* functions
常规页中的以下项目设置描述了要用于编译的字符集:(常规 -> 字符集)
这样,当您的项目被编译为 Unicode 时,TCHAR
将被翻译为 wchar_t
。如果它被编译为 ANSI/MBCS,它将被翻译为 char
。您可以自由使用 char
和 wchar_t
,项目设置不会影响这些关键字的任何直接使用。
T
定义如下:CHAR
#ifdef _UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif
当您将字符集设置为“使用 Unicode 字符集”时,会定义宏 _UNICODE
,因此 TCHAR
将表示 wchar_t
。当字符集设置为“使用多字节字符集”时,TCHAR 将表示 char
。
同样,为了使用单一代码库支持多种字符集,并可能支持多语言,请使用特定的函数(宏)。与其使用 strcpy
、strlen
、strcat
(包括以 _s 结尾的安全版本);或者 wcscpy
、wcslen
、wcscat
(包括安全版本),您最好使用 _tcscpy
、_tcslen
、_tcscat
函数。
正如您所知,strlen
的原型是:
size_t strlen(const char*);
而 wcslen
的原型是:
size_t wcslen(const wchar_t* );
您最好使用 _tcslen
,它的逻辑原型是:
size_t _tcslen(const TCHAR* );
WC 代表宽字符。因此,wcs
实际上是宽字符字符串。这样,_tcs
就代表 _T 字符字符串。您知道 _T 可以是 char
或 what_t
,从逻辑上讲。
但实际上,_tcslen
(以及其他 _tcs
函数)**不是**函数,而是**宏**。它们的定义非常简单:
#ifdef _UNICODE
#define _tcslen wcslen
#else
#define _tcslen strlen
#endif
您应该查阅 TCHAR.H
来查找更多此类宏定义。
您可能会问,为什么它们被定义为宏而不是函数?原因很简单:一个库或 DLL 可以导出具有相同名称和原型的单个函数(忽略 C++ 的重载概念)。例如,当您导出函数时:
void _TPrintChar(char);
客户端应该如何调用它?
void _TPrintChar(wchar_t);
_TPrintChar
无法神奇地转换为接受双字节字符的函数。必须有两个单独的函数:
void PrintCharA(char); // A = ANSI
void PrintCharW(wchar_t); // W = Wide character
并且如下定义的简单宏可以隐藏差异:
#ifdef _UNICODE
void _TPrintChar(wchar_t);
#else
void _TPrintChar(char);
#endif
客户端只需调用:
TCHAR cChar;
_TPrintChar(cChar);
请注意,TCHAR
和 _TPrintChar
都会映射到 Unicode 或 ANSI,因此 cChar
和函数参数将是 char
或 wchar_t
。
宏避免了这些复杂性,并允许我们使用 ANSI 或 Unicode 函数来处理字符和字符串。大多数接受字符串或字符的 Windows 函数都是这样实现的,并且为了方便程序员,只有一个函数(一个宏!)就足够了。SetWindowText
是一个例子:
// WinUser.H
#ifdef UNICODE
#define SetWindowText SetWindowTextW
#else
#define SetWindowText SetWindowTextA
#endif // !UNICODE
很少有函数没有宏,只有以 W 或 A 结尾的版本。一个例子是 ReadDirectoryChangesW
,它没有 ANSI 等效项。
你们都知道我们使用双引号来表示字符串。以这种方式表示的字符串是 ANSI 字符串,每个字符为 1 字节。例如:
"This is ANSI String. Each letter takes 1 byte."
上面给出的字符串文本**不是** Unicode,并且对多语言支持是有限的。要表示 Unicode 字符串,您需要使用前缀 L
。例如:
L"This is Unicode string. Each letter would take 2 bytes, including spaces."
注意字符串开头的 L,这使其成为 Unicode 字符串。所有字符(我再说一遍,**所有**字符)将占用两个字节,包括所有英文字母、空格、数字和空字符。因此,Unicode 字符串的长度始终是 2 字节的倍数。例如,长度为 7 个字符的 Unicode 字符串需要 14 个字节,以此类推。长度为 15 个字节的 Unicode 字符串,例如,在任何上下文中都无效。
一般来说,字符串将是 sizeof(TCHAR)
字节的倍数!
当您需要表示硬编码字符串时,可以使用:
"ANSI String"; // ANSI
L"Unicode String"; // Unicode
_T("Either string, depending on compilation"); // ANSI or Unicode
// or use TEXT macro, if you need more readability
没有前缀的字符串是 ANSI 字符串,带有 L 前缀的字符串是 Unicode 字符串,而 _T
或 TEXT
指定的字符串将根据编译选项而定。同样,_T
和 TEXT
只是宏,定义如下:
// SIMPLIFIED
#ifdef _UNICODE
#define _T(c) L##c
#define TEXT(c) L##c
#else
#define _T(c) c
#define TEXT(c) c
#endif
##
符号是标记粘贴运算符,它会将 _T("Unicode")
转换为 L"Unicode"
,其中传递的字符串是宏的参数——如果定义了 _UNICODE
。如果未定义 _UNICODE
,_T("Unicode")
将仅表示 "Unicode"
。标记粘贴运算符在 C 语言中就已存在,并且与 VC++ 或字符编码无关。
请注意,这些宏可用于字符串和字符。_T('R')
将转换为 L'R'
或简单的 'R'
——前者是 Unicode 字符,后者是 ANSI 字符。
**不**,您不能使用这些宏将变量(字符串或字符)转换为 Unicode/非 Unicode 文本。以下无效:
char c = 'C';
char str[16] = "CodeProject";
_T(c);
_T(str);
粗体行将在 ANSI(多字节)构建中成功编译,因为 _T(x)
仅等于 x
,因此 _T(c)
和 _T(str)
分别等于 c
和 str
。但是,当您使用 Unicode 字符集进行构建时,它将无法编译:
error C2065: 'Lc' : undeclared identifier
error C2065: 'Lstr' : undeclared identifier
我不想冒犯您的智商,去描述为什么以及那些错误是什么。
存在一套转换例程,用于将 MBCS 转换为 Unicode,反之亦然,我将很快解释。
需要注意的是,几乎所有接受字符串(或字符)的函数,尤其是在 Windows API 中,在 MSDN 等地方都会有通用的原型。例如,SetWindowTextA/W
函数可以归类为:
BOOL SetWindowText(HWND, const TCHAR*);
但是,正如您所知,SetWindowText
只是一个宏,根据您的构建设置,它将表示以下任一项:
BOOL SetWindowTextA(HWND, const char*);
BOOL SetWindowTextW(HWND, const wchar_t*);
因此,如果以下调用无法获取此函数的地址,请不要感到困惑!
HMODULE hDLLHandle;
FARPROC pFuncPtr;
hDLLHandle = LoadLibrary(L"user32.dll");
pFuncPtr = GetProcAddress(hDLLHandle, "SetWindowText");
//pFuncPtr will be null, since there doesn't exist any function with name SetWindowText !
从 User32.DLL
导出的函数是 SetWindowTextA
和 SetWindowTextW
,而不是带有通用名称的函数。
有趣的是,.NET Framework 足够智能,可以从带有通用名称的 DLL 中定位函数:
[DllImport("user32.dll")]
extern public static int SetWindowText(IntPtr hWnd, string lpString);
没什么高深莫测的,只是一堆围绕 GetProcAddress
的 if 和 else!
所有具有 ANSI 和 Unicode 版本的函数,其实际实现仅在 Unicode 版本中。这意味着,当您从代码中调用 SetWindowTextA
,并传递一个 ANSI 字符串时——它会将 ANSI 字符串转换为 Unicode 文本,然后调用 SetWindowTextW
。实际工作(设置窗口文本/标题/标题栏)将仅由 Unicode 版本执行!
再举一个例子,使用 GetWindowText
检索窗口文本。您调用 GetWindowTextA
,并将 ANSI 缓冲区作为目标缓冲区传递。GetWindowTextA
将首先调用 GetWindowTextW
,可能为其分配一个 Unicode 字符串(wchar_t
数组)。然后它会为您将该 Unicode 内容转换为 ANSI 字符串。
这种 ANSI 到 Unicode 的转换以及反向转换不仅限于 GUI 函数,还包括 Windows API 的整个集合,这些 API 接受字符串并有两种变体。一些例子是:
CreateProcess
GetUserName
OpenDesktop
DeleteFile
- etc
因此,强烈建议直接调用 Unicode 版本。反过来,这意味着您**始终**应该以 Unicode 构建为目标,而不是 ANSI 构建——仅仅因为您多年来习惯使用 ANSI 字符串。是的,您可以保存和检索 ANSI 字符串,例如在文件中,或在您的 Messenger 应用程序中作为聊天消息发送。存在转换例程来满足此类需求。
注意:还存在另一个 typedef:WCHAR
,它等同于 wchar_t
。
TCHAR
宏用于单个字符。您绝对可以声明一个 TCHAR
数组。如果您想表达一个字符指针或常量字符指针,该怎么办?以下哪种是?
// ANSI characters
foo_ansi(char*);
foo_ansi(const char*);
/*const*/ char* pString;
// Unicode/wide-string
foo_uni(WCHAR*);
wchar_t* foo_uni(const WCHAR*);
/*const*/ WCHAR* pString;
// Independent
foo_char(TCHAR*);
foo_char(const TCHAR*);
/*const*/ TCHAR* pString;
在阅读了 TCHAR
相关内容后,您**绝对会**选择最后一个作为您的选择。有更好的替代方法来表示字符串。为此,您只需包含 Windows.h。**注意**:如果您的项目隐式或显式地包含 Windows.h,则无需包含 TCHAR.H
。
首先,回顾旧的字符串函数以更好地理解。您知道 strlen
:
size_t strlen(const char*);
这可以表示为:
size_t strlen(LPCSTR);
其中符号 LPCSTR
被 typedef
为:
// Simplified
typedef const char* LPCSTR;
其含义是:
- LP - Long Pointer(长指针)
- C - Constant(常量)
- STR - String(字符串)
本质上,LPCSTR
表示(长)指向常量字符串的指针。
让我们使用新式类型名称来表示 strcpy
:
LPSTR strcpy(LPSTR szTarget, LPCSTR szSource);
szTarget 的类型是 LPSTR
,类型名称中没有 C。它定义为:
typedef char* LPSTR;
注意,szSource 是 LPCSTR
,因为 strcpy
函数不会修改源缓冲区,因此具有 const
属性。返回类型是非常量字符串:LPSTR
。
好的,这些 str
函数用于 ANSI 字符串操作。但我们需要用于 2 字节 Unicode 字符串的例程。为此,提供了等效的宽字符 str 函数。例如,要计算宽字符(Unicode 字符串)的长度,您将使用 wcslen
:
size_t nLength;
nLength = wcslen(L"Unicode");
wcslen
的原型是:
size_t wcslen(const wchar_t* szString); // Or WCHAR*
这可以表示为:
size_t wcslen(LPCWSTR szString);
其中符号 LPCWSTR
定义为:
typedef const WCHAR* LPCWSTR;
// const wchar_t*
这可以分解为:
- LP - Pointer(指针)
- C - Constant(常量)
- WSTR - Wide character String(宽字符字符串)
类似地,strcpy
的等效函数是 wcscpy
,用于 Unicode 字符串:
wchar_t* wcscpy(wchar_t* szTarget, const wchar_t* szSource)
这可以表示为:
LPWSTR wcscpy(LPWSTR szTarget, LPWCSTR szSource);
其中目标是非常量宽字符串(LPWSTR
),源是常量宽字符串。
存在一套等效的 wcs
函数对应于 str
函数。str
函数将用于纯 ANSI 字符串,而 wcs
函数将用于 Unicode 字符串。
虽然我之前建议使用 Unicode 原生函数,而不是仅 ANSI 或 TCHAR
合成的函数。原因很简单——您的应用程序应该仅为 Unicode,并且您**不**应该关心 ANSI 构建的代码可移植性。但为了完整起见,我提到了这些通用映射。
要计算字符串长度,您可以使用 _tcslen
函数(宏)。通常,它的原型是:
size_t _tcslen(const TCHAR* szString);
或者,表示为:
size_t _tcslen(LPCTSTR szString);
其中类型名称 LPCTSTR
可分类为:
- LP - Pointer(指针)
- C - Constant(常量)
- T = TCHAR
- STR = String(字符串)
根据项目设置,LPCTSTR
将映射到 LPCSTR
(ANSI)或 LPCWSTR
(Unicode)。
注意:strlen
、wcslen
或 _tcslen
将返回字符串的**字符**数,而不是字节数。
通用的字符串复制例程 _tcscpy
定义为:
size_t _tcscpy(TCHAR* pTarget, const TCHAR* pSource);
或者,更通用的形式是:
size_t _tcscpy(LPTSTR pTarget, LPCTSTR pSource);
您可以推断出 LPTSTR
的含义!
使用示例
首先,一段损坏的代码:
int main()
{
TCHAR name[] = "Saturn";
int nLen; // Or size_t
lLen = strlen(name);
}
在 ANSI 构建中,此代码将成功编译,因为 TCHAR
将是 char
,因此 name 将是一个 char
数组。调用 strlen
来处理 name
变量也将顺利进行。
好的。让我们使用 UNICODE
/_UNICODE
定义(即项目设置中的“使用 Unicode 字符集”)来编译相同的代码。现在,编译器将报告一系列错误:
- 错误 C2440:“初始化”:无法从“const char [7]”转换为“TCHAR []”
- 错误 C2664:“strlen”:无法将参数 1 从“TCHAR []”转换为“const char *”
程序员会开始犯错,将其纠正为这样(第一个错误):
TCHAR name[] = (TCHAR*)"Saturn";
这并不能让编译器满意,因为无法从 TCHAR*
转换为 TCHAR[7]
。当原生 ANSI 字符串传递给 Unicode 函数时,也会出现同样的错误。
nLen = wcslen("Saturn");
// ERROR: cannot convert parameter 1 from 'const char [7]' to 'const wchar_t *'
不幸的是(或者幸运地),这个错误可以通过简单的 C 风格类型转换来错误地纠正:
nLen = wcslen((const wchar_t*)"Saturn");
您会认为您在指针方面又获得了一个经验等级!您错了——代码将产生不正确的结果,在大多数情况下只会导致访问冲突。这样进行类型转换就像在需要一个 80 字节的结构时传递一个 float
变量(从逻辑上讲)。
字符串 "Saturn"
是 7 个字节的序列:
'S' (83) | 'a' (97) | 't'(116) | 'u' (117) | 'r'(114) | 'n'(110) | '\0' (0) |
但是,当您将相同的一组字节传递给 wcslen
时,它会将每 2 个字节视为一个单独的字符。因此,前两个字节 [97, 83] 将被视为一个值为:24915(97<<8 | 83
)的字符。这是一个 Unicode 字符:?
。下一个字符由 [117, 116] 等表示。
当然,您并没有传递那些中文字符,但错误的类型转换却做到了!因此,了解类型转换**不起作用**非常重要!所以,对于第一行初始化,您必须这样做:
TCHAR name[] = _T("Saturn");
这会根据编译结果转换为 7 个字节或 14 个字节。对 wcslen
的调用应该是:
wcslen(L"Saturn");
在上面提供的示例程序代码中,我使用了 strlen
,它在 Unicode 构建时会产生错误。无效的解决方案是 C 风格的类型转换:
lLen = strlen ((const char*)name);
在 Unicode 构建中,name 将是 14 个字节(7 个 Unicode 字符,包括空字符)。由于字符串 "Saturn" 只包含英文字母,它们可以使用原始 ASCII 表示,因此 Unicode 字母 'S'
将表示为 [83, 0]。其他 ASCII 字符将表示为一个零在它们旁边。请注意,'S'
现在表示为**2 字节**值 83
。字符串的结束将由值为 0
的**两个字节**表示。
因此,当您将这样的字符串传递给 strlen
时,第一个字符(即第一个字节)将是正确的(对于 "Saturn" 是 'S'
)。但是第二个字符/字节将指示字符串的结束。因此,strlen
将返回不正确的值 1
作为字符串的长度。
正如您所知,Unicode 字符串可能包含非英文字符,strlen 的结果将更加不确定。
总之,类型转换是行不通的。您需要将字符串表示为正确的形式,或者使用 ANSI 到 Unicode(反之亦然)的转换例程。
(还有更多内容待添加,敬请关注!)
现在,我希望您能理解以下签名:
BOOL SetCurrentDirectory( LPCTSTR lpPathName );
DWORD GetCurrentDirectory(DWORD nBufferLength,LPTSTR lpBuffer);
继续。您一定见过一些函数/方法要求您传递**字符数**,或者返回字符数。嗯,就像 GetCurrentDirectory
一样,您需要传递字符数,而不是字节数。例如:
TCHAR sCurrentDir[255];
// Pass 255 and not 255*2
GetCurrentDirectory(sCurrentDir, 255);
另一方面,如果您需要分配字符数,则必须分配正确的字节数。在 C++ 中,您可以使用 new
:
LPTSTR pBuffer; // TCHAR*
pBuffer = new TCHAR[128]; // Allocates 128 or 256 BYTES, depending on compilation.
但是,如果您使用 malloc
、LocalAlloc
、GlobalAlloc
等内存分配函数;您必须指定字节数!
pBuffer = (TCHAR*) malloc (128 * sizeof(TCHAR) );
正如您所知,需要对返回值进行类型转换。malloc
参数中的表达式确保它分配了所需的字节数——并为所需的字符数腾出了空间。