一个老朋友,字符集和字符编码格式,以及由它们导致的中文乱码问题,可能早会了些处理手段,但始终没真搞明白过!几个回荡过N次的问题:为啥会有这档子麻烦事?啥时候需要多字节与窄字节的转换?在Qt的IDE-Debug运行时,其控制台总将汉字打成乱码?Qt的QString包含中文时,如何转换为char*才能被类似C-FILE-fopen的函数正确解析使用?怎么理解QString是Unicode编码的?文件存储/文件传输与字符编码又有什么关系?
接下来我们将主要以fopen为例,展开相关的测试与讨论;因,以前遇到的编码问题,主要集中在了文件路径转换上,故挑选C-FILE函数,做几个简单的测试。C-FILE的具体相关操作请参考。
//C-FILE-fopen DEFINE
FILE *__cdecl fopen(const char * __restrict__ _Filename,const char * __restrict__ _Mode);
带着个初始问题:fopen可以接受的_Filename参数,其对编码格式是有要求的? Let’s start a new journey!
基本代码
//const char *pFileName = "F:/123.txt";
const char *pFileName = "F:/中国汉字.txt";
FILE *outFile = fopen(pFileName, "rb");
if (NULL == outFile)
qDebug() << "Can't open specified file absolute path!";
新建Qt工程(cpp文件默认utf-8编码),使用上述最简的代码,不指定任何QTextCodec设置,纯英路径打开无误,中文路径打不开。现,用NotePad改变上述cpp以ANSI格式编码(会看到 “中国汉字” 变成了 “涓浗姹夊瓧”, 删除乱码重新输入;在IDE重新加载此文件后,右上角显示器编码格式为System),重新运行程序,中文路径可打开。
PS:在QtCreator-工具-选项-文本编辑器-行为-文件编码 可以修改代码文件的默认编码格式,修改后将对新建文件生效。
保持上述cpp文件System格式不变,无QTextCodec;让我们手动引入QString,而不再使用纯粹的const char字符数组。QString引入的问题随之而来!
QString strTmp("F:/中国汉字.txt");
//错误代码 - 具体可参 该文QString转char* 章节
//char *pFileName = strTmp.toLocal8Bit().data();
char pFileName[256] = {0}; //{'\0'} 内存结果一致
memcpy(pFileName, strTmp.toLocal8Bit().data(),strTmp.toLocal8Bit().size());
qDebug() << strlen(pFileName); //17 //10个自己代表了4个汉字 三三两两的节奏啊 什么鬼?
上述测试fopen失败,是意料之外的,难道"ANSI格式编码cpp文件",**走了一遭QString再转换回去,就不对了?**这点留到下边的QString构造过程中讨论。先看测试,我们就着上边的部署,在main中增加如下的QTextCodec设置。
QTextCodec::setCodecForLocale(QTextCodec::codecForName("System")); //main.cpp
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("System")); //mian.cpp
...
qDebug() << strlen(pFileName); //15 //Every chinese character use 2 byte
重新执行测试程序后,发现可成功打开中文路径的文件。在进行QTextCodec设置前,调试第一行代码时,strTmp自己就是乱码,这说明它在构造时就出了问题,即,QString使用const char*进行构造时,默认把大于128的字符当成Latin-1字符(参见重认识QString章节),与我们的系统编码是不一样的!后来发现此测例中,CodecForLocale不设置也没关系,这点从toLocal8Bit的帮助中可以get到,因为它切实默认使用了"System"的codec。
将所有的cpp文件的编码格式都改回系统默认的utf-8,中文内容重新编辑输入。测试代码复用测试2,包括QTextCodec(这是一个意外,本来没打算测试这种情况,是昨天的测试落下了这两行QTextCodec配置,没有删除)。
fopen执行失败是意料之中的,此时strlen(pFileName)==19 每个汉字占用了3个字节。(问题就在这:cpp文件编码格式与设置的CodecForCStrings不对应,产生了一个真正的错误数据(通常的乱码,只是显示不正确,数据是对的),这个过程究竟是怎样的,从utf-8的文件,在"System"的codec作用下生成了一个unicode的strTmp,这个过程中,编译器、运行环境参与了什么?写这句话的时候,我也没具体搞明白,继续向后吧…),抛却对上述错误结果的分析不顾,采用下边的QTextCodec设置,可以使得该测试中的fopen执行成功!
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));
QTextCodec::setCodecForLocale(QTextCodec::codecForName("System"));
主要回答,什么是字符集、字符编码格式,区别是什么,联系是什么?QString在编码处理上到底有什么特别,它一堆的与编码有关的接口,咋个意思都是?必选彻底搞明白 ANSI/ASCII/UNICODE/UTF-8 etc. 都是啥?
字符集和字符编码格式有什么区别和作用,字符集和字符编码的区别,主要参考上述两篇文章,来理解了啥是字符集和字符编码,以及它们的区别,对Unicode和UTF-8的关系有了重新的认识。至于ANSI、ASCII、GBK、etc. 可参百科。
字符集是书写/显示系统各字符(字母、数字、符号、汉字…)的集合,而字符编码则是将字符映射为特定的字节或字节序列,是一种规则。通常特定字符集采用特定的编码方式,即一种字符集对应一种字符编码,例如:ASCII、IOS-8859-1、GB2312、GBK,都是即表示了字符集又表示了对应的字符编码,此时基本可将两者视为同义词。但Unicode不是,它采用现代的模型,UNICODE字符集,有多个编码方式,分别是UTF-8,UTF-16,UTF-32编码。
在UTF-8下,大多数字符中文,日文和韩文字符,编码是3个字节的,不常见的CJK字符、表情符号等通常是4字节编码。BOM(Byte Order Mark)字节序(字节顺序的标识),其实就是用大端还是小端。用UTF-8编码存储“很屌”两个字{E5BE88, E5B18C},显示时若用GBK解码进行展示,通过查表我们获得以下信息:E5BE{寰} 88E5{埚} B18C{睂},两个字符的数据硬生生的解成了3个乱码字符。
常见的汉字字符集编码:
写在前边,不要用错目录分隔符哦,这是部分猿在进行文件操作时的常见问题:主要是毁在了win上的目录分隔符/正斜杠与\反斜杠都可以,而反斜杠的使用又涉及到了转义字符。
PS:路径分隔符号的问题
QString-Detailed Description
The QString class provides a Unicode character string. QString stores a string of 16-bit QChars, where each QChar corresponds(符合) one Unicode 4.0 character.
Unicode is an international standard that supports most of the writing systems in use today. It is a superset(超集) of US-ASCII and Latin-1, and all the US-ASCII/Latin-1 characters are available at the same code positions.
QString str = "Hello";
QString converts the const char * data into Unicode using the fromAscii() function. By default, fromAscii() treats character above 128 as Latin-1(ISO-8859-1编码) characters, but this can be changed by calling QTextCodec::setCodecForCStrings().
通常我们设置了QTextCodec::setCodecForCStrings(QTextCodec::codecForName(“UTF-8”));则理论上此时在QString构造后会调用 static QString::fromUtf8函数进行转换,将一个utf8(cpp文件的编码)的 const char *data 转换为Unicode的QString,也即QString自身始终是Unicode的。
In all of the QString functions that take const char * parameters, the const char * is interpreted(解析) as a classic C-style ‘\0’-terminated string. It is legal(合法的) for the const char * parameter to be 0.
下边是被"滥用"的两行代码,将要讨论的是,它的真正意义和作用过程!在高版本Qt中,这几个函数已经不见踪影。QTextCodec-4.8.6 和 QTextCodec-5.14,请参考。取消原因大约是:qt5以后强制要求其源码(即cpp文件编码)为utf-8编码(在4.8.6中可以将cpp文件编码强改成ANSI编码),顾,不再需要setCodecForTr/ForCString函数啦?#详不细究!以setCodecForCStrings为例,4.8.6中,它在QString使用const char*构造对象时,起作用!
Qt5中尽管去掉了setCodecForTr/ForCString函数,但是默认编码还是latin1。如果你要想使用 “中国汉字” 这样的字符串,必须自己使用QTextCodec或这QString::fromXXX 相关函数进行转换;也可以用QStringLiteral宏等手段转换。 另,Qt的这一举措,使得其与MSVC的兼容性有所降低,此处不深究。
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));
The QTextCodec class provides conversions between text encodings. Qt provides a set of QTextCodec classes to help with converting non-Unicode formats to and from Unicode.
Qt uses Unicode to store, draw and manipulate strings. In many situations you may wish to deal with data that uses a different encoding. For example, most Japanese documents are still stored in Shift-JIS or ISO 2022-JP, while Russian users often have their documents in KOI8-R or Windows-1251.
[river.qu] Chinese doc or chinese pc file system list show are still used GBK. The GBK codec provides conversion to and from the Chinese GB18030/GBK/GB2312 encoding.[QTextCodec Class Reference GB18030-0]
void QTextCodec::setCodecForCStrings ( QTextCodec * codec ) [static]
Sets the codec used by QString to convert to and from const char * and QByteArrays. If the codec is 0 (the default), QString assumes(采用) Latin-1. 要真理解这个函数,还得阅读下QString Detailed Description关于Initializing a String 的部分。函数功能:设置QString用来进行(与const char*和QByteArrays数据)格式转换的编码器对象。如:
However, in the Unicode range, there are certain codepoints that are not considered characters. The Unicode standard reserves the last two codepoints in each Unicode Plane (U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, etc.), as well as 16 codepoints in the range U+FDD0…U+FDDF, inclusive, as non-characters. If any of those appear in the string, they may be discarded and will not appear in the UTF-8 representation, or they may be replaced by one or more replacement characters.
总结:其作用主要体现在QString的构造上,将不同编码(如UTF-8、System)的const char* data、文件数据流等,调用与之对应的转换函数,转成Unicode编码的QString。此处绝不是设置QString自身的编码格式,它始终是Unicode的。
void QTextCodec::setCodecForLocale ( QTextCodec * c ) [static]
Set the codec to c; this will be returned by codecForLocale(). If c is a null pointer, the codec is reset to the default. 注意:这里的Local是Qt当前运行应用程序,不是系统的Local编码格式;该函数主要是配合toLocal8Bit的使用。
QTextCodec * QTextCodec::codecForLocale () [static]
Returns a pointer to the codec most suitable for this locale.
On Windows, the codec will be based on a system locale. On Unix systems, starting with Qt 4.2, the codec will be using the iconv library. Note that in both cases the codec’s name will be “System”.
QByteArray QString::toLocal8Bit () const
Returns the local 8-bit representation(表示法) of the string as a QByteArray. The returned byte array is undefined if the string contains characters not supported by the local 8-bit encoding.
QTextCodec::codecForLocale() is used to (被用来) perform(完成) the conversion from Unicode. If the locale encoding could not be determined, this function does the same as toLatin1().
注释:
QString::toLocal8Bit 将 Unicode编码的QString,转化为其它编码格式 ,以用以存储(如文本文件)或非Qt的显示(如win文件系统列表)。这个转成QByteArray的过程,依赖于codecForLocale() ,也即setCodecForLocale(…)的设置结果 。
其他:
QTextCodec::setCodecForTr(QTextCodec::codecForName(“UTF-8”));其含义与setCodecForCStrings类似,它是为tr()来服务的(tr()隶属QObject,但是可以使用Q_DECLARE_TR_FUNCTIONS()宏定义为非QObject派生类添加tr()函数,详见GUI-Qt4_P153)。
只知道将txt文件、cpp文件等,用NotePad打开可修改编码格式,这里的编码格式指的是?与通常说的字符编码格式是怎样的联系呢?
PS: 二进制文件和文本文件;文件编码格式的检查策略:临时参考1、临时参考2;
至于,将UTF-8编码的字符串作为文件名称创建文件或更名文件,是乱码;将UTF-8的char字符串传送给fopen不认;它们应该是与PC文件系统管理模块有关(它也是应用程序,也是猿与的),它就不认UTF-8编码,它希望的是?是ANSI(System-GBK 这个选择应该是在安装win操作系统选择-简体中文-时决定的)
为了尽可能的抛却无关影响 此处用char串 + C-FILE操作,不使用也不需要任何QString及QTextCodec设置;主要由两个代码段组成,如下:
//代码段1 //下边的代码写在ANSI(system)格式编码的cpp文件中
FILE *pFile = fopen("F:/testfile.dat", "wb");
if(NULL != pFile) qDebug() << "ok";
//char chardata[50] = "abc123";
char chardata[50] = "abc中国汉字123";
int numok = fwrite(chardata, 1, strlen(chardata), pFile);
fclose(pFile);
//代码段2 //下边的代码写在UTF-8(Without BOM)格式编码的cpp文件中
FILE *pFile = fopen("F:/testfile.dat", "ab");
if(NULL != pFile) qDebug() << "ok";
char chardata[50] = "abc中国汉字123";
int numok = fwrite(chardata, 1, strlen(chardata), pFile);
fclose(pFile);
具体测试过程如下:
测试结论如下(可能是片面的):
当第一次向文本文件写入汉字时采用的编码格式,会被标记记录。之后再执行字符串(含汉字)追加写入时,标记不变。且使用文本编辑器打开文件时,按照标记的编码格式进行解码。非标记编码格式的字符串将会以"乱码示人"。文本文件的默认编码格式是ANSI,若始终只向其中写入英文,不管是什么编码格式的,文件的编码格式保持ANSI不变;直到第一次写入中文串,若此时中文串为UTF-8编码的,写入后,整个文件的编码格式也由ANSI变成了UTF-8格式编码。
思考一个新问题,是谁在操控,导致了上述的测试结果?主要是谁在什么时候固化了一个存储文件的编码格式?
还有,我可能想搞明白的是,cpp文件(或者说是源代码)中的,那个字符串"中国汉字",是怎么跟运行程序联系起来,又是怎样没有被解析好,而产生了乱码?这可能在编译阶段就决定了?(这里有一步,从文件文件cpp,到二进制文件exe的转换过程…)
理解过上边的测试用例,再重新思考:文件编码、文件编辑器、代码中的字符串、代码变异等概念,将会更快些。
对于文件存储和传输本身,它们无关编码格式,因为它们处理的是比特流,存取/收发什么都无关紧要,只要存储/传输的执行者,知道解码即可。只是,针对同一个信息(字符串),采用不同编码格式(如GBK和UTF-8),其存储/传输的字节大小可能不同。
从一个异常问题起:在IDE的Locals and Expressions调试窗口中,查看一个char*变量时,代码与结果如下:
...QString strZipFileName = "F:/4.zip";
//执行前 因未赋空 调试窗显示pcharZipName 指向3个字节
char *pcharZipName = (char*)malloc(strZipFileName.toLocal8Bit().size());
//执行后 调试窗显示pcharZipName指向23个乱字节 //实际size为8
//执行后 pcharZipName 为""
memset(pcharZipName, '\0', strZipFileName.toLocal8Bit().size());
//前8个字节为正确内容"F:/4.zip" 后边是乱码
memcpy(pcharZipName, strZipFileName.toLocal8Bit().data(), strZipFileName.toLocal8Bit().size());
//更正后的代码实现:
char *pcharZipName = (char*)malloc(strZipFileName.toLocal8Bit().size() + 1);
//size() == 8
memset(pcharZipName, '\0', strZipFileName.toLocal8Bit().size() + 1);
memcpy(pcharZipName, strZipFileName.toLocal8Bit().data(), strZipFileName.toLocal8Bit().size());
将QString转换为char array[n]时存在同样的问题,若不多开一个字节的’\0’,则可能导致char指针内存错误;Here,因内存指向范围错误,导致的乱码,与C字符串的要求’\0’结尾有关,而与pcharZipName指向的字符串是什么编码格式的关系不大。编码异常导致的乱码,是存储、显示方向的错误,数据本身是对的,只是展示出来是错误的。
**PS1:*const char data 到QString的构造,QString类有自动的’\0’处理,但逆向的,QString::toLocal8Bit()、QString::toStdString() etc. 有类似的处理吗? 先别急着下结论,测试下:
QString strTmp("F:/abc.txt");
char *pFileName = strTmp.toLocal8Bit().data();
/** @brief running result
从内存查看器中可以可以发现pFileName的指向范围是100个字节 传入到fopen中不识别
**/
//错误现象与上述类似
const char *pFileName = strTmp.toStdString().c_str();
//就不多开一个字节 //那错误现象与上述类似 //可参见本节第一段代码
通过测试,不难得出,QString、QByteArray、std::string、etc. 逆转const char* cdata,不会在串内存的末尾有一个’\0’,必须的要自己来增加,通常方法是malloc size+1 或定义数组时 size+1,开辟+1内存并memset前,直接去给cdata赋值,不但木有意义还会发生严重错误。
//是否指定数组长度无影响
char a[/*10*/] = "river"; //变量查看器显示正常 详见如下截图
printf("%s-strlen-%d", a, strlen(a)); fflush(stdout); //river-strlen-5
/** @brief //可惜了 下边的代码是编译不通过的 不然倒是挺方便的
* array must be initialized with a brace-enclosed(大括号) initializer //little ps: 字符串常亮赋值时可省略{} ..良久
**/
char pFileName[] = strTmp.toLocal8Bit().data();
char pFileName[20] = strTmp.toLocal8Bit().data();
上图是char a[ ] = “river”;的调试截图,自己补充了一个结束符。当直接定义char a[10] = “river”;时,变量查看器显示后边有5个’\0’字符,是自动初始化上的,考虑到数组的{}赋值方法,猜测这些’\0’是省略掉的大括号的左右结果,使用char a[10] = {};测试,发现果然a被初始化成了10个’\0’字符。另外,在此处重申下strlen()与sizeof()的区别。
PS2: string.h 和 std::string 及 C字符串
首先可参考string.h百科,C标准库有
在Qt-IDE(mingw)下 #include (mingw32\i686-w64-mingw32\include\c++ 无后缀) #include
//mingw32\i686-w64-mingw32\include\c++\bits\stringfwd.h
//最终包含到文件(无后缀)中 可写#include 跳进去LookLook
namespace std _GLIBCXX_VISIBILITY(default)
{ ... typedef basic_string<char> string; ... }
//basic_string 是个C++模板类
template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{ ... has func insert/replace/find etc. ... };
至此,我们已经基本理解了QString构造char* 数据为类对象的基本过程,也知道const char* sData构成QString时,不用加’\0’,但是我们却常常在处理那些不是const的char*数据时,犯错误!例如,收到了一个交互数据 (char *buffer, bufLen),然后想存到本地
//想当然的代码就出来啦
QString strData = QString();
上述问题通常有两种解决办法:
FILE *__cdecl fopen(const char * restrict _Filename,const char * restrict _Mode);
现在可以基本回答最初的问题了,_Filename只能是系统编码格式的字符串。在Qt的IDE下,如果不多事设置CodecForLocale为非"System", 并且设置了CodecForCStrings与cpp文件编码格式一致,只要调用QString::toLocal8Bit().data(),就可得到能被fopen正确打开的,含中文的文件全路径。
但是,有时候不得不设置CodecForLocale为UTF-8,如,交互数据在传输格式要求为utf-8。这种情形下,我们总不太想每次调用类似fopen函数时,先临时设置CodecForLocale为"System",那你就等使用多字节转宽字节了。Qt通过QString和QTextCodec等类为我们封装好了多/窄字节间的转化,只要合理地利用上面章节中那些to… from… setCode… etc.函数即可。
//定义在cpp开头 函数体之外 //防止调试运行告警
static QTextCodec *pcode = QTextCodec::codecForName("gbk"); //国标
static QTextCodec *pcode = QTextCodec::codecForName("gb18030"); //效果同上
//函数体中的具体代码
QString strTmp("F:/中国汉字.txt");
//虽然测试时运行无误 //但是不建议这样直接赋值char*
const char *pFileName = pcode->fromUnicode(strTmp).data();
qDebug() << strlen(pFileName); //15 //and fopen can ok
上述代码中的fromUnicode函数的作用,有点类似setCodecForLocale(…“System”) + toLocal8Bit 结合后的作用结果。在Qt下,当迫不得已设置了ForLocale(UTF-8)时,可以用上述pcode代码来进行码制间的转换。
看来在Qt的IDE下,已经用不着自己再去写转化函数了,只有在不带框架的纯C++环境中,才可能会用到,这里简单的列举下曾经用过的代码。
//need
Here,主要是针对Qt集成开发环境的,对于qDebug() << qstringObj; 在直接运行的情况下,可以在其控制台正常打印中文,但是在Debug运行下,却会被打印成乱码?这个问题已经困扰我好久了!进来,又发现了一段奇怪的代码:
QString strTmp("F:/中国汉字.txt");
qDebug() << strTmp;
QTextCodec *pcode = QTextCodec::codecForName("gb18030"); //guobiao
const char *pFileName = pcode->fromUnicode(strTmp).data();
qDebug() << strlen(pFileName);
FILE *outFile = fopen(pFileName, "rb");
if (NULL == outFile)
qDebug() << "Can't open specified file absolute path!";
上述代码在非调试运行下,一切正常。但是在Debug运行下,全是问题,首先strTmp被打印文乱码;而且运行到第一行代码时,告警can’t find linker symbol for virtual table for `QTextCodec’ value found `shadowWidth’ instead;且变量查看器运行异常,无法查看strTmp值,设置直接导致运行崩溃;fromUnicode也运行失败,pFileName赋值也失败,指针范围错误;打开操作当然是失败!
参考了好久前写的代码,在函数体外cpp开始的地方 编写定义static QTextCodec *pcode… 可以解决上述相关告警问题。
难不成,wchar.h 中的FILE操作函数,可以直接接受QString.c_str() ? (utf-8编码),待测试。
很确认Qt默认新建的文件格式是uft-8,但是用Notepad打开,查看格式,却昭然显示着"以ANSI格式编码"为选中状态,这个吓坏我了,难不成今天的关于编码的测试,把系统搞毁了?有个想法闪了下,在QtCreateor下为代码增加了一行中文注释,保存后,这时候重新用NotePad打开此文件,这回显示对了"以UTF-8 无BOM格式编码"。
进一步的测试如下:新家了一个普通的文本文档1.txt,其中输入abc,保存。在QtCreator下,在一个.h文件中,增加一行注释 //你好然后保存。然后用NotePad打开.h文件,变更编码格式为ANSI,将变为乱码的注释(//浣犲ソ)搞到剪切板。然后打开1.txt文本文件,粘贴并保存。重新用Notepad打开1.txt查看其编码格式,也成了UTF-8格式,且拷贝的内容显示正常。猜测,NotePad会自动的做一些显示调整。此处没有再去做进一步的研究,不要被坑哦!