wchar_t* 到 char* 的转换问题

Posted

技术标签:

【中文标题】wchar_t* 到 char* 的转换问题【英文标题】:wchar_t* to char* conversion problems 【发布时间】:2012-01-27 10:58:09 【问题描述】:

我在将wchar_t* 转换为char* 时遇到问题。

我从FILE_NOTIFY_INFORMATION 结构中得到一个wchar_t* 字符串,由ReadDirectoryChangesW WinAPI 函数返回,所以我认为该字符串是正确的。

假设 wchar 字符串是“New Text File.txt” 在 Visual Studio 调试器中,当鼠标悬停在变量上时会显示“N”和一些未知的中文字母。虽然在手表中字符串表示正确。

当我尝试使用 wcstombs 将 wchar 转换为 char 时

wcstombs(pfileName, pwfileName, fileInfo.FileNameLength);

它只将两个字母转换为char*(“Ne”),然后生成错误。

wcstombs.c 在这个块的函数 _wcstombs_l_helper() 中存在一些内部错误:

if (*pwcs > 255)  /* validate high byte */

    errno = EILSEQ;
    return (size_t)-1;  /* error */

它不会作为异常抛出。

可能是什么问题?

【问题讨论】:

您能否更具体地了解生成的错误? 你肯定不会从 FILE_NOTIFY_INFORMATION 得到 wchar_t*,它是 wchar_t[]。邮政编码。 为什么需要将其转换为char 字符串? 【参考方案1】:

为了以正确的方式做您想做的事,您需要考虑几件重要的事情。我会尽力在这里为你分解它们。

让我们从the wcstombs() function's documentation on MSDN中count参数的定义开始:

多字节输出字符串中可以存储的最大字节数。

请注意,这并没有说明宽字符输入字符串中的宽字符数。尽管您的示例输入字符串(“New Text File.txt”)中的所有宽字符都可以表示为单字节 ASCII 字符,但我们不能假设输入字符串中的每个宽字符都会在输出中恰好生成一个字节每个可能的输入字符串的字符串(如果此语句让您感到困惑,您应该查看Joel's article on Unicode and character sets)。那么,如果你传递wcstombs() 输出缓冲区的大小,它怎么知道输入字符串有多长呢?文档指出,根据标准 C 语言约定,输入字符串应以空值结尾:

如果 wcstombs 在 count 发生之前或发生时遇到宽字符空字符 (L'\0'),它会将其转换为 8 位 0 并停止。

虽然文档中没有明确说明这一点,但我们可以推断,如果输入字符串不是以 null 结尾的,wcstombs() 将继续读取宽字符,直到将 count 字节写入输出字符串。因此,如果您正在处理一个非空终止的宽字符串,仅知道输入字符串的长度是不够的;您必须以某种方式确切地知道输出字符串需要多少字节(如果不进行转换就无法确定)并将其作为count 参数传递以使wcstombs() 执行您希望它执行的操作。

为什么我如此关注这个空终止问题?因为the FILE_NOTIFY_INFORMATION structure's documentation on MSDN 对它的FileName 字段有这样的说法:

包含相对于目录句柄的文件名的可变长度字段。文件名采用 Unicode 字符格式,并且不以 null 结尾。

FileName 字段不是以 null 结尾的事实解释了为什么当您在调试器中查看它时它的末尾会有一堆“未知的中文字母”。 FILE_NOTIFY_INFORMATION 结构的文档还包含关于 FileNameLength 字段的另一个智慧:

记录的文件名部分的大小,以字节为单位。

注意这里说的是字节,而不是字符。因此,即使您想假设输入字符串中的每个宽字符将在输出字符串中恰好生成一个字节,您也不应该将fileInfo.FileNameLength 传递给count;您应该传递fileInfo.FileNameLength / sizeof(WCHAR)(当然,或者使用以空字符结尾的输入字符串)。将所有这些信息放在一起,我们终于可以理解为什么您对 wcstombs() 的原始调用失败了:它读取到字符串末尾并阻塞了无效数据(从而触发了 EILSEQ 错误)。

现在我们已经阐明了问题,是时候讨论可能的解决方案了。为了以正确的方式做到这一点,您需要知道的第一件事是您的输出缓冲区需要多大。幸运的是,wcstombs() 的文档中有一个最后的花絮可以帮助我们:

如果 mbstr 参数为 NULL,wcstombs 将返回目标字符串所需的大小(以字节为单位)。

所以使用wcstombs() 函数的惯用方法是调用它两次:第一次确定输出缓冲区需要多大,第二次实际进行转换。最后要注意的是,正如我们之前所说,宽字符输入字符串至少在第一次调用wcstombs() 时需要以空字符结尾。

把这一切放在一起,这里有一段代码可以做你想做的事情:

size_t fileNameLengthInWChars = fileInfo.FileNameLength / sizeof(WCHAR); //get the length of the filename in characters
WCHAR *pwNullTerminatedFileName = new WCHAR[fileNameLengthInWChars + 1]; //allocate an intermediate buffer to hold a null-terminated version of fileInfo.FileName; +1 for null terminator
wcsncpy(pwNullTerminatedFileName, fileInfo.FileName, fileNameLengthInWChars); //copy the filename into a the intermediate buffer
pwNullTerminatedFileName[fileNameLengthInWChars] = L'\0'; //null terminate the new buffer
size_t fileNameLengthInChars = wcstombs(NULL, pwNullTerminatedFileName, 0); //first call to wcstombs() determines how long the output buffer needs to be
char *pFileName = new char[fileNameLengthInChars + 1]; //allocate the final output buffer; +1 to leave room for null terminator
wcstombs(pFileName, pwNullTerminatedFileName, fileNameLengthInChars + 1); //finally do the conversion!

当然,当您完成清理工作时,别忘了致电delete[] pwNullTerminatedFileNamedelete[] pFileName

最后一件事

写完这个答案后,我更仔细地重新阅读了您的问题,并想到了您可能犯的另一个错误。您说wcstombs() 在仅转换前两个字母(“Ne”)后就失败了,这意味着它在前两个宽字符之后遇到了输入字符串中未初始化的数据。您是否碰巧使用赋值运算符将一个 FILE_NOTIFY_INFORMATION 变量复制到另一个变量?例如,

FILE_NOTIFY_INFORMATION fileInfo = someOtherFileInfo;

如果您这样做,它只会将someOtherFileInfo.FileName 的前两个宽字符复制到fileInfo.FileName。为了理解为什么会这样,考虑FILE_NOTIFY_INFORMATION结构的声明:

typedef struct _FILE_NOTIFY_INFORMATION 
  DWORD NextEntryOffset;
  DWORD Action;
  DWORD FileNameLength;
  WCHAR FileName[1];
 FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;

当编译器为赋值操作生成代码时,它不理解FileName 是一个可变长度字段被拉取的诡计,所以它只是将sizeof(FILE_NOTIFY_INFORMATION) 字节从someOtherFileInfo 复制到fileInfo .由于FileName 被声明为一个包含一个WCHAR 的数组,因此您会认为只会复制一个字符,但编译器会将结构填充为额外的两个字节长(因此它的长度是int 的大小),这就是为什么还要复制第二个 WCHAR 的原因。

【讨论】:

哇,伙计,你真的很酷。这正是我犯的错误。非常感谢。 不客气。与以 null 结尾的字符串和可变长度结构技巧相关的问题会让任何人发疯。感谢上帝提供更高级别的语言!【参考方案2】:

我的猜测是您传递的宽字符串无效或定义不正确。

pwFileName 是如何定义的?看来你有一个FILE_NOTIFY_INFORMATION结构定义为fileInfo,那你为什么不使用fileInfo.FileName,如下图?

wcstombs(pfileName, fileInfo.FileName, fileInfo.FileNameLength);

【讨论】:

【参考方案3】:

您得到的错误说明了一切,它发现了一个无法转换为 MB 的字符(因为它没有以 MB 表示),source:

如果 wcstombs 遇到宽字符,它无法转换为 多字节字符,它返回 –1 强制转换为 size_t 类型并设置 errno 到 EILSEQ

在这种情况下,您应该避免“假设”输入,并给出一个失败的实际测试用例。

【讨论】:

以上是关于wchar_t* 到 char* 的转换问题的主要内容,如果未能解决你的问题,请参考以下文章

wchar_t到QString的转换方法?

wchar_t到unsigned char转换

如何将 char 数组转换为 wchar_t 数组?

如何将char*转换为wchar

char* 和 wchar_t* 如何互相转换

如何把wchar_t类型的汉子转换为char*