前些日子漫无目的地刷着朋友圈,突然一个ID从字丛中闯入我的眼睛——"某&字"(为保护当事人隐私,此处用'某''字'代替),浸淫于计算机而产生的直觉告诉我,这是一个有值的表达式,这位姑娘用这个表达式当ID,那她这ID的值,到底是啥呢?

一、计算机存储汉字的方法——汉字编码

话说在计算机中,姑娘们的照片和她们的ID本质上都一样,都是冷冰冰的二进制0和1。既然都是一个bit,那就有了位操作求值的——咳咳——"科学依据"。

与和英文打字键盘完全兼容的拉丁字母不同,输入汉字便成了人们必须研究的课题。从近40年前的GB2312,到扩展后的GBK标准,再到全球化的Unicode,后来又出现了Unicode的Plus版本——UTF标准。

好了,说完汉字编码的历史,我们回到计算机中。当初推出GB2312时,为了不和原有的ascii码混肴,智慧的中国人规定,当两个值都大于127的字节连在一起的时候,就表示一个汉字,前面的字节称为高字节(0xA1-0xf7),后面的字节称为低字节(0xA1-0xFE)。但后来人们发现GB2312里的汉字不够用,于是干脆降低要求,不在规定低字节一定得是大于127的值,这样,拓展的GBK诞生了,再之后又经拓展,就成了人们口中的"DBCS"(Double Byte Charecter Set 双字节字符集)。在这个标准中,程序员必须注意每一个字节的值,如果大于127,那么就有一个汉字要出现了。

而目前使用最广的UTF-8作为一种变长的编码方式,可根据不同的符号变化字节长度(1-4个字节)。在GB2312中,一个汉字为两个字节,而在UTF-8中,一个汉字为3个字节。

二、简单版本

百度得到dev-cpp的字符编码为GB-2312(此处大雾,后文会详细解释),所以一个汉字占两个字符。

因为涉及位操作,所以新建一个union结构nameBit,一个nameBit占2*16位,也就是4个字节。

  1.     union nameBit{  
  2.     short short1[2];  
  3.     struct {  
  4.         char c0;  
  5.         char c1;  
  6.         char c2;  
  7.         char c3;  
  8.     }byte;  
  9. }; 

    这个结构存储了两个汉字的GB-2312编码,分别用一个short表示,byte和short共享内存,所以c0,c1用于存储第一个汉字,c2,c3存储第二个汉字的字符编码。

  10. /*
  11.      初级版本---
  12. */
  13. #include <stdio.h>  
  14.     
  15. union nameBit{  
  16.     short short1[2];  
  17.     struct {  
  18.         char c0;  
  19.         char c1;  
  20.         char c2;  
  21.         char c3;  
  22.     }byte;  
  23. };  
  24.     
  25. int main (void)  
  26. {  
  27.     union nameBit value1;  
  28.     char arr[5];  
  29.         
  30.     printf("请输入两个汉字,中间无需空格隔开:\n"); 
  31.    fgets(arr,5,stdin);
  32.         
  33.     for(int i = 0;i<4;i++)  
  34.     {  
  35.         char *p = &value1.byte.c0 + i;  
  36.         *p = arr[i];  
  37.     }  
  38.     printf("%c%c & %c%c = ",value1.byte.c0,value1.byte.c1,value1.byte.c2,value1.byte.c3);  
  39.     value1.short1[0] = value1.short1[0] & value1.short1[1];  
  40.     printf("%c%c",value1.byte.c0,value1.byte.c1);  
  41.     
  42.     return 0;  
  43. }  

    把输入存到一个字符串中,在一字节一字节的复制到union。

输入示例"啊哈":

'啊'的GB2312编码为B0A1,'哈'的GB2312编码为B9FE,所以B0A1 & B9FE的结果为B0A0,查GB2312编码表发现这个编码并没有值,而拓展的GBK编码中B0A0对应为'盃'(同'杯')。

所以可知,该程序运行时遵循的编码方式为GBK。

三、移植到Linux后出现的种种问题

将该.c文件直接移至虚拟机的共享文件夹后,在Linux中运行此程序,出现如下情况:

将终端的字符编码改为GBK后,再次执行。

文件内字符正常显示,但是终端上的汉字出现乱码。

怀疑是源代码文件和终端上的编码不同,用file命令查看.c编码

再用strace命令查看细节,发现

其中,/345/225/212转换成16进制就是 E5 95 8A,/345/223/210就是E5 93 88,这正是"啊哈"这两个字的UTF-8编码。所以情况就很明显了,源代码用的dev-cpp的编辑器,保存为ANSI(GB2312),将文件移至Linux,编译运行后,终端和程序的编码不同,导致输出出现乱码,程序运行时,用的又是UTF-8编码一个汉字占三个字节

所以这个程序需要重构。

四、更新的版本

因为在Windows和Linux中运行时所用的编码不同,所以直接用一个10个字节的数组来存储编码。

