1. 程式人生 > >C語言寬字元——字符集與字元編碼和寬字元之間的關係

C語言寬字元——字符集與字元編碼和寬字元之間的關係

前言:

距上一篇博文,已經是3個月的時間了,忙碌著專案開發,無暇顧及部落格。現在專案總算是結束了一個段落,是該總結的時候。4月份將會更新幾篇文章,都是在專案中遇到的問題,然後再深入瞭解之後總結出來的,希望通過這個平臺能與更多的人有更多的交流。

正文:

我在做日誌管理這一部分內容的時候,碰到了這樣一個問題:程式執行到時間處理的庫函式時,如 ctime, strftime, localtime,程式就會立即斷錯誤,搜尋之後才知道這種情況很有可能是因為前面的程式出現了記憶體洩露或越界,用 valgrind 除錯的時候,它總是會在這麼一行程式上警告:

wcsncpy(log->name, pu_desc->name, sizeof(log->name)-1);
兩個結構體成員的 name 均定義成 wchar_t name[32],所以使用了 wcsncpy。開始的時候怎麼都沒看出來哪裡出問題了,我還特意在複製的大小值那裡減1,就是為了預防越界。後來用 gdb 跟蹤到這裡,gdb 顯示第三個引數傳遞的是 127,的確是沒錯,因為在 GNU 的 C 庫裡面,wchar_t 是4個位元組的,又回頭查閱了 wcsncpy 的幫助手冊,才看到這個函式要求傳遞的第三個引數是寬字元的個數,而不是位元組數。傳遞了 127 個寬字元,肯定就越界了。

除了這個問題以外,在資料庫處理的時候也碰到了字元亂碼的問題,因為這個名稱是要求用中文顯示的,後來雖然問題解決了,但是一直搞不清楚UTF-8,寬字元之間到底是一種怎麼樣的關係,更別說再扯到其他的 UTF-16 等等,就徹底糊塗了。經過這幾天的深入瞭解,總算是有些頭緒了,現在以問題列表的形式將學習內容總結如下:

一、什麼是ISO 10646,Unicode,UCS,UTF-8,UTF-16,UTF-32,UCS-2,UCS-4?它們到底存在什麼樣的關係?

1. 字符集和字元編碼

要弄清這些,先要明白兩個概念:字符集(character set)和字元編碼(character encoding)。ISO 10646是標準,UCS 和 Unicode 是字符集,剩下的都是字元編碼字符集是一個表,指定字元對應的數值(稱為 code point),而字元編碼是指這些 code points 的表現形式。舉例說明,比如字元  的字符集數值是8364(通常用十六進位制表示為U+20AC),而是用UTF-8字元編碼的結果就是E2 82 AC。其中 UCS 這個
字符集就是由 ISO 10646 標準定義的,全稱是Universal Character Set。而 Unicode 是從一些多語言軟體製作廠家組織在一起的 Unicode Project 中誕生的。UCS-2和UCS-4字元編碼是由ISO 10646標準制定,而UTF-8到UTF-32是由Unicode Project制定,UTF的全稱是 Unicode Transmission Format(也可以是 UCS Transmission Format)。

2. ISO 10646 Project 和 Unicode Project

這兩個獨立的專案組織,開始的時候都有各自的字符集(UCS 和 Unicode)。但是在1991年的時候他們意識到這樣對整個世界來說並不是好事,開始將成果統一在一起,使用統一的字元編碼表,但是仍然以兩個獨立組織存在。隨後的Unicode標準都有相對應的ISO 10646的標準,區別上也不是很大,只不過Unicode標準寫的更詳細明瞭,排版也很優秀,因此很適合作為開發的參考手冊使用,除此之外,Unicode還有一些字元比較、排序等等的演算法說明,而 ISO 10646 標準就是一張字符集表而已,字元處理演算法則定義在其他的標準上。

3. UCS-2 和 UCS-4 編碼

顧名思義,這兩個字元編碼分別對應用2個位元組和4個位元組表示一個字元。起初 ISO 10646 標準定義的都是31位的字符集,也就是UCS-4,但是直到2001年才出現第一個超過了16位編碼的字元(16位以內能表示的字符集稱為 Basic Multilingual Plane,簡稱BMP),因此使用 UCS-2 表示 BMP 的字元,而UCS-4能表示所有的 Unicode 字元。

4. UTF-8, UTF-16 和 UTF-32 編碼

Unicode起初則是用16位來定義字符集,也就是UCS-2。後來發現不夠用了,於是將超過2個位元組表示的字元擴充套件到21位,並將字符集空間(從0x00xxxx xxxx - 0x10xxxx xxxx)分成16個部分(每個部分稱為一個 Plane),高5位從 0x00 - 0x10 分別代表這16個Plane,每一個Plane都有 2^16 個編碼空間。 超過16位的如何編碼呢?Unicode 字符集定義了 surrogate pair 的16位位元組對,也就是需要32位來表示,編碼規則是將字元值減去0x10000,得到一個20位的編碼範圍,20位中的高10位加上0xD800得到 lead surrogate(範圍是0xD800 - 0xDBFF), 低10位的加上 0xDC00 得到trail surrogate(範圍是 0xDC00 - 0xDFFF),注意因為10位所能表示的編碼範圍是 0x000 - 0x03FF, 因此這兩個 surrogate 位元組不會衝突。但是它們會和 BMP 字元衝突,為了解決這個問題, Unicode 要求從0xD800 - 0xDFFF 作為保留範圍,不能用來定義字元,而且要求其他 UTF 組的編碼也要遵守這個規則(但是實際上,很多軟體並不遵守,所以這種編碼方式也導致了很多bug的存在)。 這個需要用21位才能表示完整的Unicode字符集也就是 UTF-16 了,BMP 部分的編碼方式與UCS-2完全相同,採用2個位元組表示,超過 BMP 的只能用4個位元組的 surrogate pair 4個位元組來表示,因此這是一個變長的字符集。於是就有了 UTF-32,它採用固定的4個位元組來表示字元,只不過它的字符集範圍是和 UTF-16 一樣的,所以除了字符集範圍以外,UTF-32 和 UCS-4 是一樣的,都是用4個位元組來表示,而且21位以內的編碼方式完全相同。 另外由於用多位元組來表示一個字元,就會出現位元組序的問題,因此在這些編碼的後面加上BE(Big Endian)或LE(Little Endian)來表示大端和小端位元組序的編碼,不加這些字尾則根據所處環境來決定。於是就有 UCS-2, UCS-2BE, UCS-2LE, UCS-4,UCS-4LE, UCS-4BE, UTF-16, UTF-16BE, UTF16-LE, UTF-32, UTF-32BE 和 UTF-32LE 這些字元編碼。除此之外,還有一種以BOM(Byte Order Mask)來區分位元組序的方法,讀者可自行查詢相關資料。 至於 UTF-8 字元編碼則是一個充滿想象的設計,之所以名字中含有8,是因為它特殊的編碼方式決定了它不受位元組序的影響。具體說來如下所示:     (1) 128以內的,也就是 0x00-0x7F, 相容 ASCII 編碼,均用1個位元組表示。     (2) 超過128的用2-6個位元組表示,起始位元組用多個1加一個0後加資料來表示,1的個數代表總共用多少個位元組來表示一個字元,比如110x xxxx就表示用2個位元組表示一個字元(這兩個位元組包括起始位元組),後續的位元組用 10xx xxxx 的形式,均以 10 開頭,資料則填充在所有剩下的位上面(也就是 x 的位置,包括起始位元組沒有用掉的位)。比如上面說的 € 字元,它的Unicode值是 U+20AC,拆成二進位制位就是 0010 0000 1010 1100, UTF-8的編碼就是 1110 0010 1000 0010 1010 1100,也就是E2 82 AC(資料部分用下劃線標明)起始位元組的 E(1110) 也就表示是用3個位元組來表示這個字元。 UTF-8的編碼規則開始看起來可能會很複雜,需要多看幾次才能明白,但是看過之後,應該就會明白為什麼它不受位元組序的影響,因為所有128以內的位元組均是以0開頭,而128以外的都是以1開頭,其中128以外的起始位元組是不可能跟後續位元組衝突,因為最少也是以 110x 開頭(表示2個位元組)。而後續的位元組都是以10開頭,並且無論大小端,都是從起始位元組按照順序往下排的。除此之外,UTF-8編碼還有一個優點,它是符合C語言字串的要求,以 '\0' 結束,因此所有的普通字串操作如 strcpy 等對 UTF-8 編碼的字元完全有效,而 UTF-16, UTF-32等中間填充大量的 '\0' 字元,則不能使用這些函式。不過有人可能認為 UTF-8 編碼有些種族歧視,因為對西文字元,只要用1到2個位元組就可以表示,而從中東開始往東亞的方向,所需要的位元組數逐漸增加,(比如中文在 UTF-8 編碼中普遍使用3個位元組來表示)不過對於內部儲存不再成問題的今天,這個已經不算什麼了。

