C++字符串常量跨平台编译问题

C++字符串常量跨平台编译问题(与字符串编码相关),有需要的朋友可以参考下。


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 testtest.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常量字符串的问题。

posted on 2017-07-31 20:13  kenny.wmh  阅读(539)  评论(0编辑  收藏  举报

导航