您的位置:首页 > 编程语言 > C语言/C++

C++字符串常量跨平台编译问题(与字符串编码相关)

2014-05-16 08:34 369 查看
1. 问题
在C++代码中,给一个string类型的变量赋值一个中文字符串常量,例如:
string s = "中文字符串"
变量s中保存的字节内容是什么?如果源文件的编码格式转换了,比如从GB2312转换为UTF-8,变量s中的内容会发生变化吗?其结果是否与编译器有关?

假设有一个C++源程序:

#include <iostream>
#include <string>
#include <iomanip>
using namespace std;

int main(int argc, char * argv[])
{
string s="中文字符串";
cout << "Length of string: "<< s.size()<< endl;
cout << "Bytes in string: "<< endl;
for (string::size_type i=0; i< s.size();i++)
{
cout <<setfill('0')<< setw(2)<<hex<< (int)(0x0ff &s[i]) << " ";
}
cout << endl;

return 0;
}

使用三种不同的编译器编译:
Windows平台(简体中文): Vs2008 SP1
Linux x86(Ubuntu 10.04 LTS): G++ 4.4.3
ARM Linux: arm-none-linux-gnueabi-g++ 4.3.2(From Sourcery G++ Lite 2008q3-72)

2. 实验1
在Vs2008中创建一个空的VC++项目,并创建一个cpp文件,将上面的代码放进去,编译运行,得到的结果是:
Length of string: 10
Bytes in string: d6 d0 ce c4 d7 d6 b7 fb b4 ae

在Linux下使用g++编译: g++ -o test test.cpp,运行结果相同

使用ARM Linux的交叉编译工具: arm-none-linux-gnueabi-g++ -o testtest.cpp编译,并在目标板上运行,结果也相同。

在简体中文的Windows中运行的Vs2008,创建的C++源文件的缺省编码方式为CP936,即GB2312,该编码下,每个中文字符对应2个字节。通过检查(可以使用Python把那几个字节来换转换一下编码),可以确定,输出的10个字节就是这5个中文字符的CP936的编码值。

考虑到源代码的编码格式也是CP936,似乎可以得到如下结论,在这几个编译器中,字符串常量的字节内容,直接就是字符串在源代码文件中对应的字节内容。

照这个结论,如果把源文件转换为UTF-8编码存储,对应输出的字节应该是这几个中文字符的UTF-8编码,而长度应该是15(这几个中文字符的UTF-8编码都是3个字节)。

3. 实验2

在Vs2008中,打开源代码,选择文件菜单中的“高级保存选项",修改编码为Unicode (UTF-8带签名)-代码页65001,把同样的源代码分别在三个编译器中编译运行。
Vs2008:结果仍然与原来相同,即长度为10,字节内容为CP936的10个字节,而不是UTF-8的15个字节;
G++(x86): 长度变为15,字节内容为15个字节,经验证为那几个中文字符对应的UTF-8编码;
G++ ARM: 编译错误, error: stray '\357' in program
error: stray '\273' in program
error: stray '\277' in program
从实验的情况来看,x86下的GCC确实是按照实验1中的结论工作,字符串常量的值随文件编码改变而发生了相应的改变;而Vs2008输出的结果却表明,Vs2008似乎自动将编码转换成了原来的样子即CP936的形式,这个结论还是挺意外的。
而G++ ARM的编译错误,应该是由于这个版本的编译器不能识别源文件插入的BOM导致的,后来使用更新的交叉编译器(G++ 4.4.1 from Sourcery G++ lite2010q1-202)证实了这一点,并且输出结果与x86中的G++编译的完全相同。

到这一步,可以初步得出结论,G++不会试图转换常量的字符串编码,会直接使用与源文件字符编码对应的字符串常量。Vs2008会试图将Unicode的编码格式转换成对应地区(Locale)的缺省编码(简体中文系统下,为GB2312即代码页CP936),并按照这个编码的内容来确定常量字符串的值(后者有一些推论在里面)。

4. 跨平台编译的解决方案

如果一份代码需要在Visual Studio下编译,又需要使用GCC编译,如何选择文件的编码方式比较合理呢。
如果代码中不会出现中文或其它非Ascii字符,这当然不是问题,因为AsciiUTF-8及CP936这些编码格式在ASCII字符的范围内都是一致的。

如果有中文这类字符串常量,就会有点麻烦。如果全部采用UTF-8(带BOM),这样,在Windows下可以正常工作,而且类似于printf或cout这样的控制台输出,都可以正常输出中文字符串,因为如前面的实验,VisualStudio在编译时已经把UTF-8编码的源文件中的字符串常量编程了本地的编码,因此在操作系统的控制台窗口中输出完全没有问题。相反,如果直接向控制台输出一个UTF-8编码的中文字符串,显示是不正确的。
而在Linux下(以Ubuntu为例),由于GCC直接使用了UTF-8源代码文件中字符串的编码,而Ubuntu下,输出的UTF-8编码字符串可以被正确显示,而如果直接输出CP936编码的字符串就不行了。
因此,如果可以把所有的源代码全部转换为带UTF-8(带BOM)的编码存储,VS和GCC编译的结果,都可以得到正确的显示输出。

不过,如果要把字符串通过网络传输给另一个程序;或直接把字符串写到文件中去,这种方式下,传输的内容是不同的。在Windows下,传输的是CP936的编码内容,根据前面的例子“中文字符串"这五个汉字共传输10个字节;而用GCC编译的结果却传输15字节的UTF-8编码字符串。接收数据的程序,则需要来源的不同根据情况进行解码。如果接收程序是使用Java编写的,由于Java里面的字符串都使用Unicode,因此需要根据不同的来源,使用CP936或UTF-8来解码收到的内容为Unicode字符串。通常,客户端程序是不应该关心提供数据的服务程序是VS编译的还是GCC编译的,因此这样分别进行解码比较困难。

因此,更常见的做法是传输时使用统一的编码格式,而程序在从传输信道获得字符串或向传输信道写入字符串时,根据自身的情况进行解码或编码。而在UTF-8和CP936之间,选择更恰当的一种作为信道中传输的字符串,不是一个困难的选择。UTF-8可以传输所有的字符,而CP936只能传输简体中文的字符,从通用的角度考虑肯定应该是UTF-8。

也就是说,如果代码都使用UTF-8编码,而信道中传输字符串时也使用UTF-8编码的情况下,在VS中编译时,需要在输出字符串到信道或从信道获得字符串时,要进行CP936(或其它多字节编码)与UTF-8之间的转换。转换可以通过Windows提供的SDK完成,具体方法是,先将多字节字符串转换为Windows的WideChar的字符串,再将WideChar转换为UTF-8字符串(没有直接转换的API),反之类似。
相关的Windows的SDK为:
MultiByteToWideChar
WideCharToMultiByte
后者可以将WideChar转换为CP936或UTF-8.不过函数调用时,需要注意转换后的UTF-8所需要的存储长度要大于输入的WideChar字符串的长度,具体长多少,是不确定的,与字符串内容有关。有两个办法,一个是直接分配足够大的长度用来存储编码后的结果,例如WideChar字符串长度的四倍;另一个方法是一次一次试,先分配与WideChar相同的长度,调用WideChartToMultiByte,如果返回值是0且GetLastError返回 ERROR_INSUFFICIENT_BUFF,就在把缓冲区扩大点再试。

为了方便转换,写了一个Python脚本,可以用来转换一个目录内所有的.h,.cpp,.hpp,.cxx,.c文件;同时,可以识别已经转换过的文件,和有BOM标记的(UTF-16,UTF-32文件);如果没有标记,则认为是CP936编码的文件,将执行转换,脚本文件可在这里下载
使用方法为:
python toutf8.py -d youSourceDir

StackOverflow上的这篇文章同样解释了如何在C++代码里实现UTF8常量字符串的问题
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: