当使用多字节转宽字符的函数"mbstowcs"时,如果传递了一个字符串字面量,它是否使用源文件的编码?

6

附录 我自己的初步答案出现在问题底部。


我正在将一个过时的VC6 C++/MFC项目转换为VS2013和Unicode,基于utf8everywhere.org的建议。
在这个过程中,我一直在研究Unicode、UTF-16、UCS-2、UTF-8、标准库以及对Unicode和UTF-8的STL支持(或者更确切地说是标准库缺乏支持)、ICUBoost.Locale,当然还有需要UTF-16 wchar的Windows SDK和MFC API。
随着我对以上问题的研究,一个问题一直困扰着我,我无法得到一个满意的澄清答案。
考虑C库函数mbstowcs。该函数具有以下签名:
size_t mbstowcs (wchar_t* dest, const char* src, size_t max);

第二个参数 src 是(根据 文档)一个包含待解释多字节字符的 C 字符串。多字节序列应该以初始换码状态开始。
我对这个多字节字符串有疑问。我了解到,编码方式可能因字符串而异,并且标准没有规定编码方式。MSVC 文档 也没有说明特定的编码方式。
我的理解是,在 Windows 上,预计该多字节字符串应该使用当前区域设置的 ANSI 代码页进行编码。但在这一点上,我的清晰度开始消失。
我一直在思考源代码文件本身的编码是否会影响 mbstowcs 在 Windows 上的行为。此外,我也困惑于上述代码片段在编译时和运行时发生了什么。
假设您将字符串字面量传递给 mbstowcs,如下所示:
wchar_t dest[1024];
mbstowcs (dest, "Hello, world!", 1024);

假设这段代码在Windows机器上编译。假设源代码文件本身的代码页与编译器运行的机器上当前语言环境的代码页不同。编译器是否会考虑源代码文件的编码方式?生成的二进制文件是否会受到源代码文件的代码页与编译器运行时所使用的语言环境代码页不同的影响?
另一方面,也许我理解有误 - 也许运行时机器的活动语言环境决定了对字符串文字的期望代码页。因此,保存源代码文件的代码页是否需要与最终运行程序的计算机的代码页匹配?这似乎很奇怪,让我难以相信会是这种情况。但正如您所看到的,我的表述在这里缺乏清晰度。
另一方面,如果我们将对mbstowcs的调用更改为显式传递UTF-8字符串:
wchar_t dest[1024];
mbstowcs (dest, u8"Hello, world!", 1024);

