C++字符編碼

2023-09-20 09:23 更新

在計(jì)算機(jī)中,所有數(shù)據(jù)都是以二進(jìn)制數(shù)的形式存儲(chǔ)的,字符 char 也不例外。為了表示字符,我們需要建立一套“字符集”,規(guī)定每個(gè)字符和二進(jìn)制數(shù)之間的一一對(duì)應(yīng)關(guān)系。有了字符集之后,計(jì)算機(jī)就可以通過查表完成二進(jìn)制數(shù)到字符的轉(zhuǎn)換。

ASCII 字符集

「ASCII 碼」是最早出現(xiàn)的字符集,全稱為“美國(guó)標(biāo)準(zhǔn)信息交換代碼”。它使用 7 位二進(jìn)制數(shù)(即一個(gè)字節(jié)的低 7 位)表示一個(gè)字符,最多能夠表示 128 個(gè)不同的字符。如圖 3-6 所示,ASCII 碼包括英文字母的大小寫、數(shù)字 0 ~ 9、一些標(biāo)點(diǎn)符號(hào),以及一些控制字符(如換行符和制表符)。

ASCII 碼

圖 3-6   ASCII 碼

然而,ASCII 碼僅能夠表示英文。隨著計(jì)算機(jī)的全球化,誕生了一種能夠表示更多語言的字符集「EASCII」。它在 ASCII 的 7 位基礎(chǔ)上擴(kuò)展到 8 位,能夠表示 256 個(gè)不同的字符。

在世界范圍內(nèi),陸續(xù)出現(xiàn)了一批適用于不同地區(qū)的 EASCII 字符集。這些字符集的前 128 個(gè)字符統(tǒng)一為 ASCII 碼,后 128 個(gè)字符定義不同,以適應(yīng)不同語言的需求。

GBK 字符集

后來人們發(fā)現(xiàn),EASCII 碼仍然無法滿足許多語言的字符數(shù)量要求。比如漢字大約有近十萬個(gè),光日常使用的就有幾千個(gè)。中國(guó)國(guó)家標(biāo)準(zhǔn)總局于 1980 年發(fā)布了「GB2312」字符集,其收錄了 6763 個(gè)漢字,基本滿足了漢字的計(jì)算機(jī)處理需要。

然而,GB2312 無法處理部分的罕見字和繁體字?!窯BK」字符集是在 GB2312 的基礎(chǔ)上擴(kuò)展得到的,它共收錄了 21886 個(gè)漢字。在 GBK 的編碼方案中,ASCII 字符使用一個(gè)字節(jié)表示,漢字使用兩個(gè)字節(jié)表示。

Unicode 字符集

隨著計(jì)算機(jī)的蓬勃發(fā)展,字符集與編碼標(biāo)準(zhǔn)百花齊放,而這帶來了許多問題。一方面,這些字符集一般只定義了特定語言的字符,無法在多語言環(huán)境下正常工作。另一方面,同一種語言也存在多種字符集標(biāo)準(zhǔn),如果兩臺(tái)電腦安裝的是不同的編碼標(biāo)準(zhǔn),則在信息傳遞時(shí)就會(huì)出現(xiàn)亂碼。

那個(gè)時(shí)代的研究人員就在想:如果推出一個(gè)足夠完整的字符集,將世界范圍內(nèi)的所有語言和符號(hào)都收錄其中,不就可以解決跨語言環(huán)境和亂碼問題了嗎?在這種想法的驅(qū)動(dòng)下,一個(gè)大而全的字符集 Unicode 應(yīng)運(yùn)而生。

「Unicode」的全稱為“統(tǒng)一字符編碼”,理論上能容納一百多萬個(gè)字符。它致力于將全球范圍內(nèi)的字符納入到統(tǒng)一的字符集之中,提供一種通用的字符集來處理和顯示各種語言文字,減少因?yàn)榫幋a標(biāo)準(zhǔn)不同而產(chǎn)生的亂碼問題。

自 1991 年發(fā)布以來,Unicode 不斷擴(kuò)充新的語言與字符。截止 2022 年 9 月,Unicode 已經(jīng)包含 149186 個(gè)字符,包括各種語言的字符、符號(hào)、甚至是表情符號(hào)等。在龐大的 Unicode 字符集中,常用的字符占用 2 字節(jié),有些生僻的字符占 3 字節(jié)甚至 4 字節(jié)。

Unicode 是一種字符集標(biāo)準(zhǔn),本質(zhì)上是給每個(gè)字符分配一個(gè)編號(hào)(稱為“碼點(diǎn)”),但它并沒有規(guī)定在計(jì)算機(jī)中如何存儲(chǔ)這些字符碼點(diǎn)。我們不禁會(huì)問:當(dāng)多種長(zhǎng)度的 Unicode 碼點(diǎn)同時(shí)出現(xiàn)在同一個(gè)文本中時(shí),系統(tǒng)如何解析字符?例如給定一個(gè)長(zhǎng)度為 2 字節(jié)的編碼,系統(tǒng)如何確認(rèn)它是一個(gè) 2 字節(jié)的字符還是兩個(gè) 1 字節(jié)的字符?

對(duì)于以上問題,一種直接的解決方案是將所有字符存儲(chǔ)為等長(zhǎng)的編碼。如圖 3-7 所示,“Hello”中的每個(gè)字符占用 1 字節(jié),“算法”中的每個(gè)字符占用 2 字節(jié)。我們可以通過高位填 0 ,將“Hello 算法”中的所有字符都編碼為 2 字節(jié)長(zhǎng)度。這樣系統(tǒng)就可以每隔 2 字節(jié)解析一個(gè)字符,恢復(fù)出這個(gè)短語的內(nèi)容了。

Unicode 編碼示例

圖 3-7   Unicode 編碼示例

然而 ASCII 碼已經(jīng)向我們證明,編碼英文只需要 1 字節(jié)。若采用上述方案,英文文本占用空間的大小將會(huì)是 ASCII 編碼下大小的兩倍,非常浪費(fèi)內(nèi)存空間。因此,我們需要一種更加高效的 Unicode 編碼方法。

UTF-8 編碼

目前,UTF-8 已成為國(guó)際上使用最廣泛的 Unicode 編碼方法。它是一種可變長(zhǎng)的編碼,使用 1 到 4 個(gè)字節(jié)來表示一個(gè)字符,根據(jù)字符的復(fù)雜性而變。ASCII 字符只需要 1 個(gè)字節(jié),拉丁字母和希臘字母需要 2 個(gè)字節(jié),常用的中文字符需要 3 個(gè)字節(jié),其他的一些生僻字符需要 4 個(gè)字節(jié)。

UTF-8 的編碼規(guī)則并不復(fù)雜,分為以下兩種情況。

  • 對(duì)于長(zhǎng)度為 1 字節(jié)的字符,將最高位設(shè)置為 0、其余 7 位設(shè)置為 Unicode 碼點(diǎn)。值得注意的是,ASCII 字符在 Unicode 字符集中占據(jù)了前 128 個(gè)碼點(diǎn)。也就是說,UTF-8 編碼可以向下兼容 ASCII 碼。這意味著我們可以使用 UTF-8 來解析年代久遠(yuǎn)的 ASCII 碼文本。
  • 對(duì)于長(zhǎng)度為 n 字節(jié)的字符(其中 n>1),將首個(gè)字節(jié)的高 n 位都設(shè)置為 1、第 n+1 位設(shè)置為 0 ;從第二個(gè)字節(jié)開始,將每個(gè)字節(jié)的高 2 位都設(shè)置為 10 ;其余所有位用于填充字符的 Unicode 碼點(diǎn)。

圖 3-8 展示了“Hello算法”對(duì)應(yīng)的 UTF-8 編碼。觀察發(fā)現(xiàn),由于最高 n 位都被設(shè)置為 1 ,因此系統(tǒng)可以通過讀取最高位 1 的個(gè)數(shù)來解析出字符的長(zhǎng)度為 n 。

但為什么要將其余所有字節(jié)的高 2 位都設(shè)置為 10 呢?實(shí)際上,這個(gè) 10 能夠起到校驗(yàn)符的作用。假設(shè)系統(tǒng)從一個(gè)錯(cuò)誤的字節(jié)開始解析文本,字節(jié)頭部的 10 能夠幫助系統(tǒng)快速的判斷出異常。

之所以將 10 當(dāng)作校驗(yàn)符,是因?yàn)樵?UTF-8 編碼規(guī)則下,不可能有字符的最高兩位是 10 。這個(gè)結(jié)論可以用反證法來證明:假設(shè)一個(gè)字符的最高兩位是 10 ,說明該字符的長(zhǎng)度為 1 ,對(duì)應(yīng) ASCII 碼。而 ASCII 碼的最高位應(yīng)該是 0 ,與假設(shè)矛盾。

UTF-8 編碼示例

圖 3-8   UTF-8 編碼示例

除了 UTF-8 之外,常見的編碼方式還包括以下兩種。

  • UTF-16 編碼:使用 2 或 4 個(gè)字節(jié)來表示一個(gè)字符。所有的 ASCII 字符和常用的非英文字符,都用 2 個(gè)字節(jié)表示;少數(shù)字符需要用到 4 個(gè)字節(jié)表示。對(duì)于 2 字節(jié)的字符,UTF-16 編碼與 Unicode 碼點(diǎn)相等。
  • UTF-32 編碼:每個(gè)字符都使用 4 個(gè)字節(jié)。這意味著 UTF-32 會(huì)比 UTF-8 和 UTF-16 更占用空間,特別是對(duì)于 ASCII 字符占比較高的文本。

從存儲(chǔ)空間的角度看,使用 UTF-8 表示英文字符非常高效,因?yàn)樗鼉H需 1 個(gè)字節(jié);使用 UTF-16 編碼某些非英文字符(例如中文)會(huì)更加高效,因?yàn)樗恍枰?2 個(gè)字節(jié),而 UTF-8 可能需要 3 個(gè)字節(jié)。

