程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 常用編碼詳解

常用編碼詳解

編輯:關於VC++

一、通用字符集(UCS)

ISO/IEC 10646-1 [ISO-10646]定義了一種多於8比特 字節的字符集,稱作通用字符集(UCS),它包含了世界上大多數可書寫的字符系統。已 定義了兩種多8比特字節編碼,對每一個字符采用四個8比特字節編碼的稱為UCS-4,對每 一個字符采用兩個8比特字節編碼的稱為UCS-2。它們僅能夠對UCS的前64K字符進行編址, 超出此范圍的其它部分當前還沒有分配編址。

二、基本多語言面(BMP)

ISO 10646 定義了一個31位的字符集。 然而,在這巨大的編碼空間中,迄今為止 只分配了前65534個碼位 (0x0000 到 0xFFFD)。 這個UCS的16位子集稱為 “基本多 語言面 ”(Basic Multilingual Plane, BMP)。

三、Unicode編碼

歷 史上, 有兩個獨立的, 創立單一字符集的嘗試。 一個是國際標准化組織(ISO)的 ISO 10646 項目; 另一個是由(一開始大多是美國的)多語言軟件制造商組成的協會組織的 Unicode 項目。幸運的是, 1991年前後, 兩個項目的參與者都認識到: 世界不需要兩個 不同的單一字符集。它們合並雙方的工作成果,並為創立一個單一編碼表而協同工作。 兩個項目仍都存在並獨立地公布各自的標准, 但 Unicode 協會和 ISO/IEC JTC1/SC2 都 同意保持 Unicode 和 ISO 10646 標准的碼表兼容, 並緊密地共同調整任何未來的擴展 。Unicode 標准額外定義了許多與字符有關的語義符號學, 一般而言是對於實現高質量 的印刷出版系統的更好的參考。

四、UTF-8編碼

UCS-2和UCS-4編碼很難在 許多當前的應用和協議中使用,這些應用和協議假定字符為一個8或7比特的字節。即使新 的可以處理16比特字符的系統,卻不能處理UCS-4數據。這種情況導致一種稱為UCS轉換格 式(UTF)的發展,它每一種有不同的特征。 UTF-8(RFC 2279),使用了8比特字節的所有 位,保持全部US-ASCII取值范圍的性質:US-ASCII字符用一個8比特字節編碼,采用通常 的US-ASCII值,因此,在此值下的任何一個8比特位字節僅僅代表一個US-ASCII字符,而 不會為其他字符。它有如下的特性:

1)UTF-8向UCS-4,UCS-2兩者中任一個進行 相互轉換比較容易。

2)多8比特字節序列的第一個8比特字節指明了系列中8比特 字節的數目。

3)8比特字節值FE和FF永遠不會出現。

4)在8比特字符流中 字符邊界從哪裡開始較容易發現。

UTF-8定義:

在UTF-8中,字符采用1到6 個8比特字節的序列進行編碼。僅僅一個8比特字節的一個序列中,字節的高位為0,其他 的7位用於字符值編碼。n(n>1)個8比特字節的一個序列中,初始的8比特字節中高n 位為1,接著一位為0,此字節余下的位包含被編碼字符值的位。接著的所有8比特字節的 最高位為1,接著下一位為0,余下每個字節6位包含被編碼字符的位。

下表總結了 這些不同的8比特字節類型格式。字母x指出此位來自於進行編碼的UCS-4字符值。

UCS-4范圍(16進制)   UTF-8 系列(二進制)
  0000 0000<- >0000 007F  0xxxxxxx
  0000 0080<->0000 07FF  110xxxxx 10xxxxxx
  0000 0800<->0000 FFFF  1110xxxx 10xxxxxx 10xxxxxx
  0001 0000<->001F FFFF  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  0020 0000<->03FF FFFF  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  0400 0000<->7FFF FFFF  1111110x 10xxxxxx ... 10xxxxxx

從UCS-4 到 UTF-8編碼規則如下:

1)從字符值和上表第 一列中決定需要的8比特字節數目。著重指出的是上表中的行是相互排斥的,也就是說, 對於一個給定的UCS-4字符,僅僅有一個有效的編碼。

2)按照上表中第二列每行 那樣准備8比特字節的高位。

3)將UCS字符值的位,從低位起填充在標記為x地方 。從UTF8序列中最後一個字節填起,然後剩下的字符值依次放到前一個字節中,如此重復 ,直到所有標記位x的位都進行了填充。

