达内LOGO和北京达内网址达内科技培训项目:Java培训 3G培训 Android培训 软件测试培训北京达内服务电话
C++培训
关于C++的一些编码问题

   我们传统的程序基本都只在Windows或只在Linux下运行,Windows程序使用简体中文GB18030编码,Linux程序则只使用英文,多年以来这些程序运行起来都没有问题。

    近年来,随着程序的组件化,部分代码特别是公用组件都需要同时支持Windows及Linux平台,这样就出现了不同程度的编码问题,例如在编译时编译器报错,或者在运行时出现乱码。这些问题都和程序选用的字符编码不正确有关。

    本文简要地分析了C++培训中容易出现的一些字符编码问题,并提供了建议的方案。受经验和时间的限制,有些内容可能不一定全面,仅供大家参考。

    1. C++源文件的编码需要特别考虑吗?

    1.1. 几个相关概念

    首先要区分几个概念:

    C++源文件的编码

    指的是C++源程序文件(。cpp/.h)本身使用什么字符编码(GB18030/UTF-8等)。

    C++程序的内码

    编译后,C++中的字符串常量都会变成一串字节存放在可执行文件中。这个内码指的就是在可执行文件中,字符串以什么编码进行存放。这里的字符串常量指的是窄字符(char)而非宽字符(wchar_t)。宽字符通常是以Unicode(VC使用UTF-16BE,gcc使用UTF-32BE)存放。

    运行环境编码

    指的是执行程序时,操作系统或终端所使用的编码。程序中输出的字符最终要转换为运行环境编码才能正确显示,否则就会出现乱码。

    1.2. 各种环境下通常使用的编码

    C++源文件的编码

    通常在简体中文Windows环境下,各种编辑器(包括Visual Studio)新建文件的缺省编码都是GB18030,所以不特别指定的话,Windows环境下C++源文件的编码通常为GB18030。而在Linux环境下,最常使用,也是推荐使用的是UTF-8编码。

    C++程序的内码

    一般来说,我们常用的简体中文版VC所使用的内码是GB18030,而gcc/g++使用的内码缺省是utf-8,但可以通过-fexec-charset参数进行修改。Note 可以通过在程序中打印字符串每个字节十六进制形式来判断程序所使用的内码。

    运行环境编码

    我们常用的简体中文版Windows的环境编码是GB18030,而Linux下最常用的环境编码是UTF-8。

    1.3. 这几个编码之间的关系

    源程序需要由编译器编译为目标文件,目标文件运行后输出信息到终端,因此这几个编码之间存在一些的关联:

    +--------+ | 源程序 |----------源文件编码 +---+----+ | 编译器编译 +---+----+ |目标文件|----------程序内码 +---+----+ | 运行后输出信息 +---+----+ | 输出 |----------运行环境编码 +--------+

    编译器需要正确识别源文件的编码,把源文件编译为目标文件,并把源文件中的以源文件编码的字符串转换为以程序内码编制的字符串保存在目标文件中。

    Note 当源文件的字符编码与程序内码都是UTF-8时(gcc的缺省情况),gcc似乎并不会对源文件中的字符编码进行转换,而是直接把字符串原样存放到目标文件中,在这种情况下,源程序中的GB18030编码的字符串在输出时仍然为GB18030编码。但如果在其它源文件字符编码的实际值与编译选项不同时,会在编译时报无法从XXX转换到UTF-8的错,因此还不清楚为什么两个编码都是UTF-8时,GB18030 编码的源文件能通过编译。C++标准库需要正确识别终端的运行环境编码,并把程序的输出转换为运行环境所使用的编码,以便正确显示。

    在这过程中,如果有一个环节出现问题,就会导致程序的输出发生异常,产生乱码或其它更严重的后果。

    2. 源文件应该采用什么编码?

    2.1. 编译器对不同源文件编码的支持一样吗?

    gcc/vc各版本对C++源文件编码有不同的处理:

    gcc (v4.3.2 20081105):

    支持UTF-8编码的源文件,UTF-8编码的源文件不能有BOM。

    开始支持带BOM的UTF-8文件。

    vc2003:

    支持UTF-8编码的源文件,UTF-8编码的源文件可以有BOM,也可以没有。

    vc2005+:

    如果源文件使用UTF-8编码的话,必须有BOM。Note gcc提供了-finput-charset参数可以指定源文件的字符编码,但由于标准头文件都是ascii编码的,因此如果要引用标准头文件的话,源代码的编码必须兼容ascii。而vc未能找到类似的选项。

    2.2. 源文件应该采用什么编码?

    很多文章都推荐C/C++代码中只使用ascii字符,如果有非ascii字符可以用\xHH

    或\uXXXX表示。注释中建议使用utf-8编码。也可以使用gettext 把非ascii字符串放到单独的语言文件中,而在源代码中只保留ascii字符。

    在实践中,由于\xHH或\uXXXX等方式很不直观,容易出错且不易发现,而未必所有程序都需要支持多语言,因此未必想引入gettext或类似的解决方案。在这样的情况下,大家都习惯在源程序文件中直接写入中文等非ascii字符,这就需要选择一种至少能被gcc和vc接受的文件编码。本来,Unicode是解决多语言问题的最好选择,而UTF-8由于与ASCII兼容,也是最通用的Unicode编码方式,但从上面的资料中可见,如果用UTF-8的话,gcc(至少是低版本)不允许有BOM,而vc2005 以上要求必须有BOM,因此同一个文件无法在gcc及vc下通过编译,UTF-8似乎不是一个好的选择。但如果使用gcc比较高的版本(4.4.0以上?),使用带BOM的UTF-8编码文件应该也是可行的。

    考虑到目前现状,我们一般都在简体中文Windows下工作,源文件中使用GB18030=编码似乎是一个比较现实的选择。在vc下可以直接编译,而在gcc下也可以通过增加编译选项-finput-charset=gb18030予以支持。而且根据维基百科中GB18030的词条内容,GB18030 is a superset of ASCII and can represent the whole range of Unicode code points(GB18030向后兼容ASCII,并且能表示所有的Unicode码点),因此使用GB18030有足够的表达能力,可以表示所有的Unicode字符。使用GB18030的唯一缺点就是在非简体中文版本的VC下,由于无法指定源文件的编码,因此有可能无法正确识别此编码的源文件。

    3. 应该使用什么程序内码?

    正如前面提到的,C++有窄字符(char)和宽字符(wchar_t)的分别,分别有一套相应的类和函数(string/cout/strlen与wstring/wcout/wcslen等)。前者在不同的编译器下有不同的缺省编码(简体中文vc是GB18030,gcc是UTF-8),后者一般都使用Unicode,其中vc下使用UTF-16,gcc缺省使用UTF-32。C++在输出窄字符时会按程序内码原样输出,不会进行编码转换,因此在使用窄字符时要求程序内码与运行环境编码一致,这样才不会出现乱码。由于简体中文版vc的程序内码是GB18030,因此使用窄字符的vc程序只能运行在GB18030环境下。同样,由于gcc缺省使用UTF-8作为程序内码,因此使用窄字符的gcc程序只能运行在UTF-8的终端环境下。(这里说的都是在源代码中直接写中文等非ascii字符的程序。用前面提到的gettext及其它工具,使用窄字符的程序也可以在不同编码的运行环境中正确输出中文)

    C++在输出宽字符时会自动转换为运行环境的编码,因此只要正确设置了运行环境编码,同一个程序就可以在不同编码的运行环境中正确显示中文。这一点与Java/.Net很象,Java/.Net的字符串类型都使用Unicode,在输入/输出时都需要与当前运行环境的编码进行互转。

    一般来说,如果需要支持多语言,有两种比较好的做法:

    使用窄字符,但源程序中只使用ascii字符,非ascii字符通过gettext或其它

    工具放到单独的文件中,由gettext等工具处理编码转换的问题。

    在各种编码的运行环境中均能正确输出中文。

    程序中不能直接出现非ascii字符,也不能通过\uXXXX方式指定非ascii字符,后者也会被编译器转换为非ascii字符并存放在目标文件中。

    注释中可以使用ascii兼容的编码,不影响编译器。

    有比较多的现成代码可供重用。

    使用宽字符。

    在各种编码的运行环境中均能正确输出中文。

    程序中可以使用非ascii字符。

    需要配合前面的源程序文件编码设置,让编译器能正确识别源程序中的非

    ascii字符。

    由于以前使用宽字符的程序比较少,可供重用的代码较少。

    Note 如果程序中需要一些固定字符编码的字符串常量,例如固定是GB18030

    编码的字符串常量,这些常量应该以\xXX的方式存放字符串常量经GB18030编码后的内容,这样的内容才不会被转换为程序的内码,也不会转换为运行环境编码。

    4. 运行环境应该用什么字符编码?

    正如上面提到的,使用窄字符和使用宽字符的程序对运行环境的字符编码要求是

    不一样的。

    使用宽字符,只要在程序中正确设置当前环境的字符编码(一般通过locale::global(locale("")) 进行设置),C++标准库会在输入、输出时正

    确进行字符编码转换,因此可以适应各种编码的运行环境。

    使用窄字符,但程序中不出现非ascii字符的话,对运行环境没有特别要求,

    可以适应各种编码的运行环境。

    使用窄字符,程序中也直接使用汉字等非ascii字符的话,由于C++标准库会把

    目标文件中保存的字符串(以程序内码保存)直接输出,不会进行字符编码转换,因此要求运行环境的编码与程序内码一致。即简体中文VC编译的程序只能运行在GB18030环境下,gcc编译的程序只能运行在UTF-8环境下(可以在编译时通过-fexec-charset参数进行修改)。

    5. C++源文件编码的选择

    5.1. 几种可行做法

    根据上面的讨论,目前看来,要兼容Windows/Linux,VC/gcc的话,有几种做法

    :

    使用窄字符,源程序中只使用ascii字符,非ascii字符,如中文等通过

    gettext等工具放到单独的语言包中。

    这种做法比较多人推荐。

    兼容VC及gcc各版本。

    由于源程序中不出现非ascii字符,因此不需要考虑源程序文件的编码问题。

    兼容各种编码的运行环境。

    使用窄字符,源程序中允许使用非ascii字符。

    要求运行环境的编码与程序内码一致,即只支持GB18030编码的Windows及

    UTF-8编码的Linux。

    根据源程序使用的编码不同,对编译器的兼容性也不同:

    使用窄字符,源程序使用带BOM的UTF-8编码。

    兼容VC各语种的各版本。

    兼容gcc 4.4.0以上版本。

    使用窄字符,源程序使用GB18030编码。

    兼容VC的简体中文各版本。

    兼容gcc各版本,但在编译时需要加上-finput-char=gb18030参数。

    使用宽字符,源程序中允许使用非ascii字符。

    兼容各种编码的运行环境。

    根据源程序使用的编码不同,对编译器的兼容性也不同:

    使用窄字符,源程序使用带BOM的UTF-8编码。

    兼容VC各语种的各版本。

    兼容gcc 4.4.0以上版本。

    使用窄字符,源程序使用GB18030编码。

    兼容VC的简体中文各版本。

    兼容gcc各版本,但在编译时需要加上-finput-char=gb18030参数。

    5.2. 推荐做法

    根据我们的现状,对于需要支持多语种的程序,建议使用窄字符,源程序中只使

    用ascii字符。

    对于不需要支持多语种的程序,考虑到重用已有的代码,可以考虑使用窄字符,

    采用GB18030编码,但只能运行在GB18030编码的Windows环境及UTF-8编码的

    Linux环境下。

    6. 其它问题

    6.1. 用户输入、输出及持久化

    由于用户输入、输出及从文件、网络等设施读写的数据在程序底层看来都是字节

    流,因此存在在输入时如何把这些字节流解释成有效的信息,在输出时怎么把程

    序中的信息转换为正确的字节流的问题。

    如果程序本身不需要处理这些数据,只是把数据从一个来源搬到另一个地方(

    如把用户输入保存到文件,或者从一个流读入,写到另一个流等),而输入的字符编码与输出的字符编码一致的话,程序不需要对数据进行任何编码转换,只需要把读入的数据按原样写到输出即可,数据的字符编码与程序的编码没有关系。

    比如网站应用程序,只需要保证用户页面使用UTF-8编码,数据库、数据文件也都使用UTF-8编码,那么用户输入的数据可以直接写入数据库及数据文件,从数据库或数据文件中读取的数据也可以直接展现给用户,不需要进行编码转换。

    如果程序需要在一定程序上对数据进行处理(如需要判断字符个数、对字符进

    行比较、在字符串上附加或去掉内容),就要把数据转换为一种明确的字符编码,一般来说是程序内码,再进行处理,在处理后再转换为所需的字符编码进行输出。

    对于宽字符程序,如果只需要处理采用当前运行环境字符编码的数据,可以通过ios::imbue()可以指定io流的字符编码,在输入、输出时C++标准库会自动在所指定的字符编码与程序内码之间进行编码转换。如果不使用流的话,也可以通过标准的wcstombs()或mbstowcs()函数进行当前编码(通过locale::global()或setlocale()指定)与宽字符之间的转换。

    对于窄字符程序,如果数据的字符编码与程序内码一致也不需要进行编码转换,直接处理即可。

    对于其它情形,需要引入iconv或类似的字符编码转换库,以便实现不同

    字符编码之间的转换。

    6.2. gettext、iconv的替代品

    由于gettext及iconv都属于GNU Project,考虑到版权因素,并非所有程序,特别是商业程序,都适合使用这些库。在Boost 1.48.0中,Boost.Locale库首次正式发布,该库提供了gettext、iconv的功能,并在此基础上进行了增强,提供了大小写变换、字符顺序比较、时间的处理 、分词、数字的格式化输入/输出、消息格式化、多语种支持、字符编码转换等功能,值得进一步研究及使用。