我假设mbstowcs总是能够完成正确的操作 - 不受源文件代码页、编译器当前区域设置或代码运行计算机的当前区域设置的影响。我对此正确吗?
特别是针对我上面提出的具体问题,我希望能得到明确的解答。如果我所提的任何问题不恰当,我也希望得知。
附录 从@TheUndeadFish的答案下面的冗长评论以及这里一个非常相似的问题的答案,我相信我有一个初步的答案来回答自己的问题,我想提出来。
让我们跟随源代码文件的原始字节,看看整个编译到运行时行为的过程中实际字节是如何被转换的:
C++标准“表面上”要求源代码文件中的所有字符都是ASCII的一个名为“基本源字符集”的96个字符子集。但是请参阅以下要点。实际上,关于这96个字符在源代码文件中的字节级编码,标准没有指定任何特定的编码,但所有96个字符都是ASCII字符,因此实际上不存在有关源文件采用哪种编码的问题,因为现有的所有编码都使用相同的原始字节表示这96个ASCII字符。
然而,字符文字和代码注释可能通常包含基本96个字符之外的字符。通常编译器支持这一点(尽管C++标准不要求这样做)。源代码的字符集称为“源字符集”。但编译器需要在其内部字符集(称为“执行字符集”)中具有这些相同的字符,否则在编译器实际处理源代码之前,这些缺失的字符将被某个其他(虚拟)字符(例如方块或问号)替换-请参见下面的讨论。当字符出现在“基本源字符集”之外时,编译器如何确定用于对源代码文件中的字符进行编码的编码是由实现定义的。
请注意,编译器可以为其内部“执行字符集”使用与源代码文件所表示的字符集不同的字符集(无论如何编码)。这意味着即使编译器知道源代码文件的编码(这意味着编译器还了解源代码字符集中的所有字符),编译器也可能被迫将源代码字符集中的某些字符转换为“执行字符集”中的不同字符(从而丢失信息)。标准规定,这是可以接受的,但编译器不得将“源字符集”中的任何字符转换为“执行字符集”中的空字符。
C++标准没有说明“执行字符集”的编码,也没有说明需要在“执行字符集”中支持哪些字符(除了“基本执行字符集”中的字符外,其中包括“基本源字符集”中的所有字符以及少量其他字符,例如“NULL”字符和退格字符)。似乎很难找到任何清楚地记录这个过程的文档,即使是由Microsoft提供的。也就是说,编译器如何确定源代码文件的编码和相应的字符集,或者选择哪种编码在编译源代码文件时用于“执行字符集”,并没有明确指出。
在MSVC的情况下,编译器似乎会尽最大努力尝试选择给定源代码文件的编码(和相应的字符集),如果失败,则回退到运行编译器的机器的当前区域设置的默认代码页。或者,您可以采取特殊步骤使用提供每个源代码文件开头的正确字节顺序标记(BOM)将源代码文件保存为Unicode。这包括UTF-8,其中BOM通常是可选的或排除在外,在MSVC编译器读取的源代码文件的情况下,您必须包括UTF-8 BOM。
至于“执行字符集”及其在MSVC中的编码,请继续下一个要点。
然后,编译器开始读取源文件,并将源代码文件字符的原始字节从“源字符集”的编码转换为“执行字符集”中相应字符的(可能不同的)编码(如果给定字符存在于两个字符集中,则将是相同的字符)。
忽略代码注释和字符文字,所有此类字符通常都位于上述“基本执行字符集”中。这是ASCII字符集的子集,因此编码问题无关紧
感谢那些抽出时间阅读这里冗长的答案的人。

2
你正在使用的 mbstowcs 已在 mbstowcs 中有文档记录。src 字符串是使用调用线程的区域设置进行解释的。为了获得可靠的结果,您可以设置调用线程的本地设置,或者使用 Microsoft 的扩展 _mbstowcs_l,并带有一个区域设置参数。 - IInspectable
1
(1) 根据此链接(http://msdn.microsoft.com/en-us/library/69ze775t.aspx),VS2013不支持u8""字符串字面量吗? (2) MultiByteToWideChar函数可能比mbstowcs更安全,因为它允许您明确指定源编码。 (3) 对于您的源代码来说,只包含ASCII字符可能是最安全的选择,因为非ASCII字符可能会被下一年使用的编译器与现在使用的编译器处理方式不同;UTF-8或UTF-16字符串字面量可以使用\x语法构建。 - Harry Johnston
1
@DanNissenbaum:仅想澄清一下,这条评论并不是那么注重文本,而是旨在指出这里有两个产品(操作系统和编译器),它们对字符的观点并不完全相同。 - MSalters
@MSalters 谢谢。经过一番思考,我意识到二进制文件中非ASCII字符的存储和解释相关的操作系统部分是C运行时DLL。 - Dan Nissenbaum
@DanNissenbaum:Visual Studio 6及更早版本(已经停止支持)使用随操作系统一起提供的C运行时DLL。目前的任何Visual Studio版本都不会这样做。即使是提供的C运行时也不是Win32 API的一部分,因此有明显的区别。当然,C运行时使用 Win32 API。 (还可以编写不使用C运行时的代码。)警告: Win32 API中记录的某些函数实际上是C运行时的一部分;这是微软疏忽造成的。所以你并不完全错误。 :-) - Harry Johnston
显示剩余13条评论
2个回答