又由于需要在两个平台运行,所以需要有一定的移植性。这个可以用__linux__和_WIN32两个宏来区分。

  1. #if _WIN32  
  2.     //windows  
  3.     #elif __linux__  
  4.     //Linux  
  5.     #else  
  6.     //other  
  7.     #endif 

    最终版本如下:

  8. #include <stdio.h>  
  9. #define Win_N 2  
  10. #define Linux_N 3  
  11.     
  12. int main (void)  
  13. {  
  14.     char arr[10];  
  15.         
  16.     printf("Please input two Chinese charecters:");  
  17.     printf("(请输入两个汉字)\n");  
  18.         
  19.     fgets(arr,10,stdin);  
  20.         
  21.     #if _WIN32  
  22.         
  23.     printf("%c%c & %c%c = ",arr[0],arr[1],arr[2],arr[3]);  
  24.     for(int i = 0;i<Win_N;i++)  
  25.         arr[i] = arr[i]&arr[i+Win_N];  
  26.     printf("%c%c\n",arr[0],arr[1]);  
  27.         
  28.     #elif __linux__  
  29.         
  30.     printf("%c%c%c & %c%c%c = ",arr[0],arr[1],arr[2],arr[3],arr[4],arr[5]);  
  31.     for(int i = 0;i<Linux_N;i++)  
  32.         arr[i] = arr[i]&arr[i+Linux_N];  
  33.     printf("%c%c%c\n",arr[0],arr[1],arr[2]);  
  34.         
  35.     #else  
  36.         
  37.     printf("Errors that occur when selecting an operating system\n");  
  38.         
  39.     #endif  
  40.         
  41.     return 0;  
  42. }

    为了防止第十行的汉字在Linux中出现乱码,把该.c文件用Notepad++打开,转为UTF-8格式。

之后再在Linux中运行,先测试一下。

'啊'的UTF-8编码为E5 95 8A ,'哈'为E5 93 88 ,E5 95 8A & E5 93 88 = E591 88, 正好是'呈'的UTF-8编码。

五、乱码的根源(注①)

1、源码字符集,执行字符集与运行环境编码

源码字符集:英文the source character set,是指源代码文件是使用何种编码字符集保存的。

执行字符集:英文the execution character set,是指源代码经过编译、链接后的可执行文件是使用何种编码字符集保存的,程序实际执行时,内存中的字符串编码就是执行字符集。

运行环境编码:是指操作系统(或者当前控制台环境)用于显示文字的编码字符集。

2、乱码的根源

源代码文件(源码字符集)经过编译/链接,生成可执行文件(执行字符集),最后程序运行于实际环境中(运行环境编码)。

在这过程中如果有字符集不匹配,最终就无法显示预期的文字信息,甚至产生乱码。

编译器在编译源代码时,会将源码字符集转化为执行字符集,如果编译器不能正确识别源码字符集,就得不到正确的字符串数据。

可执行文件在实际运行环境中执行时,为了在控制台(或者其他UI)上显示出字符串,就要将执行字符集转化为运行环境的字符集。如果运行环境的字符集与执行字符集不同,也会导致乱码。

3、编译器层面的字符转码工作

GCC:GCC的源码字符集与执行字符集默认都是UTF-8编码,也就是说默认情况下GCC都是按UTF-8来解析源码,编译后的执行字符集也是UTF-8。

、识别源码字符集:源码文件有BOM签名的,就按BOM的编码来解析源文件;否则使用本地Locale字符集解析源文件(随系统设置而变)。

2、转化执行字符集:对于char类型,如果有设置预处理选项"#pragma execution_character_set",编译源码时,转换为预编译所设定的执行字符集;

否则使用本地Locale作为执行字符集。对于wchar_t类型,总是使用UTF-16编码。

六、结语及声明

回到开头的那个话题,朋友圈的那个表达式ID在我电脑的两个系统中有两个值,分别对应于GB2312及UTF-8下的编码,所以输入姑娘的ID,得到了两个截然不同的值(笑)。

这个结果仅供娱乐,没有任何意义,这只是偶尔的心血来潮,庸常生活里的一点趣味。

在即将结束这篇博文的这个时候,浏览器里仍然还有十几个与字符编码有关的网页,我也仅仅是掌握了一些皮毛,却已获益匪浅。拙作初成,还请多多指教。

声明:

1、引用,即"注①"

内容来源于博文浅谈C/C++编程中的字符编码转换——许振坪

2、除上文引用外,一些有干货的文章

            字符集编码与 C/C++ 源文件字符编译乱弹——Breaker

汉字编码的发展历史

从调用printf()到显示器上看到字符串

