c++程序中涉及到中文字符的输入输出以及其他操作经常会出现乱码。乱码主要是由于程序的源文件编码、可执行文件编码以及程序运行环境的编码不匹配导致。比如,c++源程序文件编码为GB18030, 在源程序中有一中文窄字符串常量,程序运行时输出该字符串常量,运行环境的系统编码为UTF8时,就会输出乱码。
一、程序相关的编码
1.程序源文件编码
程序源文件编码是指保存程序源文件内容所使用的编码方案,该编码方案可在保存文件的时候自定义。
通常在简体中文windows环境下,各种编辑器(包括visual studio)新建文件缺省编码都是GB18030,所以不特别指定的话,windows环境下的c++源文件的编码通常为GB18030(GB18030兼容GBK);在linux环境下,默认的为UTF-8编码。
2.c++程序内码
源程序编译后,c++中的字符串常量变成一串字节存放在可执行文件中,内码指的是在可执行文件中,字符串以什么编码进行存放。这里的字符串常量指的是窄字符(char)而不是宽字符(wchar_t)。宽字符通常都是以Unicode(VC使用UTF-16BE, gcc使用UTF-32BE)存放。
通常简体中文版的VC使用内码为GB18030,而gcc使用内码缺省为UTF-8,单可以通过-fexec-charset参数进行修改。(可以通过在程序中打印字符串中每个字节的16进制形式来判断程序使用的内码)。
3.运行环境编码
运行环境编码指的是,执行程序时,操作系统或终端所使用的编码。程序中输出的字符最终要转换为运行环境编码才能显示,否则就会出现乱码。
常用的简体中文版的windows环境编码是GB18030,linux下最常用的环境编码是UTF-8。
4.三种编码之间的关系
程序源文件【源文件编码】--->(编译器编译) ---->目标文件【程序内码】----> (运行后输出信息)---->输出【运行环境编码】
编译器需要正确识别源文件的编码,把源文件编译为目标文件,并把源文件中的以源文件编码的字符串转换为以程序内码编制的字符串保存在目标文件中。
如果源程序中的为窄字符串常量,则程序运行时,直接将目标文件中对应的内码字符串输出;若为宽字符串常量,则程序运行时c++标准库需要正确识别终端的运行环境编码,并把程序的输出转换为运行环境所使用的编码,以便正确显示。
二、c++宽窄字符
c++有窄字符char和宽字符wchar_t的区别,分别有一套相应的类和函数(string/cout/strlen和wstring/wcout/wsclen等)。窄字符在不同的编译器下有不同的缺省编码形式(在简体中文VC下是GB18030编码,gcc下是UTF-8编码),宽字符一般都使用unicode编码,在VC下使用UTF-16,gcc下使用UTF-32。
c++在输出窄字符时会按照程序内码原样输出,不会进行编码转换,因此在使用窄字符要求程序内码和运行环境编码一致,才不会出现乱码;
c++在输出宽字符时,会自动转换为运行环境的编码,因此只要正确设置了运行环境编码,同一个程序就可以在不同编码的运行环境下正确显示中文。在程序中设置当前环境的字符编码,通过 locale::global(locale(""))进行设置。
兼容windows和linux的字符显示的做法
对于需要支持多语种的程序,使用窄字符存储字符串常量,源程序中使用ascii字符,对于非ascii字符,如中文等通过gettext等工具放到单独的语言包中。
三、用户输入、输出以及持久化
由于用户输入、输出即从文件、网络等设施读写的数据在程序底层看来都是字节,因此存在输入时如何把字节流解释成有效的信息,在输出时怎么把程序中的信息转换为正确的字节流的问题。
程序不对内容进行处理
如果用户不需要对字节流的内容进行处理,而输入的字符编码和输出的字符编码一致,则程序不需要对数据进行任何的编码转换,只需要把读入的数据原样写到输出即可,数据的字符编码与程序的编码没有关系。
程序需要对内容进行处理
如果程序需要在一定程度上对数据进行处理(如需要判断字符个数、对字符进行比较。在字符串附加或者去掉内容),就要把数据转换为一种明确的字符编码,一般来说是程序内码,再进行处理,在处理后再转换为所需的字符编码进行输出。
1.宽字符程序
如果只需要处理采用当前运行环境字符编码的数据,可以通过ios::imbue(可以指定io流的字符编码),在输入、输出时c++标准库会自动在所指定的字符编码与程序内码之间进行编码转换。如果不使用流的话,也可以通过标准的wcstombs()或者mbstowcs()函数进行当前编码(通过locale::globale()或setlocale()指定)与宽字符之间的转换。
2.窄字符程序
对于窄字符程序,如果数据的字符编码与程序内码一致也不需要进行编码转换,直接处理即可。
四、几个小实验
case 1
在windows简体中文环境下,设置vs2013源文件的编码为utf-8(默认为gb2312,通过“文件”——“高级保存选项”,选择UTF-8),同时设置编译器编译结果的内码为UTF-8(通过在cpp和h文件的头部添加
#pragma once
#pragma execution_character_set("utf-8")
即可),然后使用如下程序:
#pragma once
#pragma execution_character_set("utf-8")
// 本文件为utf-8 编码格式
#include<iostream>
#include<string>
using namespace std;
int main(){
std::string str = "好人";
cout << str << endl;
return 0;
}
由于简体中文windows的系统环境的编码为GB18030,而源文件被设置成UTF-8编码,同时也指示编译器编译的目标文件的程序内码使用UTF-8编码,那么在输出的时候会将中文输出乱码。
case 2
在windows简体中文环境下,设置vs2013源文件为utf-8编码,而可执行文件的内码保持默认为gb2312:
//文件选择保存为utf-8格式
#include<iostream>
#include<string>
using namespace std;
int main(){
std::string str = "好人";
cout <<str << endl;
return 0;
}
编译器会按照utf-8格式解析源文件,并进行编译,编译出可执行程序的内码为gb2312,系统运行环境的编码为GBK,可以正常显示中文。
case 3
(3.0)在windows简体中文环境下,vs2013源文件分别采用gb2312和utf-8编码,可执行程序的内码分别采用gb2312和utf-8编码(四种组合),直接wcout输出宽字符,结果均为空。
#pragma once
//#pragma execution_character_set("utf-8")
// 本文件为utf-8 编码格式
#include<iostream>
#include<string>
using namespace std;
int main(){
std::wstring wstr = L"好人";
std::wcout << wstr.size() << endl; //输出为2
std::wcout << wstr << endl;
return 0;
}
(3.1)在windows简体中文环境下,vs2013源文件分别采用gb2312和utf-8编码,可执行程序的内码分别采用gb2312和utf-8编码(四种组合),imbue设置输出格式为std::locale("chs")或者通过locale::global(locale("")) 设置,然后wcout输出宽字符:
#pragma once
//#pragma execution_character_set("utf-8")
// 本文件为utf-8 编码格式
#include<iostream>
#include<string>
using namespace std;
int main(){
// locale::global(locale("")); 或者 std::wcout.imbue(std::locale("chs"));
std::wstring wstr = L"好人";
std::wcout << wstr.size() << endl;
std::wcout << wstr << endl;
return 0;
}
因为,宽字符在输出时候会自动进行编码格式的转换,对wcout指定了输出流的输出编码,则可以输出正确的结果。
case 4
在windows下使用mysqlconn库连接mysql数据库,由于为了和客户端通信,mysql数据库采用UTF-8编码。在visual studio 2013下程序访问数据库中中文字符的某条属性,在select的时候,需要将中文字符写在源代码中。由于vistual studio默认的源文件编码和可执行程序的内码都是GBK2312,因此在访问mysql数据库的时候中文字符变为乱码从而无法访问。
可以通过将包含中文字符的那个源文件(只需要含中文字符的那个即可,不需要全部)强制编译为UTF-8编码的可执行程序内码,然后访问mysql即可。
#pragma once
#pragma execution_character_set("utf-8")
// 本文件为utf-8 编码格式
#include<iostream>
#include<sstream>
#include"SQLExecutor.h"
using namespace std;
int main(){
SQLExecutor* sql_conn = new SQLExecutor();
std::ostringstream os;
os << "select attribute_id from attribute_define where attribute_name = '入网状态'";
std::cout << os.str() << endl;
ResultSet* rs = sql_conn->ExecuteQuery(os.str());
while (rs && rs->next()){
std::cout << "attribute_id = " << rs->getUInt(1) << endl;
}
delete rs;
delete sql_conn;
return 0;
}