5
源代码文件的编码不会影响mbstowcs的行为。毕竟,函数的内部实现并不知道可能调用它的源代码是什么。
在你提供的MSDN文档中,写到:
mbstowcs对于任何与语言环境相关的行为都使用当前语言环境;_mbstowcs_l除了使用传递进来的语言环境外,其余完全相同。有关更多信息,请参见Locale
这个链接页面关于语言环境的内容引用了setlocale,这就是可以影响mbstowcs行为的方法。
现在,看一下你提议的通过UTF-8传递的方式。
mbstowcs (dest, u8"Hello, world!", 1024);

很不幸,据我所知,一旦使用有趣的数据,这将无法正常工作。即使编译成功,也只是因为编译器必须将 u8 视为 char*。而且,就 mbstowcs 而言,它会认为字符串是在设置的区域设置下编码的。
更不幸的是,我认为(在 Windows/Visual Studio 平台上)没有任何方法可以设置区域设置以使用 UTF-8。
所以,这只适用于 ASCII 字符(前 128 个字符),因为它们在各种 ANSI 编码和 UTF-8 中具有完全相同的二进制值。如果尝试使用任何超出此范围的字符(例如带有重音或变音符号的任何字符),则会遇到问题。

个人认为,mbstowcs等函数相对有限且笨重。我发现Windows API函数MultiByteToWideChar通常更有效。特别是它可以通过将代码页参数设置为CP_UTF8轻松处理UTF-8。


谢谢。快速问题 - 当前区域设置 - 这是指编译器的当前区域设置,还是指程序运行所在系统的当前区域设置? - Dan Nissenbaum
1
实际上,这是程序中 C Runtime 的当前所在地。由于它位于程序的内存中,因此 setlocale 可以更改它。 - TheUndeadFish
2
Visual Studio附带的CRT按线程而非进程管理区域设置。当前活动的区域设置指的是代码运行所在的线程的区域设置。 - IInspectable
1
啊,是的,我在线程方面有些粗心了。虽然似乎有一个_configthreadlocale函数可以设置setlocale只影响当前线程还是所有线程。 - TheUndeadFish
@DietmarKühl - 谢谢。我目前正在学习执行字符集。关于这个特定的StackOverflow问题 - 如果有人能够花时间详细说明字符串字面量的原始字节发生了什么,从源代码文件的原始字节开始,通过编译器读取这些原始字节并转换为源代码和执行字符集,直到烧入可执行文件的字节,最后在运行时使用mbstowcs处理这些字节,那就太好了。我知道这是很多要求! - Dan Nissenbaum
显示剩余15条评论

1

mbstowcs()的语义是根据当前安装的C语言环境来定义的。如果您正在处理具有不同编码的字符串,则需要使用setlocale()更改当前使用的编码。 C标准中相关的语句在7.22.8段1中:

多字节字符串函数的行为受当前语言环境的LC_CTYPE类别的影响。

我对C库不够了解,但据我所知,这些函数中没有一个真正是线程安全的。我认为使用C++的std::locale工具更容易处理不同的编码和一般的文化约定。关于编码转换,您可以查看std::codecvt<...>特征。不过,这些并不容易使用。

当前语言环境需要一些澄清:程序有一个当前全局语言环境。最初,该语言环境由系统某种方式设置,并可能由用户的环境以某种形式控制。例如,在UNIX系统上,有选择初始语言环境的环境变量。一旦程序运行,它就可以更改当前语言环境。但是如何更改取决于使用的是什么:运行中的C++程序实际上有两个语言环境:一个由C库使用,一个由C++库使用。
C语言环境用于所有依赖于语言环境的C库函数,例如mbstowcs(),但也用于tolower()printf()等函数。 C++语言环境用于所有特定于C ++库的依赖于语言环境的函数。由于C ++使用语言环境对象,因此全局语言环境仅用作未明确设置语言环境的实体的默认值,主要用于流(您可以使用s.imbue(loc)设置流的语言环境)。根据您设置的语言环境,有不同的方法来设置全局语言环境:
  1. 对于C语言环境,使用setlocale()
  2. 对于C++语言环境,使用std::locale::global()

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