从有值的ID到汉字编码的更多相关文章

  1. Jmeter 通过json Extracted 来获取 指定的值的id

    在没有 精确或模糊查询的接口时可以使用jmeter 获取指定的值的ID import java.lang.String ; String getTargetName="iphone632g& ...

  2. name值与id值在Js获取元素时的区别

    1.适用范围 除base.head.html.script.meta.title标签外,id都可以用:name只适用于select.form.frame.iframe.img.a.input等中. H ...

  3. Mysql 查询表中某字段的重复值,删除重复值保留id最小的数据

    1 查询重复值 ); 2 删除重复值 -- 创建临时表 ) ); -- 把重复数据放进临时表 INSERT Hb_Student_a SELECT id,studentNumber FROM Hb_S ...

  4. 【JS】js获得下拉列表选中项的值和id

    function tijiao(){ var elem = document.getElementById("dish_sort"); var index=elem.selecte ...

  5. ID属性值为小数

    获取带有.的id值 <h1 id="123.45">dom对象</h1> <script> $('#123\\.45').attr('id') ...

  6. HTML控件ID和NAME属性及在CS页面获得.ASPX页面中HTML控件的值

    <转载>来自网络 一.ID是在客户端脚本里用!NAME是用于获取提交表单的某表单域信息,在form里面,如果不指定Name的话,就不会发送到服务器端,所以有name属性的控件,必须指定na ...

  7. HTML控件ID和NAME属性的区别,以及如何在asp.net页面的.CS文件中获得.ASPX页面中HTML控件的值

    在html中:name指的是用户名称,ID指的是用户注册是系统自动分配给用户的一个序列号. name是用来提交数据的,提供给表单用,可以重复: id则针对文档操作时候用,不能重复.如:document ...

  8. SSM获取表单数据插入数据库并返回插入记录的ID值

    以下指示插入操作以及获取记录值的ID的部分操作代码!!! 首先是简单的表单实现 <%@ page language="java" contentType="text ...

  9. Vue 2.0 v-for 响应式key, index及item.id参数对v-bind:key值造成差异研究

    Vue 2.0 v-for 响应式key, index及item.id参数对v-bind:key值造成差异研究 在github上阅览README.md以获得最佳阅读体验,点这里 v-for响应式key ...

随机推荐

  1. N个数组中所有元素的排列组合(笛卡尔积)算法

    (1)N个数组对象中所有元素排列组合算法 private List<List<Object>> combineAlg(List<Object[]> nArray) ...

  2. 谈谈Ext JS的组件——组件基类:Ext.Component

    概述 Ext.Component是所有Ext组件的基类,这在Ext.Component的API中第一句话就提到了.然后第二段说明了它包含的基本功能:隐藏/显示.启用/禁用以及尺寸控制等.除了以上这些基 ...

  3. TCP的定时器系列 — SYNACK定时器

    主要内容:SYNACK定时器的实现,TCP_DEFER_ACCPET选项的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 在上一篇博客中,已经连带 ...

  4. JSP编译成Servlet(一)语法树的生成——语法解析

    一般来说,语句按一定规则进行推导后会形成一个语法树,这种树状结构有利于对语句结构层次的描述.同样Jasper对JSP语法解析后也会生成一棵树,这棵树各个节点包含了不同的信息,但对于JSP来说解析后的语 ...

  5. ABB机器人基础培训资料整理与总结

    之前对机械臂了解较少,这方面知识比较匮乏.只使用过PowercCube六自由度机械臂. 感谢ABB公司何老师的耐心指导. 学习资料汇总:(最重要的ABB Robot 官网就不列出了,这里以中文资料为主 ...

  6. kafka原理简介并且与RabbitMQ的选择

    kafka原理简介并且与RabbitMQ的选择 kafka原理简介,rabbitMQ介绍,大致说一下区别 Kafka是由LinkedIn开发的一个分布式的消息系统,使用Scala编写,它以可水平扩展和 ...

  7. cas 单点登录(SSO)实验之二: cas-client

    cas 单点登录(SSO)实验之二: cas-client 参考文章: http://my.oschina.net/indestiny/blog/200768#comments http://wenk ...

  8. 使用GDAL将下载的Google卫星图像转为带坐标的tif

    网上有很多下载Google地图的卫片的软件,一般下载下来的图像都是jpg格式的,另外附带一个坐标信息的描述文件.这样的数据不能直接拿来在遥感或者GIS软件中使用,因为图像里面没有投影和坐标信息,所以就 ...

  9. zTree的调用设使用(跨两个系统,两类技术实现的项目案例SpringMVC+Spring+MyBatis和Struts2+Spring+ibatis框架组合)

    1.从zTree官网上下载zTree的包,zTree的官方网址是:http://www.ztree.me/v3/main.php#_zTreeInfo 2.引入zTree所需的依赖,例如(jQuery ...

  10. mysql进阶(十五) mysql批量删除大量数据

    mysql批量删除大量数据 假设有一个表(syslogs)有1000万条记录,需要在业务不停止的情况下删除其中statusid=1的所有记录,差不多有600万条, 直接执行 DELETE FROM s ...