一文搞懂 GB2312、GBK、UTF-8 编码范围与解析过程

84次阅读
没有评论

共计 9435 个字符,预计需要花费 24 分钟才能阅读完成。

在嵌入式开发、串口通信、屏幕点阵字库、日志解析、旧系统兼容中,经常会碰到中文编码问题。

最常见的几个编码是:

ASCII
GB2312
GBK
UTF-8

它们之间的关系经常容易混淆,尤其是:

GB2312 和 GBK 是什么关系?GBK 和 UTF-8 是什么关系?为什么同一个“中”字,在不同编码下字节不一样?嵌入式里拿到一串字节,应该怎么解析?

本文从编码范围、解析流程和 C 语言示例代码三个角度进行整理。


1. 先理解一个核心概念

Unicode 是字符编号

Unicode 负责给全世界字符分配一个统一编号。

例如:

 中 = U+4E2D
国 = U+56FD
A  = U+0041

Unicode 本身更像是一个“字符编号表”。


UTF-8 是 Unicode 的一种编码方式

UTF-8 负责把 Unicode 编号转换成具体的字节序列。

例如:

 字符:中
Unicode:U+4E2D
UTF-8:E4 B8 AD

GB2312 / GBK 是另一套中文编码体系

GB2312 和 GBK 不是 Unicode 的编码方式,而是历史上的中文编码体系。

例如:

 字符:中
GB2312:D6 D0
GBK:D6 D0
UTF-8:E4 B8 AD

可以看到,同一个“中”字,在 GBK 和 UTF-8 中的字节完全不一样。


2. 几种编码的整体关系

 全球字符
  ↓
Unicode:统一字符编号,例如“中”= U+4E2D
  ↓
UTF-8 / UTF-16 / UTF-32 等编码方式

另一套中文编码体系:GB2312
  ↓ 扩展
GBK
  ↓ 扩展
GB18030

简单理解:

Unicode / UTF-8:现代互联网、跨平台系统常用
GB2312 / GBK:中文旧系统、点阵字库、嵌入式屏幕中常见 

3. ASCII 编码范围

ASCII 是最基础的单字节编码。

3.1 编码范围

0x00 ~ 0x7F

常见字符:

'A' = 0x41
'0' = 0x30
'' = 0x20'\n' = 0x0A

ASCII 只占 1 个字节。


3.2 ASCII 解析规则

只要当前字节满足:

byte <= 0x7F

就可以认为它是 ASCII 字符。

示例:

41 42 43

解析结果:

0x41 = A
0x42 = B
0x43 = C

4. GB2312 编码范围

GB2312 是早期简体中文编码标准,主要包含常用简体汉字和符号。

它兼容 ASCII。

也就是说:

0x00 ~ 0x7F 仍然表示 ASCII
中文字符使用两个字节表示 

4.1 GB2312 双字节范围

GB2312 的常见双字节范围是:

 高字节:0xA1 ~ 0xF7
低字节:0xA1 ~ 0xFE

组合范围大致为:

A1A1 ~ F7FE

其中汉字区常见范围:

B0A1 ~ F7FE

4.2 GB2312 分区

GB2312 使用区位码思想。

每个字符由:

 区号 + 位号 

组成。

计算方式:

 区号 = 高字节 - 0xA0
位号 = 低字节 - 0xA0

例如:

“中”= D6 D0

计算:

 区号 = 0xD6 - 0xA0 = 0x36 = 54
位号 = 0xD0 - 0xA0 = 0x30 = 48

所以:

“中”在 GB2312 中是第 54 区,第 48 位 

4.3 GB2312 示例

 中 = D6 D0
国 = B9 FA
你 = C4 E3
好 = BA C3

字符串:

 中国 

GB2312 编码:

D6 D0 B9 FA

4.4 GB2312 解析流程

GB2312 的解析过程比较简单:

 读取一个字节
  ↓
如果 <= 0x7F
  ↓
ASCII 字符,单字节解析
  ↓
如果在 0xA1 ~ 0xF7
  ↓
