在Windows Visual C/C++编程时,经常需要从Github或者其他既有项目中“借鉴”(呵呵哒)一些代码过来。这个时候,就要格外注意字符集和回车换行带来的编译问题。
这类编译问题造成的错误千奇百怪,可能在编译时、链接时,甚至运行时造成困扰。尤其是运行时,很难发现,在极其倒霉的时候,会造成删库跑路或者惨痛的事故。
1. 源代码文件的规格
源代码文件存储为可见文本,但各个平台、工具链上具体的编码规格是略有区别的。对含有中文等非ASCII标准字符的源代码,会遇到编码/字符集的概念。对跨越Linux、Windows环境的源代码,会涉及换行符的概念。
- 编码/字符集: windows下,Visual Studio创建的C语言源文件一般存储为 本地字符集(如中文windows就是GB2312)。Linux下,默认是UTF-8的。
- 换行符: windows下,Visual Studio创建的C语言源文件每行代码后面是两个不可见字符,即\015\012,也就是\n\r,而Linux默认只有\n.
下表是几类C++ IDE/编辑器常用(默认配置或常用配置)例子。
操作系统 | 编辑器/IDE | 编码/字符集 | 回车/换行 |
---|---|---|---|
Windows | Visual Studio | ANSI(GB2312系) | \n\r |
Windows | Visual Studio Code | UTF-8 | \n\r |
Windows | Qt Creator | UTF-8 | \n\r |
Windows | CodeBlocks | ANSI(GB2312系) | \n\r |
Windows | Notepad | ANSI(GB2312系) | \n\r |
Linux | 大部分编辑器/IDE | UTF-8 | \n |
因此,如果引用的代码原本就是Linux下的,那它十有八九是UTF-8+\n的配置组合。如果引用的是既往合作企业的Visual C++头文件,那通常都是GB2312+\n\r的组合。
2. Visual C++编译器踩坑
Visual C++编译器默认处理的是 ANSI本地字符集,若用的是中文Windows,则默认.c、.h文件里都是GB2312编码的汉字+\n\r换行。如果直接引用了非默认格式的代码,则会有很多现象。
汉字编码 | 回车换行 | 编程工序 | 可能故障 | 规避方法 |
---|---|---|---|---|
GB2312 | \n\r | 默认 | ||
GB2312 | \n | 一般没问题 | ||
UTF-8 | \n\r | 编译 | 嵌入式注释导致编译报错 | 1.首末汉字前后插入多个空格 2.独立注释,后续间隔空行 |
UTF-8 | \n | 编译 | 嵌入式注释导致编译报错 | 1.首末汉字前后插入多个空格 2.独立注释,后续间隔多个空行 |
UTF-8 | \n | 链接 | 导出符号名称不正确或含有奇怪字符 | 转换源码为\n\r存储 |
UTF-8 | 运行 | 乱码 | 转换源码为GB2312存储 Qt用QString::from编码临时解决 Qt用英文界面+翻译解决 | |
UTF-8 | 运行 | 崩溃 | 数字节和数字符得到的数字不一致,也不是x2关系。UTF-8存在3字节汉字。转换源码为GB2312存储 | |
UTF-8 | 运行 | 意外的结果 | 多字节汉字导致if等语句被吃掉了。转换源码为GB2312存储 |
3. 原因分析
UTF-8编码通过首字节连续1的个数来确定当前字符使用几个字节来表示。其原理引用网络截图如下表:
编译器用GB2312的定长2字符来处理非ASCII可见字符,而3字节以上的汉字会打破这种奇偶属性。因此,如果错误的用GB2312来解释UTF-8,对3字节以上的编码,后续的字节会“粘”到下一个正常ASCII字符的起点上去:
比如下面这个字符:
1110xxxx 10xxxxxx 10xxxxxx 00010111
本应是1个汉字+1个字母,却被解释为2个汉字。
为什么插入空格有用: 错误的UTF-8译码带来的误差,在尾部存在多个空白字符时可能被抵消,因此拥有很多尾部空格的汉字一般不会造成编译错误。
为什么\n换行比\n\r换行错误更古怪: 因为\n\r相当于2个字符,可以比\n一个字符纠正更多的解析误差。
3.1 隐晦的链接错误
隐晦的错误是多行函数定义里的注释:
/**!...巴拉巴拉...导出DLL
一堆Doxygen注释
*/
DLL_EXP_API
/*返回值修改巴拉巴拉 2021-02*/
int
STDCALL fun (
int p1, /*bala*/
int p2, /*bala*/
)
{
//...
}
曾经出现过,因为相邻两个注释块因为字节数误差连成一体,变成了:
/**!...巴拉巴拉...
#$%^&**%乱七八糟L_-02*/
int
STDCALL fun (
int p1, /*bala*/
int p2, /*bala*/
)
{
//...
}
结果死活报链接错误!因为这个符号压根没有导出。
3.2 危险的运行时错误
被吃掉的if:
//...巴拉巴拉...
//一顿注释猛如虎
if(safe(root))/*这安全了吧*/
if(auto_del && partion_size(root_id) >= MAX_C)
deleteAll(root);/*一堆IF绝对安全*/
变成了:
//...巴拉巴拉...
//一顿%$全了吧*/
if(auto_del && partion_size(root_id) >= MAX_C)
deleteAll(root);/*芭比Q了,时怎么断到这里的?!*/
这种错误在测试时也是比较难发现的,尤其是当MAX_C相对较大的时候。
4. 建议操作-使用Git批量维护差异
既然使用Visual Studio 作为开发工具,建议还是尽可能使用默认的编码,除非Visual Studio 向 MinGW 等第三方编译器,或者Linux平台输出库,此时建议使用UTF-8编码。复用代码要注意这些事情,有没有什么比较简单和普适的批处理方法呢?
有的,可以使用Git服务作为中转。Windows的Git可以设置编码自动转换:
user@Dev283$ git config --global core.autocrlf true
设置后,在克隆和签出等操作中,会把代码转换为windows换行符;在哈希、提交时,会转换回Linux换行符。