這裡我們僅僅實現Unicode到UTF8的轉換 ,Unicode都是兩個字節,定義為:

typedef usigned short WCHAR
// 輸 出的UTF8編碼至多是3個字節。
int UnicodeToUTF8(WCHAR ucs2, unsigned char *buffer)
{
  memset(buffer, 0, 4);
  if ((0x0000 <= ucs2) && (ucs2 <= 0x007f)) // one char of UTF8
  {
     buffer[0] = (char)ucs2;
    return 1;
  }
  if ((0x0080 <= ucs2) && (ucs2 <= 0x07ff)) // two char of UTF8
  {
    buffer[1] = 0x80 | char(ucs2 & 0x003f);
    buffer[0] = 0xc0 | char((ucs2 >> 6) & 0x001f);
    return 2;
  }
  if ((0x0800 <= ucs2) && (ucs2 <= 0xffff))  // three char of UTF8
  {
    buffer[2] = 0x80 | char(ucs2 & 0x003f);
    buffer[1] = 0x80 | char((ucs2 >> 6) & 0x003f);
    buffer[0] = 0xe0 | char((ucs2 >> 12) & 0x001f);
    return 3;
  }
  return 0;
} 

理論上,簡單的通過用2個0值的8比特字節來擴展每個UCS-2字符,則從 UCS-2到UTF-8編碼的算法可以從上面得到。然而,從D800到DFFF間的UCS-2值對(用 Unicode說法是代理對),實際上是通過UTF-16來進行UCS-4字符轉換,因此需要特別對待 :UTF-16轉換必須未完成,先轉換到於UCS-4字符,然後按照上面過程進行轉換。

從UTF-8到UCS-4解碼過程如下:

1)初始化UCS-4字符4個8比特字節的所有位為0。

2)根據序列中8比特字節數和上表中第二列(標記為x位)來決定哪些位編碼用於 字符值。

3)從編碼序列分配位到UCS-4字符。首先從序列最後一個8比特字節的最 低位開始,接著向左進行,直到所有標記為x的位完成。如果UTF-8序列長度不大於3個8比 特字節,解碼過程可以直接賦予UCS-2。

WCHAR UTF8ToUnicode(unsigned char *buffer)
{
  WCHAR temp = 0;
  if (buffer[0] < 0x80)                   // one char of UTF8
  {
    temp = buffer[0];
  }
  if ((0xc0 <= buffer[0]) && (buffer[0] < 0xe0))     // two char of UTF8
  {
    temp = buffer[0] & 0x1f;
    temp = temp << 6;
    temp = temp | (buffer[1] & 0x3f);
  }
  if ((0xe0 <= buffer[0]) && (buffer[0] < 0xf0))     // three char of UTF8
  {
    temp = buffer[0] & 0x0f;
    temp = temp << 6;
    temp = temp | (buffer[1] & 0x3f);
    temp = temp << 6;
    temp = temp | (buffer[2] & 0x3f);
   }
  if ((0x80 <= buffer[0]) && (buffer[0] < 0xc0))      // not the first byte of UTF8 character
    return 0xfeff;                     // 0xfeff will never appear in usual
   return temp;                      // more than 3-bytes return 0
}

注意:上面解碼算法的實際實現應該進行安全保護,以 便處理解碼無效的系列。例如:實現可能(錯誤)解碼無效的UTF-8系列0xC0 0x80為字符 U+0000,它可能導致安全問題或其他問題(比如把0當作數組結束標志)。更詳細的算法 和公式可以在[FSS_UTF],[UNICODE] 或[ISO-10646]附錄R中找到。

五、UTF-7編 碼

UTF-7:A Mail-Safe Transformation Format of Unicode(RFC1642)。這是一 種使用 7 位 ASCII 碼對 Unicode 碼進行轉換的編碼。它的設計目的仍然是為了在只能 傳遞 7 為編碼的郵件網關中傳遞信息。 UTF-7 對英語字母、數字和常見符號直接顯示, 而對其他符號用修正的 Base64 編碼。符號 + 和 - 號控制編碼過程的開始和暫停。所以 亂碼中如果夾有英文單詞,並且相伴有 + 號和 - 號,這就有可能是 UTF-7 編碼。

協議中定義的轉換規則:

1)集合D中的Unicode字符可以直接的編碼為 ASCII的等值字節。集合O中的字符可以有有選擇的的直接編碼為ASCII的等值字節,但要 記得其中的很多的字符在報頭字段是不合法的,或者不能正確的穿過郵件網關。

2 )通過在前面加上轉換字符"+",任何一個Unicode序列都可以使用集合B(更 改過的base64)中的字符編碼。"+"意味著後面的字節將被作為更改過的 BASE64字母表中的元素解析,直到遇到一個不是字母表中的字符為止。這些字符中會包含 控制字符,比如回車和換行;因此,一個Unicode轉換序列總是在一行上結束。注釋:有 兩個特殊的情形:"+-"表示''+'',"+ …… --"表示有一個真正的''-''字符出現了。多數 情況是沒有''-''標記結束。

3)空格、tab、回車和換行字符可 以直接使用ASCII等價字節表示。

那麼我們就可以定義算法了,我們先定義字符集 的相關數組:

typedef unsigned char byte
// 64 characters for base64 coding
byte base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";< br />// 8 characters are safe just as base64 characters for MAIL gates
byte safeChars[] = "''(),-.:?";
// 4 characters all means space
byte spaceChars[] = " \t\n\r";

注:在編 碼處理時候,我們需要對一個字節判斷屬於哪類字符,以便確定處理規則,如果簡單的使 用范圍比較的方式,效率很低,我們采用哈希表的思路:建立一個256長的數組,那麼對 於每一個字節的值,就可以定義一個類型。判斷時候,對每個字符都直接取數組的值。

// mask value defined for indentify the type of a byte
#define   BASE64    0x01
#define  SAFE    0x02
#define  SPACE     0x04
byte byteType[256];    // hash table used for find the type of a byte
bool firstTime = true;   // the first time to use the lib, wait for init the table
// 注:為了解碼base64編碼部分的字符,需要一 個哈希表,對一個base64字符都可以直接得到0-64之間的一個數:
byte base64Value[128];
這兩個哈希表在使用前要初始化:
void initUTF7Tables()
{
   byte *s;
   if(!firstTime)
   return;
   // not necessary, but should do it to be robust
   memset(byteType, 0, 256);
   memset(base64Value, 0, 128);

   for(s=base64Chars; *s!=''\0''; s++)
   {
    byteType[*s] |= BASE64;
    base64Value[*s] = s - base64Chars; // the offset, it is a 6bits value,0-64
   }

   for(s=safeChars; *s!=''\0''; s++)
    byteType[*s] |= SAFE;

   for(s=spaceChars; *s!=''\0''; s++)
    byteType[*s] |= SPACE;
   firstTime = false;
}

UTF-7編碼轉換時候,是與當前字 符是與狀態有關的,也就是說:

1)正處於Base64編碼狀態中

2)正處於直 接編碼狀態中

3)現在UTF-7的緩沖區裡,當前的字符是轉換開關 "+"

所以要定義相關的字段:

// the state of current character
#define  IN_ASCII  0
#define  IN_BASE64  1
#define AFTER_PLUS  2
在使用規則2進行編碼時候,需要使用base64的方法 ,也就需要2個全局的輔助變量:

int state;         // state in which we are working
int nbits;         // number of bits in the bit buffer
unsigned long bitBuffer;  // used for base64 coding
把 一個Unicode字符轉化為一個UTF-7序列:返回寫到緩沖區裡的字節數目,函數影響了 state,nbits,bitBuffer三個全局變量。這裡先實現了一個簡單的輔助函數,功能是把 一個Unicode字符轉變後寫到提供的緩沖區中,返回寫入的字節個數。在開始編碼Unicode 字符數組中第一個字符的時候,state,nbits,bitBuffer三個全局變量需要被初始化:

state = IN_ASCII;
nbits = 0;
bitBuffer = 0;
int UnicodeToUTF7(WCHAR ucs2, byte *buffer)
{
  byte *head = buffer;
  int index;

  // is an ASCII and is a byte in char set defined
  if (((ucs2 & 0xff80) == 0)) && (byteType[(byte)u2] & (BASE64|SAFE|SPACE)))
  {
    byte temp = (byte)ucs2;

    if (state == IN_BASE64) // should switch out from base64 coding here
    {
     if (nbits > 0)     // if some bits in buffer, then output them
     {
        index = (bitBuffer << (6 - nbits)) & 0x3f;
       *s++ = base64[index];
     }
     if ((byteType[temp] & BASE64) || (temp == ''-''))
       *s++ = ''-'';
   state = IN_ASCII;
    }
    *s++ = temp;

    if (temp == ''+'')
      *s++ = ''-'';
  }
  else
  {
     if (state == IN_ASCII)
    {
     *s++ = ''+'';
     state = IN_BASE64;     // begins base64 coding here
     nbits = 0;
     bitBuffer = 0;
    }
    bitBuffer <<= 16;
    bitBuffer |= ucs2;
    nbits += 16;

    while(nbits >= 6)
     {
     nbits -= 6;
     index = (bitBuffer >> nbits) & 0x3f;  // output the high 6 bits
     *s++ = base64 [index];
    }
  }
  return (s - head);
}

說明:對於合法的Unicode字符數組,可以通過逐個輸入數組中的字符, 連續調用上面的函數,得到一個UTF-7字節序列。需要說明的是:最後一個Unicode字符應 該是上面三個字節數組中某個字符的等值。

下面,我們實現一個簡單的說明函數 ,功能是:輸入一個UTF-7字節,可能得到並返回一個合法Unicode字符;也可能不能得到 ,比如遇到''+''或者因為還沒有完成一個字符的拼裝,這時返回一個 標志字符0xfeff,這個字符常用來標志Unicode編碼。

注:函數影響了state, nbits,bitBuffer三個全局變量。在開始處理第一個字節時候,變量需要被初始化為:

state = IN_ASCII;
nbits = 0;
bitBuffer = 0;
#define RET0 0xfeff
WCHAR UTF7ToUnicode(byte c)
{
   if(state == IN_ASCII)
   {
    if (c == ''+'')
    {
      state = AFTER_PLUS;
      return RET0;
    }
    else
     return (WCHAR)c;
   }
   if (state == AFTER_PLUS)
   {
    if (c == ''-'')
     {
      return (WCHAR)''+'';
    }
    else
    {
      state = IN_BASE64;
      nbits = 0;
      bitBuffer = 0; // it is not necessary
      // don''t return yet, continue to the IN_BASE64 mode
    }
   }

   // state == Base64
   if (byteType[c] & BASE64)
   {
    bitBuffer <<= 6;
    bitBuffer |= base64Value[c];
    nbits += 6;
    if (nbits >= 16)
    {
      nbits -= 16;
    return (WCHAR)((bitBuffer >> nbits) & 0x0000ffff);
    }
    return RET0;
   }
   // encount a byte which is not in base64 character set, switch out of base64 coding
   state = IN_ASCII;
   if (c != ''-'')
   {
    return (WCHAR)c;
   }
   return RET0;
}

說明:對於一個UTF-7序列,可以通過連續輸入字節 並調用上面的函數,判斷返回值,得到一個Unicode字符數組。

六、GB2312編碼中 漢字的確定

最早,表示漢字的區位碼中,分為94個區,每個區94個漢字,1-15區 是西文字符,圖形等,16-5為一級漢字,56-87為二級漢字,87區以上為新字用。而我們 在Windows默認的編碼,GB2312(1981年國家頒布的《信息交換用漢字編碼字符集基本集 》)國標碼,和區位碼的換算為:

國標碼 = 區位碼 + 2020H

而在漢字在 計算機內表示的時候為保證ASCII碼和漢字編碼的不混淆,又做了一個換算:

漢字 機內碼 = 國標碼 + 8080H

所以,真正的在Windows上的GB2312漢字編碼是機內碼 ,從上邊的兩個公式可以得到的就是:

漢字機內碼 = 區位碼 + a0a0H

一 個漢字的編碼最少要a0a0H,因此我們在CString中辨別漢字的時候可以認為:當一個字符 的編碼大於a0的時候它應該是漢字的一個部分。但是也有特殊的情況的,不是每個漢字的 兩個字節編碼都是大於a0H的,例如‘镕’的編碼是 ‘E946’,後 面的部分就不滿足大於a0H的條件。

七、Windows下多字節編碼和Unicode的轉換

