1. 概述

unicode 码又称为“万国码”,顾名思义,世界上所有语言对应的字符都可以在 unicode 码表中找到对应的编号。而我们常说的 utf-8,utf-16, utf-32 等则是编码方式,是将码表中的编码转化成计算机字节的方法。


unicode 是一种码表,像这样的还有 US-ASCII,GBK,UCS-2,UCS-4 等。许多人搞不清楚 unicode 和 utf-16 之间的区别,将他们混为一谈。这里做个简单澄清,希望对字符编码“小白”有些帮助。


编码方式将任意一种码表中的每个编号用一种通用方式存储在计算机中,比如下文中要提到的 utf-8 编码。


2. utf-8

utf-8 是一种变长编码,根据维基百科的描述,它的编码方式如下:



Bits for
code point
First
code point
Last
code point
Byte 1Byte 2Byte 3Byte 4
17U+0000U+007F0xxxxxxx


211U+0080U+07FF110xxxxx10xxxxxx

316U+0800U+FFFF1110xxxx10xxxxxx10xxxxxx
421U+10000U+10FFFF11110xxx10xxxxxx10xxxxxx10xxxxxx


稍微解释一下:

U+0000 - U+007F 对应的 US-ASCII 码表,它在 utf-8 编码中只占用一个字节。也就是将 ASCII 码值直接用二进制表示

U+0080 - U+10FFFF 则是扩展的码表,其中包含了 GB2312 码表(U+A1A1 - U+FEFE)。


C 语言代码如下:

头文件

#pragma once

enum EncodeType
{
    u8,    // utf-8 编码
    u16,    // short-wchar 编码,就是在使用 wchar_t 时候,添加编译选项 -fshort-wchar
    u32,    // wchar_t (linux 下的 wchar_t 是 4 个字节存储的)
    other
};

/*
    @brief
        将目标字符串转换为 utf-8 编码
    @param [in] src
        源串
    @param [out] dest
        存储转换后的结果
    @param [in] maxBytes
        最多转换的字节数,以防止越界。如果转换的字符串超过 maxBytes,则会做截断处理,
        并保证截断的结果是合法的 utf-8 字符串。
    @param [in] type
        源串的编码方式,一般而言: linux char 是 u8 编码;wchar_t 则是 u32 编码;如果
        在编译的时候使用 -fshort-wchar 选项,则 wchar_t 是 u16 编码
    @return
        转换后的串长度 
*/
int convert2Utf8(const unsigned char* src, char* dest, int maxBytes, EncodeType type);

源文件

#include "utf8.h"
#include <wchar.h>
#include <locale.h>
#include <string.h>
#include <stdio.h>


static char g_utf8[10];

static void _convertCharacter(unsigned character, int& bytes)
{
    /* 注意:以下算法获取的结果需要进行逆序才正确 */
    if (character < 128)
    {
        bytes = 1;
        g_utf8[0] = (char)character;
        return;
    }
    
    bytes = 0;
    unsigned bits = 0;
    unsigned afterShift = character >> 6;
    
    while (afterShift > 0)
    {
        bits = character & 63;    /* 截取 6 位 */
        bits |= 128;        /* 添加 10 */
        g_utf8[bytes++] = bits;
        character = afterShift;
        afterShift = character >> 6;
    }
    g_utf8[bytes] = character;
    for (int i = 0; i < bytes + 1; i++)
    {
        g_utf8[bytes] |= 1 << (7 - i);
    }
    bytes++;
}

int convert2Utf8(const unsigned char* src, char* dest, int maxBytes, EncodeType type)
{
    switch (type)
    {
        case u8:
            {
                int i;
                maxBytes--; /* 需要保证能存储字符串结束符,因此有效字符个数应该不超过 maxBytes-1 */
                for (i = 0; i < maxBytes && src[i] != '\0'; i++)
                {
                    dest[i] = src[i];
                }

                dest[i] = '\0'; /* 添加串结束符 */
                return i;
            }
        case u32:
            {
                int len = 0;
                int bytes;
                wchar_t* target = (wchar_t*)src;
            
                for (int i = 0; target[i] != 0; i++)
                {
                    /* 转换一个字符 */
                    _convertCharacter(target[i],  bytes);
                    if (len + bytes >= maxBytes)
                        return len;
                    for (int i = bytes-1; i > -1; i--)
                    {
                        dest[len++] = g_utf8[i];
                    }
                    dest[len] = '\0'; /* 不可少 */
                }
                return len;
            }
        case u16:
            {
                int len = 0;
                int bytes;
            
                unsigned short* target = (unsigned short*)src;
                for (int i = 0; target[i] != 0; i++)
                {
                    /* 转换一个字符 */
                    _convertCharacter(target[i],  bytes);
                    if (len + bytes >= maxBytes)
                        return len;
                    for (int i = bytes-1; i > -1; i--)
                    {
                        dest[len++] = g_utf8[i];
                    }
                    dest[len] = '\0'; /* 不可少 */
                }
                return len;
            }
        default:
            {
                printf("Unknown encoding!!\n");
                return 0;
            }
    }
}

int main(void)
{
    const wchar_t* str = L"中国";

    /* 获取系统环境变量设置的本地配置,用于正常显示中文 */
    setlocale(LC_ALL, "");

    char strU8[20];
    int n = convert2Utf8((unsigned char*)str, strU8, 20, u16);
    printf("%s, length: %d\n", strU8, n);
    return 0;
}


3. utf-16/utf-32


utf-16 编码就是将超出 127 码值的 unicode 编号用两个字节存储。utf-32 是 utf-16 的扩展,目前只是简单地将 utf-16 编码结果的高位字补 0,以凑齐 4 个字节。


utf-8 转化为 utf-16/utf-32 的代码如下。

#include <locale.h>
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>

#define safe_add(ch) if (c < maxBytes) dest[c++] = (wchar_t)ch; else break

int convert2Unicode(const char* src, wchar_t* dest, int maxBytes)
{
    int i = 0;
    int c = 0;

    maxBytes--;

    while (src[i])
    {
        if (src[i] > 0)
        {
            safe_add(src[i]);
            i++;
        }
        else
        {
            /* 计算多少个字节 */
            int bytes = 0;
            unsigned char t = src[i];
            for (int shift = 7; shift > 0; shift--)
            {
                if ((t & (1<<shift)) == 0)
                    break;
                bytes++;
            }

            /* 检查是否合法 utf-8 格式 */
            for (int j = 1; j < bytes; j++)
            {
                t = src[i+j];
                if ((t >> 6) != 2)
                {
                    printf("INVALID FORMAT!!\n");
                    exit(1);
                }
            }
            /* 转换 */
            unsigned tranform = 0;
            t = src[i];
            
            tranform += t & ((1<< (7-bytes)) - 1);
            for (int j = 1; j < bytes; j++)
            {
                t = src[i+j];
                tranform <<= 6;
                tranform += t & 63;
                
            }
            safe_add(tranform);
            i += bytes;
        }
    }
    dest[c] = 0;
    return c;
}

int main(void)
{
    setlocale(LC_ALL, "");
    char* str = "中国人, *,shsu,";
    wchar_t wstr[20];
    convert2Unicode(str, wstr, 20);
    wprintf(L"%ls\n", wstr);
    return 0;
}