5. 補充

有一個叫 unicode 的小程式,可以列印字元的 Unicode 編碼,比如像下圖所示為“中”的 Unicode 編碼
只關注編碼部分,輸出結果的第一行顯示“中”在 Unicode 字符集的數值是 U+4E2D, 對應的 UTF-8 和 UTF-16BE 的編碼分別是 E4 B8 AD 和 4E2D(讀者可自行按照編碼規則來檢驗 UTF-8 編碼)

二、C語言的寬字元又跟這些有什麼關係?

為什麼 C 語言會有一個寬字元的型別?這要涉及到兩個概念,內部表示(Internal representation)和外部表示(External representation),前者表示字元是如何儲存在記憶體裡面的,也就是字元在程式執行時的表現形式;而後者表示字元是如何存放在外部介質上以及在外部通訊的表現形式。歷史上這兩個本是同一個概念,但是隨著字符集的逐漸擴大,也就獨立出兩個概念。上面所說的編碼方式全部是外部表示,也就是我們通常所說的某個檔案採用何種編碼。C語言定義的寬字元則是內部表示,即這些字元在程式中的表現形式。 然而在C語言的標準裡面,沒有指定 wchar_t 的具體長度,只是說這個型別能表示所有的字元。在 GNU 的 C 庫,wchar_t 是和 UCS-4 相同的,即統一使用4個位元組來表示所有寬字元,而不像UCS-2的兩個位元組或UTF-8的變長位元組。但是如果在某些嵌入式系統中,把 wchar_t 定義成 char 也不是沒有可能,因此可移植的程式中不推薦使用 wchar_t。 另外除了 wchar_t 以外,還有 wint_t 型別對應 int 型別。除此之外,還有兩個巨集定義 WCHAR_MIN 和 WCHAR_MAX 分別表示寬字元的最小長度和最大長度,檔案結束符 EOF 也有相對應的寬字元表示 WEOF。

三、什麼時候該使用寬字元?什麼時候可以不用?

如果你所需要的又只是簡單的複製移動操作,那完全可以不用寬字元,但是如果需要諸如查詢字元的個數、字串排序等操作,就必須要轉換成寬字元了。不過對複製移動操作,需要注意的是,當外部表示是用 UTF-8 的編碼方式時,可以採用 str 組的字串操作,而對於其他的編碼,那就只能使用 mem 組的記憶體處理函式,否則會造成資料的丟失。

相關推薦

C語言字元——字符集字元編碼字元之間關係

前言: 距上一篇博文,已經是3個月的時間了,忙碌著專案開發,無暇顧及部落格。現在專案總算是結束了一個段落,是該總結的時候。4月份將會更新幾篇文章,都是在專案中遇到的問題,然後再深入瞭解之後總結出來的,希望通過這個平臺能與更多的人有更多的交流。 正文: 我在做日誌管理這一部

C語言中結構體字元陣列之間的相互轉換

#include <stdio.h> #include <stdlib.h> #include <string.h> #pragma  pack(push)  //儲存對齊狀態   #pragma  pack(1)   typedef st

C語言中字串常量字元陣列

字串常量與字元陣列的關係在C語言中沒有專門的字串變數,如果想將一個字串存放在變數中以便儲存,必須使用字元陣列,即用一個字元型陣列來存放一個字串,陣列中每一個元素存放一個字元。例如“char a[10]="love".”

資料結構 c語言實現順序佇列(輸數字入隊,字元出隊)

一.標頭檔案seqqueue.h實現 #ifndef __SEQQUEUE_H__ #define __SEQQUEUE_H__ #include<stdio.h> #include<stdlib.h> #include<stdbool.h&g

Leetcode演算法題(C語言)15--字串中的第一個唯一字元

題目:字串中的第一個唯一字元 給定一個字串,找到它的第一個不重複的字元,並返回它的索引。如果不存在,則返回 -1。 案例: s = “leetcode” 返回 0. s = “loveleetco

c語言】第一個只出現一次的字元題目:在字串中找出第一個只出現一次的字元

// 第一個只出現一次的字元題目:在字串中找出第一個只出現一次的字元。 // 如輸入“abaccdeff”,則輸出’b’。 #include <stdio.h> #include <string.h> char find_one(ch

c語言:用getchar函式讀入兩個字元給c1,c2,用putcharprintf輸出。思考問題

用getchar函式讀入兩個字元給c1,c2,分別用putchar和printf輸出這兩個字元。思考以下問題:(1)變數c1和c2定義為字元型還是整型?或二者皆可?(2)要求輸出c1和c2的ASCII碼,應如何處理?(3)整形變數和字元變數是否在任何情況下都可以互相代替?ch

C#語言-08.序列化反序列化

clas 本質 cnblogs 語法 信息 字段 使用 serializa col a. 序列化:是將對象的狀態存儲到特定存儲介質中的過程 i. 語法:public void Serialize(序列化過程的文件流,保存的對象)

C#語言中數組集合的區別(以List集合為例)

類型 添加元素 list 添加 一個 保存 操作方法 適用於 length 數組用於保存固定數量的數據,定長,占用內存少,遍歷速度快; 集合保存的數據數量,可以在程序的執行過程中,不斷發生變化,不定長,占用內存多,遍歷速度慢; 在功能上,數組能實現的所有功能,集合都能實現;

C語言的運算符表達式(下)

C語言;編程入門;  通過昨天的介紹,大家知道了+、-、*、/、%這額運算符的使用方法。今天我們來講講昨天沒說的++和--運算符。  在C語言中,++和--占了很重要的地位,比如循環,判斷等語句都需要使用。下面我們來說一下他們的使用方法:1、++運算符:使用++運算符的

學習筆記-C語言6(指標動態記憶體分配)

1. 指標 指標的引入: 指標是C語言最強大的功能之一,使用指標可以儲存某個變數在記憶體中的地址,並且通過操作指標來對該片記憶體進行靈活的操作,例如改變原變數的值,或者構造複雜的資料結構。指標一般初始化為NULL(0)。& 是取地址運算,* 是間接運算子,通過 * 可以訪問與修改

C語言利用連結串列檔案實現登入註冊

C語言實現簡登入和註冊功能 C語言實現註冊登入 使用連結串列 使用檔案 版本二:利用連結串列 此版本使用的連結串列,第一個版本使用的是陣列 陣列版本連線 這裡我使用的線性連結串列,一定要注意在判斷語句或賦值語句中不可將指標指向未定義的區域,這會產生很大問題,所以

嵌入式C語言C語言的高階表達指標的高階應用

指標陣列與陣列指標、函式指標:                   指標陣列  int *p[5]   相當於int *(p[5])  (陣

C語言檔案的開啟關閉

C語言檔案的開啟與關閉 在C語言中,檔案操作都是由庫函式來完成的。 檔案的開啟(fopen函式) fopen() 函式用來開啟一個檔案,它的原型為: FILE *fopen(char *filename, char *mode); filename為檔名(包括檔案路徑),mo

C語言之 分支語句迴圈語句粗見

今天讓我們走進C語言中的兩個基本語句的世界中,C語言呢,一共有兩大種語句,即分支語句和迴圈語句,他們的身影幾乎遍插整個程式設計界,是最最基本的語法知識。所以不可小看他們!接下來先看看分支語句: 分支語句(選擇語句) 分支語句又稱選擇語句,那麼先看看語句是什麼? 語句:以分號

windows c語言控制檯接收按鍵滑鼠

#include <iostream> #include <Windows.h> int main() { //get console handler HANDLE h = GetStdHandle(STD_INPUT_HANDLE); if (h =

C語言筆記14--指標

指標是C語言繞不過的話題,指標的功能也非常強大,指標也有多級,但常用的也就一級和二級指標。指標其實就是地址,指標變數就是儲存指標的變數。有了指標就可以修改變數的值,也是遊戲外掛的原理。 1.指標的長度 #include<stdio.h> #i

C語言知識點(3)-運算子表示式

運算子與表示式 算數運算子 算術:+,-,*,/,%  考試一定要注意:“/” 兩邊都是整型的話,結果就是一個整型。 3/2的結果就是1.   “/” 如果有一邊是小數,那麼結果就是小數。 3/2.0的結果就是1.5  %符號兩邊要求是整數。不是整數

C語言,迭代遞迴

概念 迭代(iteration)是重複反饋過程的活動,其目的通常是為了逼近所需目標或結果。每一次對過程的重複稱為一次“迭代”,而每一次迭代得到的結果會作為下一次迭代的初始值。 遞迴( recursion)是程式呼叫自身的程式設計技巧。 *迭代跟遞迴本質都是一種方法。而遞迴函式顧

C語言變量定義數據溢出(初學者)

function res color RoCE abc 說明符 形式 string bold 1、變量定義的一般形式為:類型說明符、變量名標識符等;例:int a,b,c;(abc為整型變量) 在書寫變量定義時應註意以下幾點: (1)允許在一個類型說明符後,定義多個相同類型