Windows提供了API函數,可以把Unicode字符數組轉換為GB2312字符串。其中, Unicode數組在傳入時候最後一個為0,也就是所謂的null termidated字符串。在函數內 部得到要返回字節串的大小,請求空間,進行真正的轉換操作,指針在外部使用後釋放, 或者在類中加如其他的操作來處理,比如析構函數中釋放。返回值為寫到字節串裡數目。 int StringEncode::UnicodeToGB2312(char **dest, const WCHAR *src)
{
  char* buffer;
  int size = ::WideCharToMultiByte(CP_ACP, 0, src, -1, NULL, 0, NULL, NULL);
  // null termidated wchar''s buffer
  buffer = new char[size];
  int ret = ::WideCharToMultiByte(CP_ACP, NULL, src, -1, buffer, size + 1, NULL, NULL);
  if (*dest != 0)
    delete *dest;
  *dest = buffer;
  return ret;
}

注:其中見到有人在使用的時候,申請緩沖區空間時候是申請了(zise + 1)個來,最後一個字節寫''\0'',結束字符串。但是在我調試時候發現:系統給的size已經包含了一個寫入''\0''的字節,而且最後得到的串中,''\0''是已經被系統API寫入了。(也許我的實驗有錯誤,有待驗證)。把Unicode字符數組轉換為UTF-8和UTF-7的方法類似,只要是WideCharToMultiByte函數的第一個表示代碼頁參數改為CP_UTF7(65000)和CP_UTF8(65001)。

同樣道理,把多字節轉換為Unicode字符數組,也有相應的函數。和上 面的函數類似,可以通過先提供一個空緩沖區而先得到需要的大小,然後開辟空間得到最 後的字符數組。但是考慮到效率,可以適當犧牲一些空間,提供一個足夠大的字符數組, 數組大小在極端的情況下(全是ASCII)是和字節數組大小一樣的。

int StringEncode::Gb2312ToUnicode(WCHAR **dest, const char *src)
{
   int length = strlen(src);        // null terminated buffer
   WCHAR *buffer = new WCHAR[length + 1];  // WCHAR means unsinged short, 2 bytes
                      // provide enough buffer size for Unicodes
  int ret = ::MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, src, length, buffer, length);
  buffer[ret] = 0;
  if (*dest != 0)
    delete *dest;
  *dest = buffer;
  return ret;
}

注:刪除以前的緩沖區時候的操作,其實沒有必 要判斷是不是為空,因為刪除空指針是沒有問題的,因為delete內部提供了這樣的機制。

八、URL 解碼

用IE發送GET請求的時候,URL是用UTF-8編碼的,當對截包 數據分析時候就需要對數據解碼,下面的函數是一個簡單的實現:

CString CTestUrlDlg::UrlToString(CString url)
{
  CString str = "";
  int n = url.GetLength();
  url.MakeLower();
  BYTE a, b1, b2;
  for (int i=0; i= ''0'') && (c <= ''9''))
    d = c - ''0'';
  else if ((c >= ''a'') && (c <= ''f''))
  {
    d = c - ''a'' + 10;
  }
  else if ((c >= ''A'') && (c <= ''F''))
   {
    d = c - ''A'' + 10;
  }
  else
    d = 0;
  return d;
}
static void UnicodeToGB2312 (const WCHAR unicode, char* buffer)
{
//  int size = ::WideCharToMultiByte(CP_ACP, 0, unicode, -1, NULL, 0, NULL, NULL);
   int ret = ::WideCharToMultiByte(CP_ACP, NULL, &unicode, -1, buffer, 3, NULL, NULL);
}
CString CTestUrlDlg::Uft8ToGB(CString url)
{
  CString str = "";
  char buffer[3];
  WCHAR unicode;

  unsigned char * p = (unsigned char *)(LPCTSTR)url;
  int n = url.GetLength();
  int t = 0;
  while (t < n)
  {
    unicode = UTF8ToUnicode(p, t);
     UnicodeToGB2312(unicode, buffer);
    buffer[2] = 0;
    str += buffer;
  }
  return str;
}

示例:

CString str = "/MFC%E8%8B%B1%E6%96%87%E6%89%8B%E5%86% 8C.chm";
CString ret = UrlToString(str);
ret = Uft8ToGB(ret);  // MFC英文手冊.chm

九、總結

常見算法還有MIME等,由於篇幅限 制,並且網上已經有很多帖子,在此不再贅述。

對於本文,由於個人能力有限, 難免有疏漏的地方,還望指教,共同進步。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved