字符编码简介
???????
目錄
一、ascii編碼
二、擴(kuò)展ascii編碼
三、多字節(jié)編碼(multi bytes)
四、寬字符編碼(wide char)
五、unicode編碼
六、utf-8編碼
七、結(jié)語
?????????大家好,我是略游。本文的目的是講清楚,字符編碼的今生來世。看完后你會(huì)對(duì)字符編碼的規(guī)則有一個(gè)宏觀印象。
一、ascii編碼
????????字符(character)是計(jì)算機(jī)與人交互的媒介,人雖然可以看懂二進(jìn)制串,但文字是更加直觀的。所以需要用數(shù)字來表示字符,字符與數(shù)字的對(duì)應(yīng)關(guān)系就叫編碼(coding)。
????????由于計(jì)算機(jī)發(fā)源于美國,一開始不需要顯示其他語言的文字,所以就挑選了常用的128個(gè)字符,形成了ASCII碼表。其中有一些特殊的字符是不能顯示的,例如換行、空、水平制表符等。如下圖所示:
ascii碼表? ? ? ?上圖是我打印的ascii編碼,其中索引32的字符為空格,之前的稱為控制字符。但是我們使用isprint函數(shù)來判斷一個(gè)字符是否“可打印的”,就會(huì)發(fā)現(xiàn)水平制表符(\t)其實(shí)是返回的非0。? ? ? ?
???????? 前輩程序員使用1個(gè)字節(jié)表示字符,8位二進(jìn)制一共可表示256個(gè)不同的值,但這里只用到了前面的128個(gè)位置。在C++里面char是帶有符號(hào)的,也就是可以是負(fù)數(shù),范圍為[-128, 127],這樣看起來只用[0, 127] ,似乎有點(diǎn)道理(才怪)。
二、擴(kuò)展ascii編碼
? ? ? ? 128個(gè)值當(dāng)然很快就會(huì)不夠用了,IBM于是制訂了擴(kuò)展ascii編碼,也就是用上另外的128個(gè)值。當(dāng)然這并非標(biāo)準(zhǔn),到現(xiàn)在已經(jīng)很少有人使用了,或許在嵌入式單片機(jī)等內(nèi)存緊張的地方還有人使用。
三、多字節(jié)編碼(multi bytes)
? ? ? ?如果要包含中文、日文、韓文、俄文、希臘字母、阿拉伯文和數(shù)學(xué)符號(hào)等字符,明顯256個(gè)值是不夠的(65536個(gè)值也夠嗆),并且為了兼容ascii碼表(已有的定義不能修改)。所以又有聰明人發(fā)明了雙字節(jié)編碼。
????????多字節(jié)編碼大多情況都指雙字節(jié)編碼,但是這個(gè)名字并不準(zhǔn)確,因?yàn)楹竺嫠v的utf-8編碼按原理來說也是多字節(jié)編碼,但人們說多字節(jié)字符串的時(shí)候,往往是指雙字節(jié)編碼的字符串。
? ? ? ? 所謂雙字節(jié)編碼,就是用1個(gè)或2個(gè)char來表示1個(gè)字符。當(dāng)用2個(gè)char時(shí),可表示的數(shù)量就平方了,也就是65536。但實(shí)際并非如此,因?yàn)樗枰嫒輆scii碼表。
????????當(dāng)程序讀取到1個(gè)char時(shí),需要先判斷它是否小于等于127,如果小于,則說明它是ascii碼中的字符,它自己1個(gè)就代表了1個(gè)字符。如果它的值大于127,則說明它后面的一個(gè)char與自己組合起來代表一個(gè)字符,所以叫雙字節(jié)編碼。
? ? ? ? 首先我在vs里輸入了一段utf-8編碼的字符串(在項(xiàng)目命令行里定義/utf-8,可以使字符串字面量為utf-8編碼),可以看到“a中文b”一共占7個(gè)char。
utf-8編碼? ? ? ? 隨后我轉(zhuǎn)為了雙字節(jié)編碼,可以看到變成了占用6個(gè)char。因?yàn)槊總€(gè)漢字占2個(gè)char,并且它的第一個(gè)char一定為大于127(char類型溢出到負(fù)數(shù)),如下圖所示:
雙字節(jié)編碼? ? ? ? ?可以看到,這時(shí)編譯器可以正確識(shí)別字符串內(nèi)的內(nèi)容。因?yàn)樗J(rèn)string的內(nèi)容編碼是雙字節(jié)編碼。如果要在斷點(diǎn)中正確顯示utf-8編碼,可以使用u8string類型,它基于C++20的char8_t類型。不過到目前為止,需要多次點(diǎn)開才能看到字符串的實(shí)際內(nèi)容。
? ? ? ? 回到雙字節(jié)編碼,第一個(gè)char可表示范圍數(shù)量是128,而第二個(gè)是256,所以雙字節(jié)編碼所能表示的字符數(shù)量為128 + 128 * 256,為32896。
????????雙字節(jié)編碼也可稱為本地編碼,它有各種不同的規(guī)格,比如內(nèi)地用的gb2312標(biāo)準(zhǔn),臺(tái)灣用的big5(大五碼)標(biāo)準(zhǔn)。Windows系統(tǒng)在不同的國家和地區(qū)便是使用的不同的本地編碼,使用記事本和excel導(dǎo)出的文件默認(rèn)情況下都是雙字節(jié)編碼,當(dāng)使用另一臺(tái)非相同本地編碼的計(jì)算機(jī)打開文件時(shí)就會(huì)出現(xiàn)亂碼。使用notepad++可以看到有如下許多不同的本地編碼:
各式不同的雙字節(jié)編碼? ? ? ? ?在vs里面,通過高級(jí)保存選項(xiàng)也可以設(shè)定代碼文件使用的編碼,此處并不影響編譯,只是設(shè)定文件保存時(shí)存儲(chǔ)數(shù)據(jù)的方式。可以看到每個(gè)編碼都擁有代碼頁的定義值。其中中文(gb2312)是936,utf-8是65001(它不屬于本地編碼,但也有代碼頁),還有許多其他國家地區(qū)使用的本地編碼。
vs里的代碼頁? ? ? ? ?通過以上可以清楚的看到雙字節(jié)編碼的缺點(diǎn),依賴它所編成的程序在換一個(gè)環(huán)境時(shí)就會(huì)出現(xiàn)亂碼。如果通過指定代碼頁來保證正確,那么這件事也會(huì)變得維護(hù)艱難。
? ? ? ? 除此之外它還有一個(gè)嚴(yán)重的缺點(diǎn),就是漢字被分為2個(gè)char,按語義來說“中文”二字的長度為2,但是實(shí)際它占4個(gè)char,在使用strlen之類的函數(shù)時(shí),其含義發(fā)生了變化。例如在編寫輸入框控件時(shí),就需要額外的判斷來防止?jié)h字被拆開。當(dāng)然utf-8編碼也有此缺點(diǎn)。
四、寬字符編碼(wide char)
? ? ? ? 意識(shí)到以上問題后,為何不干脆直接用2個(gè)字節(jié)的類型表示字符呢?一共可表示65536個(gè)字符呢。于是出現(xiàn)了寬字符編碼,也就是char變?yōu)榱?strong>wchar_t。但是wchar_t的大小卻沒有規(guī)定,在windows系列編譯器里它是2個(gè)字節(jié),但在linux系列編譯器里它是4個(gè)字節(jié)。這個(gè)缺點(diǎn)導(dǎo)致代碼在跨平臺(tái)時(shí)需要額外處理。
? ? ? ? windows平臺(tái)為了維護(hù)自己的正義性,一般稱寬字符編碼為unicode編碼。這個(gè)說法沒有錯(cuò)誤,但是不夠嚴(yán)格。
? ? ? ? 在vs編譯器里面,我們可以設(shè)置項(xiàng)目的字符集。其中“使用unicode字符集”指的便是基于wchar_t類型的寬字符編碼。
vs設(shè)置字符集? ? ? ? ?微軟將大部分的函數(shù)都寫了兩個(gè)版本,一個(gè)為A,一個(gè)為W。其中這里對(duì)應(yīng)著char和wchar_t。也對(duì)應(yīng)著雙字節(jié)編碼和寬字符編碼。也可以叫多字節(jié)編碼和unicode編碼。在標(biāo)準(zhǔn)庫中對(duì)應(yīng)的是string和wstring。針對(duì)char的函數(shù)大部分都又以wchar_t版本實(shí)現(xiàn)了一遍,例如wcscmp與wcslen。
兼容雙字節(jié)的寬字符寫法? ? ? ? ?在代碼中我們可以這樣定義一個(gè)寬字符字符串:
寬字符編碼? ? ? ? ?就目前來說,許多舊工程使用的是雙字節(jié)編碼,部分新工程使用的是寬字符編碼,然而還有少許人使用utf-8編碼。先下一個(gè)結(jié)論,使用utf-8編碼是時(shí)代潮流。
? ? ? ? 寬字符編碼有個(gè)嚴(yán)重的缺點(diǎn)就是它沒有定義大小,并且windows平臺(tái)的實(shí)現(xiàn)是2字節(jié)的大小,對(duì)于漢語來說,是根本放不下的,比如《康熙字典》就收錄了4萬7千余漢字,而1994年的《中華字海》便超過了9萬字。外國的程序員們?cè)谒伎歼@個(gè)問題的時(shí)候,便出現(xiàn)了不一致的想法。65536絕已囊括大部分的文字,但是又放不下一些比較少用的文字。
五、unicode編碼
? ? ? ? 所謂unicode編碼就是每個(gè)字都用同樣大小的類型保存,目前有基于2字節(jié)的UCS-2標(biāo)準(zhǔn),和基于4字節(jié)的UCS-4標(biāo)準(zhǔn)。其中UCS-4是兼容UCS-2的,這意味著UCS-2轉(zhuǎn)為UCS-4后所有高位為0。
? ? ? ? 其中UCS-2對(duì)應(yīng)2字節(jié)的wchar_t,UCS-4對(duì)應(yīng)4字節(jié)的wchar_t。但目前用得更多的還是UCS-2,但這并不代表它就應(yīng)該被用,這有歷史因素在里面。個(gè)人認(rèn)為UCS-4優(yōu)先于UCS-2,因?yàn)樗鼈兊娜秉c(diǎn)都是浪費(fèi)空間,但UCS-4的優(yōu)點(diǎn)更明顯,它能表示的范圍又平方了,40多億的范圍。
六、utf-8編碼
? ? ? ? ?用4個(gè)字節(jié)來表示文字,在處理時(shí)不會(huì)被拆開,一個(gè)字就是一個(gè)此類型。但是在表示英文和常用符號(hào)時(shí)卻浪費(fèi)了3倍的空間。于是聰明的程序員發(fā)明了utf-8編碼,用它來壓縮UCS-4編碼,根據(jù)字符的值來占據(jù)不同的字節(jié)個(gè)數(shù),所以它也可以視為“多字節(jié)編碼”,叫變長字節(jié)編碼更合適。
? ? ? ? 它是以char類型作為基礎(chǔ),這樣在兼容ascii碼時(shí),就不需要進(jìn)行截?cái)唷J紫萚0, 127]與ascii編碼相同,當(dāng)大于127時(shí),它的最高位一定是1。
| 二進(jìn)制 | 十進(jìn)制 |
| 01111111 | 127 |
| 1000000 | 128 |
| 11111111 | 255 |
? ? ? ? 所以[0, 127]的值對(duì)應(yīng)[00000000, 01111111],這與ascii碼表一致,如果用x來替代可變位,則是0xxxxxxx。
? ? ? ? 當(dāng)最高位為1時(shí),如果我們用1xxxxxxx來表示另一堆字符,那么又可表示128個(gè)字符。所以一共可以表示256個(gè)字符……
? ? ? ? 很明顯,1個(gè)char辦不到更多的表示了,所以我們后面增加1個(gè)char。但是我們?cè)趺粗篮竺娴腸har是和前面一起的呢?所以我們約定最高位為1時(shí),后面額外有個(gè)char。即如下兩種情況:
| 二進(jìn)制范圍 | 十進(jìn)制范圍值 |
| 00000000 - 01111111 | 128 |
| 10000000 00000000 - 11111111 11111111 | 32768 |
? ? ? ? 可以看到雙字節(jié)編碼最多可以表示32896個(gè)字符,這與前面的計(jì)算結(jié)果一致。本身2字節(jié)可以表示的范圍值為65536,這就是壓縮數(shù)據(jù)帶來的范圍損耗。如果我們用x來表示可變位,那么根據(jù)x的數(shù)量計(jì)算2^x,則能更清晰的看到范圍值:
0xxxxxxx 2^7
1xxxxxxx xxxxxxxx 2^15
? ? ? ? 這時(shí)我們會(huì)發(fā)現(xiàn)一個(gè)問題,此時(shí)無法表示后面擁有更多的char。因?yàn)榈诙呶皇莤,它的數(shù)據(jù)是變化的,無法作為控制位。假設(shè)我們以2位來表示后面所接的char數(shù)量:
00xxxxxx 后面0個(gè)char
01xxxxxx 后面1個(gè)char
10xxxxxx 后面2個(gè)char
11xxxxxx 后面3個(gè)char
? ? ? ? 很明顯,01xxxxxx會(huì)不兼容ascii編碼,所以我們總結(jié)出:“在有后接char時(shí),最高位必須為1”。所以我們?cè)俅涡薷亩x:
0xxxxxxx 后面0個(gè)char
100xxxxx?后面1個(gè)char
101xxxxx?后面2個(gè)char
110xxxxx?后面3個(gè)char
111xxxxx?后面4個(gè)char
? ? ? ? 根據(jù)x的數(shù)量,我們得出各自的表示范圍值:
| 首字節(jié)二進(jìn)制 | x數(shù)量 | 值 | 值 |
| 0xxxxxxx | 7 | 2^7 | 128 |
| 100xxxxx | 5+8 | 2^13 | 8192 |
| 101xxxxx | 5+16 | 2^21 | 2097152 |
| 110xxxxx | 5+24 | 2^29 | ... |
| 111xxxxx | 5+32 | 2^37 | ... |
? ? ? ? 通過這個(gè)方法,3個(gè)char可表示完UCS-2,而5個(gè)char才能表示完UCS-4。當(dāng)字符的unicode編碼小于128時(shí),只用1個(gè)char。小于8192+128時(shí)使用2個(gè)char。在約200萬的范圍內(nèi)只需要3個(gè)char。但要完全涵蓋UCS-4,則必須是5個(gè)char。
? ? ? ? 但以上只是我們實(shí)驗(yàn)的方法,是數(shù)值范圍最大的表示情況,實(shí)際上utf-8擁有糾錯(cuò)碼,所有的后繼char都以10開頭(10xxxxxx)。它建立在一個(gè)斷言之上:“字符的首個(gè)char必定不是10開頭”。如下所示:
| 首字節(jié)二進(jìn)制 | x數(shù)量 | 值 | 值 |
| 0xxxxxxx | 7 | 2^7 | 128 |
| 110xxxxx 10xxxxxx | 5+6 | 2^11 | 2048 |
| 1110xxxx?10xxxxxx(x2) | 4+12 | 2^16 | 65536 |
| 11110xxx?10xxxxxx(x3) | 3+18 | 2^21 | 2097152 |
| 111110xx?10xxxxxx(x4) | 2+24 | 2^26 | ... |
| 1111110x?10xxxxxx(x5) | 1+32 | 2^33 |
? ? ? ? ?所以u(píng)tf-8編碼用2個(gè)char可以表示2048+128個(gè)值,3個(gè)char能夠表示完UCS-2。要表示完整的UCS-4需要6個(gè)char。至于有些文章說utf-8編碼的變長范圍為[1, 4]是不正確的,由于4個(gè)char能表示約200萬的字符,在當(dāng)前看來是正確的。
? ? ? ? 在實(shí)際使用時(shí),我們還會(huì)發(fā)現(xiàn)utf-8編碼有帶BOM和不帶BOM的。所謂帶BOM就是在文件頭部多寫入幾個(gè)字節(jié)來表示自己是utf-8文件,并且可以標(biāo)明自身的字節(jié)序。有標(biāo)準(zhǔn)推薦使用帶BOM的utf-8,而微軟也是這么操作的。但是我認(rèn)為BOM屬于文件的元數(shù)據(jù)(meta),一堆數(shù)據(jù)在那兒,它是什么取決于程序怎么解讀,而一堆數(shù)據(jù)的一部分內(nèi)容描述自己是什么,在一定程度上是多此一舉。
七、結(jié)語
? ? ? ? 個(gè)人認(rèn)為項(xiàng)目應(yīng)該統(tǒng)一使用utf-8無BOM編碼,在做文本編輯等邏輯時(shí)轉(zhuǎn)換到utf-32編碼,以進(jìn)行字符串拆分、排版和顯示等操作。
? ? ? ? 雖然C++20提出了char8_t類型來專門表示utf-8編碼,但我們還是可以用char數(shù)組和string來表示utf-8字符串,因?yàn)榫幾g器依舊支持字符串字面量直接為utf-8編碼。在使用std::filesystem時(shí),需要強(qiáng)轉(zhuǎn)指針為u8string,這樣標(biāo)準(zhǔn)庫才知道你傳入的是utf-8字符串,如下所示:
?????????
? ? ? ? 如果你覺得此文章寫得不錯(cuò),可以點(diǎn)擊收藏,然后點(diǎn)擊關(guān)注,這可以極大的支持我發(fā)更多的文章。
? ? ? ? 你還可以加我的QQ群討論:游戲編程星云閣 170100866
總結(jié)
- 上一篇: 【DirectX12】1.基本组件创建和
- 下一篇: 【C++】Visual studio样式