传递字符串文字时,多字节到宽字符串的转换函数“mbstowcs”是否使用源文件的编码?

Dan*_*aum 5 c++ windows encoding utf-8 visual-studio-2013

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


我是一个古老的VC6 C ++ / MFC项目转换为VS2013和Unicode的基础上,建议在utf8everywhere.org

一路上,我一直在研究 Unicode、UTF-16、UCS-2、UTF-8、Unicode 和 UTF-8 的标准库和 STL 支持(或者,更确切地说,标准库缺乏支持)、ICUBoost .Locale,当然还有需要 UTF-16 的 Windows SDK 和 MFC 的 API wchar

在我研究上述问题的过程中,一个问题不断出现,我无法以清晰的方式回答我满意的问题。

考虑 C 库函数mbstowcs。此函数具有以下签名:

size_t mbstowcs (wchar_t* dest, const char* src, size_t max);
Run Code Online (Sandbox Code Playgroud)

第二个参数src是(根据文档)a

带有要解释的多字节字符的 C 字符串。多字节序列应从初始移位状态开始。

我的问题是关于这个多字节字符串。我的理解是多字节字符串的编码可能因字符串而异,并且标准未指定编码。MSVC 文档似乎也没有为此函数指定特定的编码。

我此时的理解是,在 Windows 上,这个多字节字符串应该使用活动语言环境的 ANSI 代码页进行编码。但我的清晰度在这一点上开始消退。

我一直想知道源代码文件本身的编码是否会对的行为产生影响mbstowcs,至少在 Windows 上。 而且,对于上面的代码片段,我也对编译时发生的情况与运行时发生的情况感到困惑。

假设您有一个字符串文字传递给mbstowcs,如下所示:

wchar_t dest[1024];
mbstowcs (dest, "Hello, world!", 1024);
Run Code Online (Sandbox Code Playgroud)

假设这段代码是在 Windows 机器上编译的。假设的代码页源代码文件本身不同的不是当前语言环境的机器在其上的代码页编译运行。编译器会考虑源代码文件的编码吗?生成的二进制文件是否会受到以下事实的影响:源代码文件的代码页与编译器运行所在的活动语言环境的代码页不同?

另一方面,也许我错了——也许运行时机器的活动语言环境决定了字符串文字预期的代码页。因此,保存源代码文件的代码页是否需要程序最终运行的计算机的代码页匹配?这对我来说似乎太打击了,我发现很难相信情况会如此。但正如你所看到的,我在这里缺乏清晰度。

另一方面,如果我们将调用更改mbstowcs为显式传递 UTF-8 字符串:

wchar_t dest[1024];
mbstowcs (dest, u8"Hello, world!", 1024);
Run Code Online (Sandbox Code Playgroud)

...我认为这mbstowcs将始终做正确的事情 - 无论源文件的代码页、编译器的当前语言环境或运行代码的计算机的当前语言环境如何。我对此是否正确?

我希望能澄清这些问题,尤其是我在上面提出的具体问题。如果我的任何或所有问题格式不正确,我也会很高兴知道这一点。


附录从@TheUndeadFish 的回答下面的冗长评论,以及对一个非常相似主题的问题的回答,我相信我对我想提出的我自己的问题有一个初步的答案。

让我们跟随源代码文件的原始字节来看看实际字节是如何通过整个编译过程转换为运行时行为的:

  • C++ 标准“表面上”要求任何源代码文件中的所有字符都是 ASCII 的(特定)96 个字符子集,称为basic source character set. (但请参阅以下要点。)

    就源代码文件中这96个字符的实际字节级编码而言,标准并没有规定任何特定的编码,但是96个字符都是ASCII字符,所以在实践中,源代码是什么编码从来没有问题文件在,因为所有存在的编码都使用相同的原始字节表示这 96 个 ASCII 字符。

  • 但是,字符文字代码注释通常可能包含这些基本 96 以外的字符。

    这通常由编译器支持(即使 C++ 标准不要求这样做)。源代码的字符集称为source character set. 但是编译器需要在其内部字符集(称为execution character set)中提供这些相同的字符,否则那些丢失的字符将在编译器实际之前被其他(虚拟)字符(例如正方形或问号)替换处理源代码 - 请参阅下面的讨论。

    编译器如何确定用于对源代码文件的字符进行编码的编码(当出现在 之外的字符时basic source character set)是实现定义的。

    请注意,编译器可以为其内部使用与源代码文件编码所表示的字符集不同的字符集(随心所欲地编码)execution character set

    这意味着即使编译器知道源代码文件的编码(这意味着编译器也知道源代码字符集中的所有字符),编译器仍可能被迫转换源代码文件中的某些字符。字符设置为不同的字符execution character set(从而丢失信息)。标准规定这是可以接受的,但编译器不得将 中的任何字符转换source character setexecution character set.

    没有什么是对C ++标准说,编码用于execution character set,就像没有提到的人物所需要的的支持execution character set(除了在人物basic execution character set,其中包括在所有字符basic source character set加上额外的那些少数例如NULL字符和退格字符)。

    即使是微软,似乎也没有在任何地方非常清楚地记录在 MSVC 中如何处理此过程中的任何一个。 即,编译器如何确定源代码文件的编码和相应字符集是什么,和/或选择的execution character set是什么,和/或将用于execution character set源代码文件编译期间的编码是什么.

    似乎在 MSVC 的情况下,编译器将尽最大努力为任何给定的源代码文件选择编码(和相应的字符集),回退到机器的当前语言环境的默认代码页编译器正在运行。或者,您可以采取特殊步骤,使用编辑器将源代码文件保存为 Unicode,该编辑器将在每个源代码文件的开头提供正确的字节顺序标记 (BOM)。这包括 UTF-8,BOM 通常是可选的或排除在外的 - 对于 MSVC 编译器读取的源代码文件,您必须包括 UTF-8 BOM。

    execution character setMSVC 的 及其编码而言,继续下一个要点。

  • 编译器继续读取源文件,并将源代码文件的字符的原始字节从 的编码source character set转换为(可能是不同的)相应字符的编码execution character set(如果给定,则为相同的字符)字符出现在两个字符集中)。

    忽略代码注释和字符文字,所有这些字符通常都在basic execution character set上面提到的。这是 ASCII 字符集的一个子集,因此编码问题无关紧要(实际上,所有这些字符在所有编译器上都以相同的方式编码)。

    但是,关于代码注释和字符文字:代码注释被丢弃,如果字符文字仅包含 中的字符basic source character set,那么没问题 - 这些字符将属于basic execution character set并且仍然是 ASCII。

    但是,如果源代码中的字符文字包含 之外的字符basic source character set,那么这些字符将被转换为 ,如上所述execution character set(可能会有一些损失)。但如前所述,该字符集的字符和编码都不是由 C++ 标准定义的。同样,MSVC 文档在此编码和字符集方面似乎非常薄弱。也许它是编译器运行所在机器上的活动语言环境指示的默认 ANSI 编码?也许它是UTF-16?

  • 在任何情况下,将被刻录到字符串文字的可执行文件中的原始字节与编译器对execution character set.

  • 运行时,mbstowcs被调用并传递来自前一个项目符号点的字节,不变。

    现在是 C 运行时库解释传递给 的字节的时候了mbstowcs

    因为没有提供语言环境来调用mbstowcs,所以 C 运行时不知道在接收这些字节时使用什么编码 - 这可以说是这个链中最薄弱的环节。

    C++(或 C)标准没有记录应该使用什么编码来读取传递给mbstowcs. 我不确定标准是否规定输入到的字符应该与编译器的字符mbstowcs相同,或者编译器的编码是否与execution character setexecution character setmbstowcs.

    但我的初步猜测是,在 MSVC C 运行时中,显然当前运行线程的区域设置将用于确定运行时execution character set和表示此字符集的编码,后者将用于解释传递给mbstowcs.

    这意味着这些字节很容易被误解为与源代码文件中编码的字符不同的字符 - 就我而言,非常难看。

    如果我对这一切都是正确的,那么如果您想强制C 运行时使用特定编码,您应该调用 Window SDK's MultiByteToWideChar,正如@HarryJohnston 的评论所示,因为您可以将所需的编码传递给该函数。

  • 由于上述混乱,确实没有一种自动方法来处理源代码文件中的字符文字。

    因此,正如/sf/answers/130666791/ 所提到的,如果您的字符文字中有可能包含非 ASCII 字符,您应该使用资源(例如GetText's 方法,它也可以通过Boost.Locale在 Windows 上xgettextPoedit附带的.exe结合使用),并且在您的源代码中,只需编写函数以将资源加载为原始(未更改)字节。

    确保你的资源文件保存为UTF-8,然后确保通话功能在运行时明确支持UTF-8为他们char *的和std::string的,如(从建议utf8everywhere.org使用)Boost.Nowide(不是真的在 Boost 中,我认为)wchar_t在调用任何将文本写入对话框等的 Windows API 函数之前的最后一刻从 UTF-8 转换为(并使用W这些 Windows API 函数的形式)。对于控制台输出,您必须调用SetConsoleOutputCP-type 函数,如/sf/answers/130666791/ 中所述

感谢那些花时间在这里阅读冗长的建议答案的人。

The*_*ish 5

源代码文件的编码不会影响mbstowcs. 毕竟,该函数的内部实现不知道什么源代码可能正在调用它。

在您链接的 MSDN 文档上是:

mbstowcs 使用当前语言环境来执行任何与语言环境相关的行为;_mbstowcs_l 是相同的,只是它使用传入的区域设置。有关详细信息,请参阅区域设置

然后,有关区域设置的链接页面会引用setlocalembstowcs ,这就是影响的行为的方式。

现在,看看您提出的传递 UTF-8 的方式:

mbstowcs (dest, u8"Hello, world!", 1024);
Run Code Online (Sandbox Code Playgroud)

不幸的是,据我所知,一旦您使用有趣的数据,这将无法正常工作。即使它能够编译,它也只能编译,因为编译器必须将u8其视为与char*. 就其而言mbstowcs,它会相信该字符串是在设置的任何语言环境下进行编码的。

更不幸的是,我不相信有任何方法(在 Windows / Visual Studio 平台上)可以设置使用 UTF-8 的区域设置。

因此,这恰好适用于 ASCII 字符(前 128 个字符),只是因为它们在各种 ANSI 编码以及 UTF-8 中碰巧具有完全相同的二进制值。如果您尝试使用除此之外的任何字符(例如带有重音或变音符号的任何字符),那么您会发现问题。


就我个人而言,我认为mbstowcs这些都是相当有限和笨拙的。我发现 Window 的 API 函数MultiByteToWideChar一般来说更有效。特别是,它只需传递CP_UTF8代码页参数即可轻松处理 UTF-8。

  • Visual Studio 附带的 CRT 管理每个线程的区域设置,而不是每个进程的区域设置。*当前活动的语言环境*是指代码运行所在的线程的语言环境。 (2认同)

Die*_*ühl 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().