问题引入
考虑这样的一段代码:
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
void aligned_string() {
string cstr1 = "中文", cstr2 = "对齐";
string estr1 = "English", estr2 = "Alignment";
cout << setw(20) << left << cstr1 << cstr2 << endl;
cout << setw(20) << left << estr1 << estr2 << endl;
}
int main() {
aligned_string();
}
在默认的windows中文环境下(源代码文件和控制台的编码都是936
,即一种GBK),setw()
函数在中英文混合的场景下可以正常工作。
但是在unix环境下(默认使用UTF-8作为源代码和控制台的编码),setw()
函数在中英文混合的场景下并不能正常工作。
产生原因
-
setw()
函数的特性
setw()
函数中传入的参数,作为最终约定的“宽度”,并不是字符数,而是字节数。中英文在不同的编码方式下,有着不同的编码方式,每个字符所对应的字节数也不尽相同。 -
编码方式
Windows环境下的GBK和unix环境下的UTF-8对于英文字符均使用相同的单字节的编码方式(即ASCII),而对于中文字符的编码处理并不相同:- GBK:使用定长的双字符编码。因此由于等宽字体中,中文字符的宽度是英文字符的两倍,同时字节也是两倍,所以最终得到了正常的打印效果。
- UTF-8:使用变长的字符编码。
使用以下的函数来按照小端序打印字符串"中文"
:
void print_string_bytes(string str) {
for (int i = 0; i < str.size(); ++i)
cout << hex << (unsigned)(unsigned char)str[i] << " ";
cout << endl;
}
Windows平台的输出如下:
与在线平台的转换结果相同
Unix平台的输出如下:
和使用在线平台进行UTF-8转换的结果相同
解决方法
根据上面的分析可以得出结论:unix平台下setw()
对于中文字符处理的问题在于变长字符编码。因此只要将字符串转换为定长字符编码(比如说GBK),之后计算两者之间的差值修改setw()
的参数即可。
使用以下函数计算两种编码方式之间字节数量的差值:
#include <codecvt>
#include <locale>
class chs_codecvt : public std::codecvt_byname<wchar_t, char, std::mbstate_t> {
public:
chs_codecvt() : codecvt_byname("zh_CN.GBK") {}
};
int encoding_diff(string str) {
wstring_convert<codecvt_utf8<wchar_t>> cv1;
wstring wstr = cv1.from_bytes(str);
wstring_convert<chs_codecvt> cv2;
return str.size() - cv2.to_bytes(wstr).size();
}
其中codecvt_byname
的构造函数是protected
的,因此需要自定义一个转换器类,继承codecvt_byname
。
需要说明的是,在Linux环境下需要进行locale-gen
,编辑/etc/locale.gen
,删除zh_CN.GBK
前面的注释符号,并执行locale-gen
命令。
最终将打印函数修改如下:
void aligned_string() {
string cstr1 = "中文", cstr2 = "对齐";
string estr1 = "English", estr2 = "Alignment";
string cestr1 = "中文English", cestr2 = "对齐Alignment";
cout << setw(20 + encoding_diff(cstr1)) << left << cstr1 << cstr2 << endl;
cout << setw(20 + encoding_diff(estr1)) << left << estr1 << estr2 << endl;
cout << setw(20 + encoding_diff(cestr1)) << left << cestr1 << cestr2 << endl;
}
打印效果如下: