网页资讯视频图片知道文库贴吧地图采购
进入贴吧全吧搜索

 
 
 
日一二三四五六
       
       
       
       
       
       

签到排名:今日本吧第个签到,

本吧因你更精彩,明天继续来努力!

本吧签到人数:0

一键签到
成为超级会员,使用一键签到
一键签到
本月漏签0次!
0
成为超级会员,赠送8张补签卡
如何使用?
点击日历上漏签日期,即可进行补签。
连续签到:天  累计签到:天
0
超级会员单次开通12个月以上,赠送连续签到卡3张
使用连续签到卡
07月26日漏签0天
c语言吧 关注:798,850贴子:4,357,577
  • 看贴

  • 图片

  • 吧主推荐

  • 视频

  • 游戏

  • 1 2 3 下一页 尾页
  • 49回复贴,共3页
  • ,跳到 页  
<<返回c语言吧
>0< 加载中...

[探索] 字符编码那些事

  • 只看楼主
  • 收藏

  • 回复
  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
你是否有过这样的烦恼:想用printf打印汉字,结果出来的全是乱码?用scanf输入的汉字,明明看起来和代码中预设的字符串一模一样,可是strcmp就是不能比较相等?涉及到汉字输入输出的程序,在自己计算机上的表现完美无瑕,到了同学的计算机上就变成了一团浆糊?不必忧虑,因为这个帖子将与你一起探索scanf和printf的秘密,发现字符和字符编码之间的关系,最终解决以上问题。


  • GTA小鸡
  • 吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
好好好,时不时就有人问为什么乱码,每次都解释一遍根本没精力


2025-07-26 05:52:30
广告
不感兴趣
开通SVIP免广告
  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
前言
由于众所周知的历史原因,字符编码是一笔糊涂账,为了建立一个清晰的认识,首先明确此帖的讨论范围。
研究对象:
1. 与字符相关的类型(char, wchar_t, wint_t);
2. 字符串字面量;
3. 程序参数(argv);
4. 多字节/宽字符转换函数;
5. scanf, printf, fscanf, fprintf及其宽字符版本。
影响研究对象的因素:
1. 系统默认代码页;
2. 控制台代码页;
3. 编译器选项;
4. setlocale。
如无特别说明,其他一切可能影响研究对象的因素将保持系统默认值。
由于lz没有大端序计算机,以下一切多字节对象和多字节编码均默认为小端序。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
正式开始前,请先允许我请出一位大诗人:

接下来“李白”将带领我们一起探索字符编码,各位需要对这个表格的内容有一个大致的印象,否则后面遇到“李白”的各种“化身”时容易一头雾水。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第一章 msvcrt
作为一个历史悠久的C Runtime,msvcrt实在是让人又爱又恨。但无论如何,只要你是Windows系统的老用户,就不可避免地会与它纠缠在一起。
在这一章,我们将使用MSYS2构建的GCC编译器,如无特殊说明,C语言标准将采用C23,编译器的任何选项将保持默认值。
这一章的scanf和printf系列函数均指代msvcrt的导出版本。(是的,scanf函数有不止一个版本,我们只关心msvcrt提供的那一个)


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
测试代码已通过百度网盘分享,可以在自己的计算机上验证结论。
s/1HwN_QaHpThdxchBV-CLz9w
百度网盘的域名是什么不需要lz多说吧?
提取码:vcrt
至于这些代码怎么用,后面会说。


  • 魔法使坚果墙
  • 麻婆豆腐
    11
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
3


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第一章第一节 结论
(谁写文章把结论放开头啊)
先看看3L提及的四个因素到底会产生什么影响。
1. 系统默认代码页决定了argv中字符串的编码,还会影响setlocale(LC_ALL, "");的行为;
2. 控制台代码页影响从控制台窗口输入的字符和打印到控制台窗口的字符;
3. 编译器选项-finput-charset, -fexec-charset, -fwide-exec-charset分别影响源文件、多字节字符串字面量和宽字符串字面量的编码;
4. setlocale决定了运行时多字节字符的编码,从而影响多字节/宽字符转换函数和scanf, printf系列函数的行为。
接下来简单解释一下这四句的含义。