继续读取下一个字节
  ↓
如果第二字节在 0xA1 ~ 0xFE
  ↓
组成一个 GB2312 双字节字符 

流程图:

 当前字节 ch

ch <= 0x7F
    → ASCII

0xA1 <= ch <= 0xF7
    → 读取下一个字节
    → 判断是否在 0xA1 ~ 0xFE
    → 是:GB2312 字符
    → 否:非法编码

其他情况
    → 非法编码 

4.5 GB2312 解析示例代码

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

static int is_gb2312_first_byte(uint8_t ch)
{return ch >= 0xA1 && ch <= 0xF7;}

static int is_gb2312_second_byte(uint8_t ch)
{return ch >= 0xA1 && ch <= 0xFE;}

void parse_gb2312(const uint8_t *data, size_t len)
{
    size_t i = 0;

    while (i < len)
    {uint8_t ch = data[i];

        if (ch <= 0x7F)
        {printf("ASCII: 0x%02X, char = %c\n", ch, ch);
            i++;
        }
        else if (is_gb2312_first_byte(ch))
        {if (i + 1 >= len)
            {
                printf("GB2312 error: incomplete character at offset %lu\n",
                       (unsigned long)i);
                break;
            }

            uint8_t ch2 = data[i + 1];

            if (is_gb2312_second_byte(ch2))
            {
                int area = ch - 0xA0;
                int pos  = ch2 - 0xA0;

                printf("GB2312: 0x%02X%02X, area = %d, pos = %d\n",
                       ch, ch2, area, pos);

                i += 2;
            }
            else
            {
                printf("GB2312 error: invalid second byte 0x%02X at offset %lu\n",
                       ch2, (unsigned long)(i + 1));
                i++;
            }
        }
        else
        {
            printf("GB2312 error: invalid byte 0x%02X at offset %lu\n",
                   ch, (unsigned long)i);
            i++;
        }
    }
}

int main(void)
{
    /*
        "A 中国" 的 GB2312 编码:A    = 41
        中   = D6 D0
        国   = B9 FA
    */
    uint8_t text[] = {0x41, 0xD6, 0xD0, 0xB9, 0xFA};

    parse_gb2312(text, sizeof(text));

    return 0;
}

运行结果类似:

ASCII: 0x41, char = A
GB2312: 0xD6D0, area = 54, pos = 48
GB2312: 0xB9FA, area = 25, pos = 90

5. GBK 编码范围

GBK 是 GB2312 的扩展版本。

它的特点是:

 兼容 GB2312
支持更多汉字
支持更多生僻字
仍然兼容 ASCII

也就是说,GB2312 中已有的字符,在 GBK 中编码不变。

例如:

 中:GB2312 = D6 D0
GBK    = D6 D0

5.1 GBK 双字节范围

GBK 的双字节范围是:

 首字节:0x81 ~ 0xFE
尾字节:0x40 ~ 0xFE,排除 0x7F

也就是:

8140 ~ FEFE

但是尾字节不能是:

0x7F

5.2 GBK 和 GB2312 的范围对比

编码 首字节范围 尾字节范围 是否兼容 ASCII 是否兼容 GB2312
GB2312 A1 ~ F7 A1 ~ FE 本身
GBK 81 ~ FE 40 ~ FE,排除 7F

GBK 范围更大。

GB2312 的字节范围基本是 GBK 的一个子集。


5.3 GBK 示例

 中 = D6 D0
国 = B9 FA
芃 = C6 4D

其中:

“芃”不在 GB2312 中“芃”在 GBK 中 

这也是 GBK 比 GB2312 支持更多汉字的典型例子。


5.4 GBK 解析流程

GBK 的解析流程:

 读取一个字节
  ↓
如果 <= 0x7F
  ↓
ASCII 字符
  ↓
如果在 0x81 ~ 0xFE
  ↓
读取下一个字节
  ↓
判断第二字节是否在 0x40 ~ 0xFE 且不等于 0x7F
  ↓
是:GBK 双字节字符
  ↓
否:非法编码 

流程图:

 当前字节 ch

ch <= 0x7F
    → ASCII

0x81 <= ch <= 0xFE
    → 读取下一个字节
    → 判断是否满足:0x40 <= ch2 <= 0xFE && ch2 != 0x7F
    → 是:GBK 字符
    → 否:非法编码

其他情况
    → 非法编码 

5.5 GBK 解析示例代码

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

static int is_gbk_first_byte(uint8_t ch)
{return ch >= 0x81 && ch <= 0xFE;}

static int is_gbk_second_byte(uint8_t ch)
{return ch >= 0x40 && ch <= 0xFE && ch != 0x7F;}

void parse_gbk(const uint8_t *data, size_t len)
{
    size_t i = 0;

    while (i < len)
    {uint8_t ch = data[i];

        if (ch <= 0x7F)
        {printf("ASCII: 0x%02X, char = %c\n", ch, ch);
            i++;
        }
        else if (is_gbk_first_byte(ch))
        {if (i + 1 >= len)
            {
                printf("GBK error: incomplete character at offset %lu\n",
                       (unsigned long)i);
                break;
            }

            uint8_t ch2 = data[i + 1];

            if (is_gbk_second_byte(ch2))
            {printf("GBK: 0x%02X%02X\n", ch, ch2);
                i += 2;
            }
            else
            {
                printf("GBK error: invalid second byte 0x%02X at offset %lu\n",
                       ch2, (unsigned long)(i + 1));
                i++;
            }
        }
        else
        {
            printf("GBK error: invalid byte 0x%02X at offset %lu\n",
                   ch, (unsigned long)i);
            i++;
        }
    }
}

int main(void)
{
    /*
        "A 中芃" 的 GBK 编码:A    = 41
        中   = D6 D0
        芃   = C6 4D
    */
    uint8_t text[] = {0x41, 0xD6, 0xD0, 0xC6, 0x4D};

    parse_gbk(text, sizeof(text));

    return 0;
}

输出结果类似:

ASCII: 0x41, char = A
GBK: 0xD6D0
GBK: 0xC64D

需要注意:

 判断字节范围,只能判断它“像不像 GBK 编码”。想要知道具体对应哪个 Unicode 字符,还需要 GBK → Unicode 映射表。

6. UTF-8 编码范围

UTF-8 是目前最常见的文本编码,网页、JSON、Linux 文件、网络通信中大量使用 UTF-8。

UTF-8 的特点是:

 兼容 ASCII
变长编码
英文通常 1 字节
中文通常 3 字节
emoji 通常 4 字节 

6.1 UTF-8 字节结构

UTF-8 使用字节开头的二进制位来判断字符长度。

字节数 二进制格式 说明
1 字节 0xxxxxxx ASCII
2 字节 110xxxxx 10xxxxxx 常见欧洲字符
3 字节 1110xxxx 10xxxxxx 10xxxxxx 常见中文
4 字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx emoji、生僻字符等

6.2 UTF-8 常见范围

1 字节

00 ~ 7F

示例:

A = 41

2 字节

C2 80 ~ DF BF

结构:

110xxxxx 10xxxxxx

3 字节

E0 A0 80 ~ EF BF BF

结构:

1110xxxx 10xxxxxx 10xxxxxx

常见中文大多是 3 字节。

例如:

 中 = E4 B8 AD
国 = E5 9B BD

4 字节

F0 90 80 80 ~ F4 8F BF BF

结构:

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

常见 emoji 多数是 4 字节。

例如:

😀 = F0 9F 98 80

6.3 UTF-8 解析流程

UTF-8 的解析比 GBK 复杂一些。

核心判断:

 读取第一个字节
  ↓
0xxxxxxx
  → 1 字节 ASCII

110xxxxx
  → 后面还需要 1 个后续字节

1110xxxx
  → 后面还需要 2 个后续字节

11110xxx
  → 后面还需要 3 个后续字节 

后续字节必须满足:

10xxxxxx

也就是:

0x80 ~ 0xBF

6.4 UTF-8 示例

字符:

Unicode:

U+4E2D

