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

 
 
 
日一二三四五六
       
       
       
       
       
       

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

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

本吧签到人数:0

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

  • 图片

  • 吧主推荐

  • 视频

  • 游戏

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

回复:[探索] 字符编码那些事

  • 只看楼主
  • 收藏

  • 回复
  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
四个影响因素已经分析完了,接下来lz将转换视角,从C程序可能遇到的各种字符串入手,重新审视四个影响因素的作用。
(哎,说好的简单解释呢?你管这叫简单?好吧,不管这些了,刚才的都是饭前甜点,下面该上正餐了)


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
C程序中的字符串,无非有以下几种来源:
1. 程序员书写的字符串字面量;
2. 经程序参数(argv)传入的字符串;
3. 用户从控制台窗口输入的字符串;
4. 标准库函数产生的字符串;
5. 从文件中读取的字符串。
有几点需要额外说明:
1. lz不会分析标准库函数产生的字符串,而是以printf系列函数作用在输出流上的效果代替;
2. 不要把“文件”狭隘地理解成硬盘上的文件,而是应该把一切可以当做文件进行操作的对象都视为文件;
3. 外部库函数产生的字符串不在考虑范围内,因为外部库自有其文档。
字符数组是字符串的容器,在开始分析字符串之前,我们需要先了解一下C语言中与字符有关的各种类型和常量。


2025-07-26 21:22:40
广告
不感兴趣
开通SVIP免广告
  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第一章第二节 类型和常量
C语言中有7种与字符有关的类型和两种end-of-file常量,在Windows上,它们
与系统默认代码页无关,
与控制台代码页无关,
与三个编译选项(-finput-charset, -fexec-charset, -fwide-exec-charset)无关,
与setlocale无关。


虽然EOF和WEOF不属于字符,但它们与scanf系列函数密切相关,故将其列举于此;同样地,虽然int和wint_t不属于字符类型,但它们是处理EOF和WEOF时所必需的类型,因此也认为它俩与字符有关。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第一章第三节 字符串字面量
C语言有五种字符串字面量,它们的编码
与系统默认代码页无关,
与控制台代码页无关,
与setlocale无关;
其中UTF-8, UTF-16, UTF-32字符串字面量还与三个编译选项无关,字符串字面量(狭义)只与编译选项-fexec-charset有关,wchar_t字符串字面量只与-fwide-exec-charset有关。

注:
1. -fexec-charset可以选择GBK, Big5, UTF-8等编码,但必须保证基本执行字符集的每一个字符都是单字节的,所以不能把它设为UTF-16;
2. -fwide-exec-charset可以选择任何包含基本执行字符集的编码,但由于Windows总是把wchar_t的编码视为UTF-16,因此只有选择UTF-16才是正确的。
在C23中,宽字符串字面量是UTF-16, UTF-32和wchar_t字符串字面量的统称,但lz在这里将错就错,把宽字符串字面量和wchar_t字符串字面量视为同义词,宽字符串和wchar_t字符串同理。


  • 木本
  • 毛蛋
    1
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
赶上直播了


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第一章第四节 程序参数(argv)
如果我们要通过程序参数接收一些文件名,就不能不考虑文件名中可能存在汉字这一问题。
程序参数的传递是一个复杂的过程,其中涉及到API调用,进程创建,C运行环境初始化等多个方面。然而令人惊奇的是,从结果上来讲,argv数组中字符串的编码又出乎意料的简单,它仅仅取决于系统默认代码页,与控制台代码页、三个编译选项和setlocale均无关。

如图,当我们在控制台窗口输入
prog 李白
并按下回车后,一连串的事件将随之发生:
首先,字符串"prog 李白"会以UTF-16编码存储在cmd.exe内部;
然后,cmd尝试寻找prog.exe;
找到prog.exe后,cmd调用CreateProcessW函数创建进程,刚才存储的宽字符串"prog 李白"将作为第二个参数传递给CreateProcessW函数;
接下来这个宽字符串将在内核中经过一段漫长的旅行,最终来到prog.exe的地址空间中;
prog.exe需要进行一系列的初始化操作以使得C运行环境可用。在调用main函数之前,prog.exe需要先调用msvcrt提供的__getmainargs函数获得程序参数,以此法得到的argv已按照系统默认代码页转换成了多字节字符串(这里是GBK编码);
最后,prog.exe把来自于__getmainargs的argc和argv传递给main函数,我们熟悉的main函数就从这里开始了。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第一章第五节 多字节/宽字符转换函数
这一节介绍四个多字节/宽字符转换函数:
mbrtowc:将一个多字节字符转换为宽字符;
wcrtomb:将一个宽字符转换为多字节字符;
mbsrtowcs:将多字节字符串转换成宽字符串;
wcsrtombs:将宽字符串转换成多字节字符串。
这些函数使用的多字节字符编码取决于setlocale,与控制台代码页和三个编译选项无关。
多字节/宽字符转换函数是很有用的,我们可以先把各种多字节字符串转换成宽字符串,这样在程序内部处理时会比较方便,等到输出结果的时候再转换成多字节字符串。
多字节/宽字符转换函数的效果如图所示:

setlocale和多字节字符串的编码相匹配,多字节/宽字符转换函数可以给出一致的结果。
如果setlocale和多字节字符串的编码不匹配,这些转换函数将得到糟糕的结果:

后续对这些“乱码”宽字符串的处理将变得毫无意义。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
第一章第六节 scanf, wscanf, fscanf, fwscanf
折腾了这么久,总算轮到这两个守关大BOSS了。天天见的scanf,你是否窥探过它的内心深处?
还是先来看个图吧:

注:
1. 这里虽然只展示了%s,但实际上对%c和%[也适用,包括其对应的宽字符转换说明;
2. 控制台窗口指我们的C程序建立的控制台窗口,而不是cmd.exe,尽管在默认情况下,C程序的控制台代码页会继承自cmd(如果通过cmd启动);
3. 在这一步转换中,不存在于控制台代码页中的字符将被转换成问号(?);
4. 这是msvcrt内部的空间;
5. 这一步通常不发生转换,常见的例外是CR LF(0D 0A)被转换为LF(0A);
6. 这里省略字符串末尾的空终止符('\0');
7. C标准使用%ls匹配宽字符串,而msvcrt使用%S,与C标准不一致;
8. 这一步实际上通过调用msvcrt的_mbtowc_l函数完成。
scanf首先根据控制台代码页将输入的字符转换成对应的编码,如果转换说明是%s,则不加修改地将结果复制到调用者提供的字符数组中;如果转换说明是%S,则根据setlocale设置的多字节字符编码将结果转换为UTF-16并存储到调用者提供的字符数组中。


2025-07-26 21:16:40
广告
不感兴趣
开通SVIP免广告
  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
刚才我们只是看到了scanf的表面,那么scanf的里面到底是什么样的呢?

scanf函数在内部调用_filbuf函数,_filbuf调用_read,_read调用ReadFile,ReadFile在内部使用6号系统调用从控制台窗口读取数据,随后一路返回至scanf内部某处,如果转换说明是%s,则继续返回;如果转换说明是%S,则调用ungetwc,ungetwc在内部调用_mbtowc_l将多字节字符转换为宽字符,之后一路返回,scanf的流程结束。
这里有两个值得注意的地方:
1. syscall从控制台窗口读取数据时,字符已经按照控制台代码页完成了转换,因此syscall得到的是多字节字符;
2. 从ReadFile返回到_read之后,_read函数会把缓冲区中的CR LF调整为LF。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
如果说scanf是直来直去的话,wscanf的路线真可以用诡异妖娆来形容。

注:
1. 在msvcrt中,wscanf使用%S匹配多字节字符串,%s匹配宽字符串。嗯,怎么说呢,微软总是能有一些让人摸不着头脑的奇思妙想。
wscanf的第一步和scanf是一样的,从控制台窗口读取字符,并按照控制台代码页进行转换,之后事情就开始变得魔幻起来了;是的,我没有写错,你更没有看错,如果转换说明是%S,wscanf会先根据locale进行一次从多字节字符到宽字符的转换,再进行一次从宽字符到多字节字符的转换。拜服,拜服!微软之大才实非吾等凡人所能及也。如果转换说明是%s,就比较简单,只需根据locale把多字节字符转换成宽字符就完事了。
好吧,前面只是吐槽一下微软,其实这种看似多此一举的转换还是有点作用的,而且也符合C标准的描述。简单来说,这两步转换可以把某些字符变成问号(?),因为setlocale设置的是多字节字符编码,而大多数多字节字符编码只能表示它们范围内的一部分字符,所以如果转换前的某些字符超出了这个范围,这些字符最终会变成问号(编码是3F)。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
见识过wscanf那诡异妖娆的表面之后,咱们来看一下它那错综复杂的内部:

前半截跟scanf差不多,都是利用6号系统调用从控制台窗口读取数据,之后无论是%S还是%s都会调用mbtowc把多字节字符转换成宽字符,如果转换说明是%s,到这里就结束了;如果是%S,还要再调用wctomb_s把宽字符转换回多字节字符。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
理解了scanf和wscanf之后,fscanf和fwscanf就简单多了。

注:
1. 应该把文本文件当做二进制数据流来看待,而不是当成文本来看待;
2. 这一步会把文件中的数据原封不动地复制到msvcrt内部的缓冲区当中,如果文件是以文本模式打开的(这正是我们所讨论的情况),对CR LF的转换将在后续的步骤中发生,这一点已经在24L和25L提到过了。
这幅图就不过多解释了,反正跟scanf和wscanf差不多。
内部细节也跟scanf和wscanf差不多,无需赘述。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
需要注意的是,以上描述的scanf, wscanf, fscanf和fwscanf的行为是指它们在原本用途下所表现出来的行为,也就是说,scanf和wscanf确实是从控制台窗口中接受输入,fscanf和fwscanf确实是从文件中读取数据。如果把标准输入重定向到文件,那么scanf会表现出fscanf的行为;如果fscanf的首参数是stdin,fscanf将表现出scanf的行为。以下同理。
总结一下:scanf, wscanf, fscanf和fwscanf不(直接)受系统默认代码页影响,与三个编译选项无关;scanf和wscanf受控制台代码页影响,而fscanf和fwscanf不受影响;这四个函数的行为都与setlocale有关(因为它们在内部调用了多字节/宽字符转换函数)。
因为scanf和wscanf同时受到控制台代码页和setlocale的影响,所以应该保持setlocale和控制台代码页一致,这样才能在最大程度上保证scanf和wscanf给出正确的结果。fscanf和fwscanf要简单一些,只要保证setlocale设置的多字节字符编码和文本文件的编码相同即可。


  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
到这里scanf系列函数就算是讲完了,虽然这些函数咋一看很吓人,但是归根到底,只要处理好控制台代码页和setlocale,一切就都不叫个事儿。
嗯,漫长的第六节终于结束了……吗?
细心的读者应该会发现,无论是fscanf还是fwscanf,第一步读取进来的数据都是多字节编码的,之后无论是%s还是%S,都是把这些数据当作多字节字符来解读,并根据需要把它们转换成宽字符,所以事实上,不管文本文件的编码是什么,fscanf和fwscanf都会把它们的内容当成多字节字符来对待。
欸,不对啊,这么说的话,如果文本文件的编码本来就是UTF-16,岂不是没有合适的转换说明可用了吗?嘿,你说对了,还真没有。
爱动脑筋的小伙伴可能会说,硬要用fscanf也不是没有办法,我们可以fscanf(stream, "%s", mbs);(mbs是char数组),因为%s基本不会对读取到的文件数据进行变换,所以这么做能最大程度地保证数组mbs得到的是原汁原味的UTF-16数据。
对此,我只能说,理想很丰满,现实很骨感。且不说UTF-16的家应该是wchar_t,用char数组来存储UTF-16本身就看着很奇怪,何况这还只是个不痛不痒的小问题,真正的大问题还在后面。


2025-07-26 21:10:40
广告
不感兴趣
开通SVIP免广告
  • XeO2
  • 小吧主
    14
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
有些机智的小伙伴估计忍不住要说了:哎,fscanf不是变参数函数嘛?那我直接fscanf(stream, "%s", wcs);(wcs是wchar_t数组)不就行了嘛,悄悄滴进村,打枪滴不要,讲究的就是一手瞒天过海;编译器报warning?不要怕,C标准说啦,指向对象类型的指针可以转换到char*哒,等我加上一个显式转换:fscanf(stream, "%s", (char*)wcs);,编译器不就哑口无言了吗?


看,这不是堪称完美么?
呵,naive,我承认阁下的fscanf很强,但如果我请出王昌龄,阁下又该如何应对?


不可能,绝对不可能!如今%s虽已被斩于马下,但我的手中,还有一张王牌!%[^\n],请助我一臂之力!


呵,这就是你的绝招么?陶渊明直接让你这破程序彻底哑火:



登录百度账号

扫二维码下载贴吧客户端

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