2025-07-26 05:46:30
广告
不感兴趣
开通SVIP免广告
  • 锯条🪚
  • 彩虹面包
    13
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
c/cpp 本地化处理几乎是所有人必踩的坑,win 平台和 linux 的处理方式也是天差地别


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
1.1.1 系统默认代码页
在我们熟悉的简体中文系统上,系统默认代码页是936,对应的编码是GB2312,后来的GBK和GB18030在GB2312的基础上作了扩充,这里我们不管这么多,直接把CP936和GBK当作等同的说法。
系统默认代码页不是一成不变的,而是随着语言和区域的不同而变化。在许多西欧国家,系统默认代码页是1252,在繁体中文系统上,系统默认代码页是950,对应的编码是Big5,即著名的大五码,相信老玩家们对这个编码一定不会陌生。
即便在同一台计算机上,系统默认代码页也是可以通过语言和区域设置修改的。从Windows 10 version 1803 (10.0.17134.0)开始,Windows支持把UTF-8设为系统默认代码页,不难看出有向Linux靠拢的意思。

系统默认代码页对C程序中的两个东西有显著的影响,一个是程序参数argv,一个是setlocale函数。
在main函数中,形参argv的声明长这样:
char *argv[]
argv是一个数组,它的每个元素都是指向多字节字符串的指针(除了argv[argc]是空指针),这些多字节字符串的编码就是系统默认代码页所对应的编码。
举两个例子:
假设有一个C程序prog.exe,在简体中文系统上以
prog 李白
启动该程序,那么argv[1]的前4个元素是:{0xc0, 0xee, 0xb0, 0xd7}。

如果在繁体中文系统上以同样的方式启动该程序,那么argv[1]的前4个元素是:{0xa7, 0xf5, 0xa5, 0xd5}。

请注意,以上两个示意图是对实际过程的一种高度简化的描述,程序参数传递的过程实际上非常复杂,其中涉及到多次编码转换,远非三言两语能说得明白。
至于setlocale函数,C标准对它有这样的描述:

我们感兴趣的是以空字符串为参数调用setlocale的情况:
setlocale(LC_ALL, "");
如果系统默认代码页是936,这相当于
setlocale(LC_ALL, ".936");
如果系统默认代码页是950,则相当于
setlocale(LC_ALL, ".950");


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
1.1.2 控制台代码页
首先明确一点,这里的控制台指的是cmd.exe。
在默认情况下,控制台代码页和系统默认代码页是一样的,这可以免去很多不必要的麻烦。当然,我们可以通过修改控制台窗口属性,或者使用CHCP命令来改变控制台代码页,让它变得和系统默认代码页不一样。
控制台代码页有什么用呢?
我们知道,C程序处理的是数字,就算对字符,C语言也是当作整数来处理的,可想而知,当我们在控制台输入的字符传递给C程序时,其中必定发生了从字符到字符编码的转换。要转换到哪种编码呢?自然是控制台代码页说了算。
举个例子,假设控制台代码页是936,一个C程序调用了scanf函数,此时在控制台窗口中输入
李白
并按下回车,则进入stdin的前4个字节是:{0xc0, 0xee, 0xb0, 0xd7}。

如果控制台代码页是950,前4个字节将变成:{0xa7, 0xf5, 0xa5, 0xd5}。
当C程序向控制台窗口输出字符时,会发生相反的转换:

控制台窗口不会在乎stdout传过来的字节到底具有什么含义,它只是忠实地按照控制台代码页来“解读”这些字节,如果这些字节本身的编码和控制台代码页对应的编码相同,那么打印在控制台窗口上的字符就是我们想要的结果;然而,它们一旦不相同,结果可就糟糕了:

如图,明明是GBK编码的字符,却按照大五码来解读,这样打印在控制台窗口上的就只能是“乱码”了。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
1.1.3 编译器选项
-finput-charset
输入字符集
这个选项决定了编译器会以何种编码解读源代码文件。
这个选项的用法很简单,如果源代码文件source.c的编码是GBK,只要在编译时加入
-finput-charset=GBK
就可以确保编译器能正确地读取代码中的汉字;如果source.c的编码是UTF-8,只需要
-finput-charset=UTF-8
即可。
认识到这一点,就不难想出一些有趣的用法。比如有几个编码各不相同的源文件,我们可以用不同参数的-finput-charset分别编译每一个源文件,再把它们链接到一起。(虽然可以这么做,但是一个项目的源文件用几种不同的编码实在是糟糕透顶)
如今,使用UTF-8存储非ASCII文本已然成为了共识,越来越多的编程语言已经把UTF-8作为默认的字符编码,所以,如果你在源代码中使用了汉字,最佳的做法是把源文件保存为UTF-8编码,并在编译时加上-finput-charset=UTF-8选项。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
-fexec-charset
执行字符集
这个选项决定了C代码中多字节字符串字面量和字符常量的编码。
这里的“多字节字符串字面量”并不是一个很正式的说法,只是用来指代形如"李白"的字符串字面量,之所以加上“多字节”是为了与形如"Li Bai"的字符串字面量进行区分,因为后者的每一个字符都是单字节。
C代码中的一切字符串字面量最终都会变成char数组,如果编译时使用了
-fexec-charset=GBK
那么"李白"会化作具有5个元素的char数组:{0xc0, 0xee, 0xb0, 0xd7, 0x0},
如果是
-fexec-charset=Big5
数组的内容就会变成:{0xa7, 0xf5, 0xa5, 0xd5, 0x0}。
执行字符集也可以使用UTF-8编码。
和输入字符集只需简单地与源文件编码匹配不同,选择执行字符集是一件困难的事情,也许你只想要把-fexec-charset设置成跟系统默认代码页一样就算了,但事情远不止这么简单。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
-fwide-exec-charset
宽执行字符集
这个选项决定了C代码中宽字符串字面量和宽字符常量的编码。
虽然看起来和-fexec-charset很像,但是选择宽执行字符集要简单得多。在Windows上,wchar_t总是两字节的无符号整数类型,并且它代表的编码总是UTF-16,所以我们完全可以不假思索地用
-fwide-exec-charset=UTF-16LE
把宽执行字符集也设为UTF-16,以此与Windows的规定保持一致。
在Windows上,把宽执行字符集设为UTF-16是唯一正确的做法,此时宽字符串字面量L"李白"会被编译器转换成具有3个元素的wchar_t数组:{0x674e, 0x767d, 0x0}。
如果把宽执行字符集设为UTF-32,数组的内容会变成:{0x674e, 0x0, 0x767d, 0x0, 0x0, 0x0},这显然不是我们想要的结果。


2025-07-26 05:40:30
广告
不感兴趣
开通SVIP免广告
  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
1.1.4 setlocale
C标准库提供了大量处理字符和字符串的函数,其中不乏能处理多字节字符的佼佼者,像mbrtowc就直接把这种能力写到了名字上,而诸如scanf和printf等函数,虽不显山露水,但它们也有处理多字节字符串的能力。不过问题来了,多字节字符的编码有那么多种,这些标准库函数怎么知道要用哪种编码呢?
这是由程序的locale信息决定的。正好,C标准库也提供了setlocale函数来修改程序的locale信息。
C标准只定义了两种标准locale:"C"和""。"C"代表翻译C程序所需的最小环境,""代表locale-specific native environment,除此之外的locale都是实现定义的。
C程序启动时,会自动地执行一次setlocale(LC_ALL, "C");,这个"C" locale只支持单字节字符,对处理汉字没有任何帮助,我们不需要它。
那我们只好选择setlocale(LC_ALL, "");了,这个函数调用会把多字节字符编码设为和系统默认代码页相同,在简体中文系统上,这个函数调用会把多字节字符编码设为GBK,一旦调用成功,后续的mbrtowc, scanf和printf等函数都会把多字节字符当成GBK编码的字符来对待。
不要把setlocale的作用和-fexec-charset混淆,-fexec-charset只是决定字符串字面量的编码,不影响标准库函数的行为,而setlocale不会改变已经存在的字符串字面量。
显然,只有在setlocale设置的多字节字符编码和执行字符集匹配时,标准库函数才能够正确地处理字符串字面量。如果不匹配,乱码就又要在我们的面前蹦跶了:

如图,编译时使用了-fexec-charset=Big5把字符串字面量的编码设为大五码,运行时却使用setlocale(LC_ALL, ".936");把多字节字符编码设为GBK,此时mbsrtowcs不会在乎字符数组的内容是大五码这一事实,而是强行将其按照GBK进行解读,并转换成宽字符串(UTF-16编码),这样转换得到的结果显然不是"李白"的UTF-16编码。


登录百度账号

扫二维码下载贴吧客户端

下载贴吧APP
看高清直播、视频!
  • 贴吧页面意见反馈
  • 违规贴吧举报反馈通道
  • 贴吧违规信息处理公示
  • 1 2 3 下一页 尾页
  • 49回复贴,共3页
  • ,跳到 页  
<<返回c语言吧
分享到:
©2025 Baidu贴吧协议|隐私政策|吧主制度|意见反馈|网络谣言警示