從兼容性的角度看,UTF-8 的通用性最佳,許多工具和庫(kù)都優(yōu)先支持 UTF-8 。

編程語言的字符編碼

對(duì)于以往的大多數(shù)編程語言,程序運(yùn)行中的字符串都采用 UTF-16 或 UTF-32 這類等長(zhǎng)的編碼。在等長(zhǎng)編碼下,我們可以將字符串看作數(shù)組來處理,這種做法具有以下優(yōu)點(diǎn)。

  • 隨機(jī)訪問: UTF-16 編碼的字符串可以很容易地進(jìn)行隨機(jī)訪問。UTF-8 是一種變長(zhǎng)編碼,要找到第 i 個(gè)字符,我們需要從字符串的開始處遍歷到第 i 個(gè)字符,這需要 O(n) 的時(shí)間。
  • 字符計(jì)數(shù): 與隨機(jī)訪問類似,計(jì)算 UTF-16 字符串的長(zhǎng)度也是 O(1) 的操作。但是,計(jì)算 UTF-8 編碼的字符串的長(zhǎng)度需要遍歷整個(gè)字符串。
  • 字符串操作: 在 UTF-16 編碼的字符串中,很多字符串操作(如分割、連接、插入、刪除等)都更容易進(jìn)行。在 UTF-8 編碼的字符串上進(jìn)行這些操作通常需要額外的計(jì)算,以確保不會(huì)產(chǎn)生無效的 UTF-8 編碼。

實(shí)際上,編程語言的字符編碼方案設(shè)計(jì)是一個(gè)很有趣的話題,其涉及到許多因素。

  • Java 的 ?String? 類型使用 UTF-16 編碼,每個(gè)字符占用 2 字節(jié)。這是因?yàn)?Java 語言設(shè)計(jì)之初,人們認(rèn)為 16 位足以表示所有可能的字符。然而,這是一個(gè)不正確的判斷。后來 Unicode 規(guī)范擴(kuò)展到了超過 16 位,所以 Java 中的字符現(xiàn)在可能由一對(duì) 16 位的值(稱為“代理對(duì)”)表示。
  • JavaScript 和 TypeScript 的字符串使用 UTF-16 編碼的原因與 Java 類似。當(dāng) JavaScript 語言在 1995 年被 Netscape 公司首次引入時(shí),Unicode 還處于相對(duì)早期的階段,那時(shí)候使用 16 位的編碼就足夠表示所有的 Unicode 字符了。
  • C# 使用 UTF-16 編碼,主要因?yàn)?.NET 平臺(tái)是由 Microsoft 設(shè)計(jì)的,而 Microsoft 的很多技術(shù),包括 Windows 操作系統(tǒng),都廣泛地使用 UTF-16 編碼。

由于以上編程語言對(duì)字符數(shù)量的低估,它們不得不采取“代理對(duì)”的方式來表示超過 16 位長(zhǎng)度的 Unicode 字符。這是一個(gè)不得已為之的無奈之舉。一方面,包含代理對(duì)的字符串中,一個(gè)字符可能占用 2 字節(jié)或 4 字節(jié),從而喪失了等長(zhǎng)編碼的優(yōu)勢(shì)。另一方面,處理代理對(duì)需要增加額外代碼,這增加了編程的復(fù)雜性和 Debug 難度。

出于以上原因,部分編程語言提出了一些不同的編碼方案。

  • Python 3 使用一種靈活的字符串表示,存儲(chǔ)的字符長(zhǎng)度取決于字符串中最大的 Unicode 碼點(diǎn)。對(duì)于全部是 ASCII 字符的字符串,每個(gè)字符占用 1 個(gè)字節(jié);如果字符串中包含的字符超出了 ASCII 范圍,但全部在基本多語言平面(BMP)內(nèi),每個(gè)字符占用 2 個(gè)字節(jié);如果字符串中有超出 BMP 的字符,那么每個(gè)字符占用 4 個(gè)字節(jié)。
  • Go 語言的 string 類型在內(nèi)部使用 UTF-8 編碼。Go 語言還提供了 rune 類型,它用于表示單個(gè) Unicode 碼點(diǎn)。
  • Rust 語言的 str 和 String 類型在內(nèi)部使用 UTF-8 編碼。Rust 也提供了 char 類型,用于表示單個(gè) Unicode 碼點(diǎn)。

需要注意的是,以上討論的都是字符串在編程語言中的存儲(chǔ)方式,這和字符串如何在文件中存儲(chǔ)或在網(wǎng)絡(luò)中傳輸是兩個(gè)不同的問題。在文件存儲(chǔ)或網(wǎng)絡(luò)傳輸中,我們通常會(huì)將字符串編碼為 UTF-8 格式,以達(dá)到最優(yōu)的兼容性和空間效率。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)