UTF-8:

E4 B8 AD

二进制:

E4 = 1110 0100
B8 = 1011 1000
AD = 1010 1101

解析器看到:

1110xxxx

就知道这是一个 3 字节字符。

后面的两个字节:

10111000
10101101

都满足:

10xxxxxx

所以这是一个合法 UTF-8 字符。


6.5 UTF-8 解析示例代码

下面代码会解析 UTF-8 字节流,并输出 Unicode 码点。

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

static int is_utf8_continuation(uint8_t ch)
{return (ch & 0xC0) == 0x80;
}

int utf8_decode_one(const uint8_t *data, size_t len, uint32_t *codepoint, size_t *used)
{
    uint8_t b0;

    if (data == NULL || len == 0 || codepoint == NULL || used == NULL)
    {return -1;}

    b0 = data[0];

    /*
        1 字节:0xxxxxxx
    */
    if (b0 <= 0x7F)
    {
        *codepoint = b0;
        *used = 1;
        return 0;
    }

    /*
        2 字节:110xxxxx 10xxxxxx

        合法首字节范围通常从 C2 开始。C0、C1 会造成过短编码,不建议接受。*/
    if (b0 >= 0xC2 && b0 <= 0xDF)
    {
        uint8_t b1;

        if (len < 2)
        {return -2;}

        b1 = data[1];

        if (!is_utf8_continuation(b1))
        {return -3;}

        *codepoint = ((uint32_t)(b0 & 0x1F) << 6) |
                     ((uint32_t)(b1 & 0x3F));

        *used = 2;
        return 0;
    }

    /*
        3 字节:1110xxxx 10xxxxxx 10xxxxxx
    */
    if (b0 >= 0xE0 && b0 <= 0xEF)
    {
        uint8_t b1;
        uint8_t b2;

        if (len < 3)
        {return -2;}

        b1 = data[1];
        b2 = data[2];

        if (!is_utf8_continuation(b1) || !is_utf8_continuation(b2))
        {return -3;}

        /*
            防止过短编码:E0 后面的第二字节必须 >= A0
        */
        if (b0 == 0xE0 && b1 < 0xA0)
        {return -4;}

        /*
            UTF-16 代理区 U+D800 ~ U+DFFF 不允许出现在 UTF-8 中。ED A0 80 ~ ED BF BF 对应代理区,需要排除。*/
        if (b0 == 0xED && b1 >= 0xA0)
        {return -5;}

        *codepoint = ((uint32_t)(b0 & 0x0F) << 12) |
                     ((uint32_t)(b1 & 0x3F) << 6) |
                     ((uint32_t)(b2 & 0x3F));

        *used = 3;
        return 0;
    }

    /*
        4 字节:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    */
    if (b0 >= 0xF0 && b0 <= 0xF4)
    {
        uint8_t b1;
        uint8_t b2;
        uint8_t b3;

        if (len < 4)
        {return -2;}

        b1 = data[1];
        b2 = data[2];
        b3 = data[3];

        if (!is_utf8_continuation(b1) ||
            !is_utf8_continuation(b2) ||
            !is_utf8_continuation(b3))
        {return -3;}

        /*
            F0 后面的第二字节必须 >= 90,防止过短编码。*/
        if (b0 == 0xF0 && b1 < 0x90)
        {return -4;}

        /*
            Unicode 最大合法码点是 U+10FFFF。F4 后面的第二字节不能超过 8F。*/
        if (b0 == 0xF4 && b1 > 0x8F)
        {return -6;}

        *codepoint = ((uint32_t)(b0 & 0x07) << 18) |
                     ((uint32_t)(b1 & 0x3F) << 12) |
                     ((uint32_t)(b2 & 0x3F) << 6) |
                     ((uint32_t)(b3 & 0x3F));

        *used = 4;
        return 0;
    }

    return -7;
}

void parse_utf8(const uint8_t *data, size_t len)
{
    size_t i = 0;

    while (i < len)
    {
        uint32_t codepoint = 0;
        size_t used = 0;
        int ret;

        ret = utf8_decode_one(&data[i], len - i, &codepoint, &used);

        if (ret == 0)
        {
            printf("UTF-8: offset = %lu, used = %lu, Unicode = U+%04lX\n",
                   (unsigned long)i,
                   (unsigned long)used,
                   (unsigned long)codepoint);

            i += used;
        }
        else
        {
            printf("UTF-8 error: offset = %lu, byte = 0x%02X, ret = %d\n",
                   (unsigned long)i,
                   data[i],
                   ret);

            i++;
        }
    }
}

int main(void)
{
    /*
        "A 中国" 的 UTF-8 编码:A    = 41
        中   = E4 B8 AD
        国   = E5 9B BD
    */
    uint8_t text[] = {
        0x41,
        0xE4, 0xB8, 0xAD,
        0xE5, 0x9B, 0xBD
    };

    parse_utf8(text, sizeof(text));

    return 0;
}

输出结果类似:

UTF-8: offset = 0, used = 1, Unicode = U+0041
UTF-8: offset = 1, used = 3, Unicode = U+4E2D
UTF-8: offset = 4, used = 3, Unicode = U+56FD

7. GB2312、GBK、UTF-8 编码范围总结

编码 ASCII 范围 中文字节数 主要范围 说明
ASCII 00 ~ 7F 不支持中文 00 ~ 7F 基础英文字符
GB2312 00 ~ 7F 2 字节 A1A1 ~ F7FE 早期简体中文编码
GBK 00 ~ 7F 2 字节 8140 ~ FEFE,尾字节排除 7F GB2312 扩展,支持更多汉字
UTF-8 00 ~ 7F 中文常见 3 字节 1 ~ 4 字节变长 现代互联网主流编码

8. 三种编码解析过程对比

8.1 GB2312

byte <= 0x7F
    → ASCII

0xA1 <= byte <= 0xF7
    → 读取下一个字节
    → 判断第二字节是否在 0xA1 ~ 0xFE
    → 组成 GB2312 字符 

8.2 GBK

byte <= 0x7F
    → ASCII

0x81 <= byte <= 0xFE
    → 读取下一个字节
    → 判断第二字节是否在 0x40 ~ 0xFE,并且不等于 0x7F
    → 组成 GBK 字符 

8.3 UTF-8

0xxxxxxx
    → 1 字节 ASCII

110xxxxx
    → 2 字节 UTF-8

1110xxxx
    → 3 字节 UTF-8

11110xxx
    → 4 字节 UTF-8

10xxxxxx
    → 后续字节,不能单独作为字符开头 

9. 同一个字符在不同编码下的表现

以“中”为例:

字符 Unicode GB2312 GBK UTF-8
U+4E2D D6 D0 D6 D0 E4 B8 AD

以“芃”为例:

字符 GB2312 GBK UTF-8
不支持 C6 4D E8 8A 83

所以,如果设备只支持 GB2312 字库,那么“芃”这种字可能无法正常显示。


10. 嵌入式场景中的实际处理方式

在嵌入式项目里,经常会遇到几种情况。


10.1 串口收到 GBK 字符串

如果上位机发的是 GBK,那么 MCU 可以按 GBK 规则解析。

 串口数据
  ↓
判断 ASCII / GBK 双字节
  ↓
如果是 ASCII,直接显示
  ↓
如果是 GBK,查 GBK 字库
  ↓
读取点阵数据
  ↓
显示到 LCD / OLED

10.2 使用 GB2312 点阵字库

很多老式中文字库,比如 HZK16、HZK24,经常使用 GB2312 区位码方式索引。

以 HZK16 为例:

 每个汉字 16x16 点阵
每个点 1 bit
总字节数 = 16 * 16 / 8 = 32 字节 

偏移计算:

offset = ((area - 1) * 94 + (pos - 1)) * 32;

其中:

area = high_byte - 0xA0;
pos  = low_byte  - 0xA0;

示例代码:

#include <stdio.h>
#include <stdint.h>

#define HZK16_BYTES_PER_CHAR 32

long gb2312_hzk16_offset(uint8_t high, uint8_t low)
{
    int area;
    int pos;
    long offset;

    if (high < 0xA1 || high > 0xF7)
    {return -1;}

    if (low < 0xA1 || low > 0xFE)
    {return -1;}

    area = high - 0xA0;
    pos  = low  - 0xA0;

    offset = ((long)(area - 1) * 94 + (pos - 1)) * HZK16_BYTES_PER_CHAR;

    return offset;
}

int main(void)
{
    /*“中”= D6 D0
    */
    long offset = gb2312_hzk16_offset(0xD6, 0xD0);

    printf("offset = %ld\n", offset);

    return 0;
}

10.3 UTF-8 转 GBK / GB2312

如果网络、JSON、网页传过来的是 UTF-8,而屏幕字库是 GBK 或 GB2312,那么中间需要转换:

UTF-8 字节流
  ↓
解析成 Unicode 码点
  ↓
Unicode 转 GBK / GB2312
  ↓
查 GBK / GB2312 字库
  ↓
显示 

这里有一个重点:

UTF-8 和 GBK 之间不能只靠公式转换。

需要映射表:

Unicode → GBK
GBK → Unicode

因为 GBK 编码和 Unicode 码点不是简单的数学关系。


11. 如何判断一段数据是 GBK 还是 UTF-8?

这个问题比较麻烦。

因为单纯靠字节流,有时不能 100% 判断。

例如:

D6 D0

在 GBK 中是:

但在 UTF-8 中:

D6 D0

不是合法的 UTF-8 字符。

所以这种情况可以判断它更像 GBK。

但是有些字节组合可能同时满足某种编码规则,尤其是包含大量 ASCII 时,不一定能准确判断。

实际项目中更推荐:

 协议里明确编码方式
文件头或配置里明确编码方式
上位机和下位机约定统一编码 

不要靠猜。


12. 实际项目建议

12.1 新项目优先使用 UTF-8

如果是新项目,尤其涉及:

 网络通信
HTTP
MQTT
JSON
Web
数据库
跨平台软件 

建议统一使用 UTF-8。


12.2 屏幕点阵字库可以继续用 GB2312 / GBK

如果是低资源 MCU,显示中文点阵,尤其是使用已有字库文件:

HZK16
HZK24
GB2312 点阵字库
GBK 点阵字库 

那么 GB2312 / GBK 仍然很实用。


12.3 最好在系统边界做一次转换

例如:

 网络层:UTF-8
内部逻辑:Unicode / UTF-8
显示层:GB2312 / GBK 字库 

可以设计成:

UTF-8 输入
  ↓
解析为 Unicode
  ↓
查 Unicode → GBK 映射表
  ↓
得到 GBK 编码
  ↓
查 GBK 字库
  ↓
显示 

这样架构会更清晰。


13. 总结

GB2312、GBK、UTF-8 的核心区别可以概括为:

GB2312:早期简体中文编码,范围较小
GBK:GB2312 的扩展,支持更多中文字符
UTF-8:Unicode 的编码方式,现代通用编码,支持全球字符 

解析规则可以简单记为:

GB2312:ASCII 单字节
    中文双字节:A1~F7 + A1~FE

GBK:ASCII 单字节
    中文双字节:81~FE + 40~FE,排除 7F

UTF-8:根据首字节高位判断 1~4 字节长度
    后续字节必须是 10xxxxxx

在嵌入式和中文字库场景中,最重要的是:

 先确认输入数据是什么编码
再按对应编码规则解析
最后根据字库格式查表显示 

不要把:

Unicode
UTF-8
GB2312
GBK

混为一谈。

它们解决的问题不一样:

Unicode:字符编号
UTF-8:Unicode 的字节编码方式
GB2312:早期简体中文编码
GBK:GB2312 的扩展编码 

搞清楚这层关系,中文乱码、串口解析、点阵字库显示这些问题就会清晰很多。

正文完
 0
lircs
版权声明:本站原创文章,由 lircs 于2026-05-05发表,共计9435字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码