c语言实现utf-8编码解码器

这个例程是在书上看到的,感觉很有意思。

简单介绍一下utf-8编码,utf-8编码是一种变长的编码方式,长度为1-4字节。

当码长为1字节的时候,兼容ascii编码,格式为0xxxxxxx (x处表示有效位)

当码长为2字节的时候,格式为110xxxxx 10xxxxxx

高字节的110表示码长为2字节,低字节的10为低字节标志位,下同

当码长为3字节的时候,格式为1110xxxx 10xxxxxx 10xxxxxx

当码长为4字节的时候,格式为11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

我们先来看一下头文件 unicodeUtf8.h

/*这段代码是utf8编码解码器头文件,在书p66*/
#ifndef _INCLUDE_UTF8_H
#define _INCLUDE_UTF8_H
#include <stdint.h>
uint32_t getByteNumOfEncodeUtf8(int value);
uint32_t getByteNumOfDecodeUtf8(uint8_t byte);
uint8_t encodeUtf8(uint8_t* buf, int value);
int decodeUtf8(const uint8_t* bytePtr, uint32_t length);
#endif

getByteNumOfEncodeUtf8 计算编码后的字节长度

getByteNumOfDecodeUtf8 计算解码后的字节长度

encodeUtf8 编码

decodeUtf8 解码

具体的实现代码

/*这段代码是utf8编码解码器具体实现代码,在书p61*/
#include "unicodeUtf8.h"
#include "common.h"

/*详细的ascii码编码规则见书P60*/

//返回value按照utf8编码后的字节数
uint32_t getByteNumOfEncodeUtf8(int value){
	ASSERT(value>0, "can't encode negative value!\n");  //只能对正数编码

	//单个ASCII码字符需要1个字节
	if(value <= 0x7f){
		return 1;
	}
	//此范围需要2字节
	if(value <= 0x7ff){
		return 2;
	}
	//此范围需要3字节
	if(value <= 0xffff){
		return 3;
	}
	//此范围需要4字节
	if(value <= 0x10ffff){
		return 4;
	}
	//超过范围返回0
	return 0;    
}

//把value编码为utf8后写入缓冲区buf,返回写入的字节数
//详细的解释在书p65
uint8_t encodeUtf8(uint8_t* buf, int value){
	ASSERT(value>0, "can't encode negative value!\n");  //只能对正数编码
	
	//按照大端字节序写入缓冲区(低地址对应高数据位)
	if(value <= 0x7f){
		*buf = value & 0x7f;   //讲value存入内存中
		return 1;
	}	
	else if(value <= 0x7ff){
		//这种情况下utf8格式为110xxxxx 10xxxxxx
		//先在低地址处写入高字节110xxxxx
		//11000000 | ((value & 0111 1100 0000)>>6)
		*buf++ = 0xc0 | ((value & 0x7c0)>>6);
		//再在高地址处写入低字节10xxxxxx
		//1000 0000 | (value & 0011 1111)
		*buf = 0x80 | (value & 0x3f);
		return 2;
	}
	else if(value <= 0xffff){
		//这种情况下utf8格式为1110xxxx 10xxxxxx 10xxxxxx
		//先在低地址写入高字节1110xxxx
		*buf++ = 0xe0 | ((value & 0xf0000)>>12);
		//然后在中地址写入中字节
		*buf++ = 0x80 | ((value & 0xfc0)>>6);
		//最后在高地址写入低字节
		*buf = 0x80 | (value & 0x3f);
		return 3;
	}
	else if(value <= 0x10ffff){
		//在这种情况下utf8格式为11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
		//先在低地址写入高字节11110xxx
		*buf++ = 0xf0 | ((value & 0x1c0000)>>18);
		//再在第二段地址写入第二段字节10xxxxxx
		*buf++ = 0x80 | ((value & 0x3f00)>>12);
		//然后在第三段地址写入第三段字节10xxxxxx
		*buf++ = 0x80 | ((value & 0xfc0)>>6);
		//最后在高地址写入低字节10xxxxxx
		*buf = 0x80 | (value & 0x3f);
		return 4;
	}
	
	NOT_REACHED();  //如果程序运行到这里就是错了
	return 0;
}

//返回解码utf8的字节数
uint32_t getByteNumOfDecodeUtf8(uint8_t byte){
	//byte应该是utf8的最高1字节,如果指向了utf8编码后面低字节部分则返回0
	if((byte & 0xc0)==0x80) return 0;  //1000 0000
	if((byte & 0xf8)==0xf0) return 4;  //1111 0000
	if((byte & 0xf0)==0xf0) return 3;  //1110 0000
	if((byte & 0xe0)==0xc0) return 2;  //1100 0000
	return 1;          //ASCII码
}

//解码以bytePtr为起始地址的utf8序列,其最大长度为length,若不是utf8序列就返回-1
int decodeUtf8(const uint8_t* bytePtr, uint32_t length){
	//若是1字节的ascii码: 0xxxxxxx
	if(*bytePtr <= 0x7f) return *bytePtr;
	
	int value;
	uint32_t remainingBytes;

	//先读取高1字节
	//根据高1字节的高n位判断相应字节数的utf8编码
	if((*bytePtr & 0xe0)==0xc0){
		//若是2字节的utf8
		value = *bytePtr & 0x1f; //记录后面的5位有效位
		remainingBytes = 1;
	}	
	else if((*bytePtr & 0xf0)==0xe0){
		//若是3字节的utf8
		value = *bytePtr & 0x0f; //记录后面的4位有效位
		remainingBytes = 2;
	}
	else if((*bytePtr & 0xf8)==0xf0){
		//若是4字节的utf8
		value = *bytePtr & 0x07;
		remainingBytes = 3;
	}
	else {return -1;} //非法编码
	
	//如果utf8被斩断了就不再读下去了
	if(remainingBytes > length - 1){return -1;}

	//再读取低字节中的数据
	while(remainingBytes > 0){
		bytePtr++;
		remainingBytes--;
		//高两位必须是10
		if((*bytePtr & 0xc0) != 0x80){return -1;}
		//从次高字节往低字节读
		value = value << 6 | (*bytePtr & 0x3f); //value左移6为写入6位有效位
	}
	return value;   //返回解码得到的value值
}

下面我就简单介绍一下编码和解码的算法

编码

我们以3字节码长为例, 当我们的value是12-16位二进制数的时候就应该编码成3字节

我们采用大端字节序,也就是数据位的高位存放在内存的低地址处,数据位的地位存放在内存的高地址处

所以我们先存高1字节,高1字节的格式为 1110 xxxx ,有4位有效为,这4位有效位是value的高4位

我们把value当16位二进制数来看

首先用 value & 0xf000 (也就是 1111 0000 0000 0000) 然后右移12位来得到高4位

之后用0xe0(即1110 0000)或 来得到头标志1110

编码第二个字节的时候也是同样的道理 我们要将value 的第7-12为写入编码中

首先 value & 0xfc0 (即1111 1100 0000)来获得7-12位 然后右移6位

之后用0x80 (即1000 0000)来得到次字节表示10

最后使用第6位写入编码

先用value & 0x3f ( 即0011 1111 )来获得低6位

然后使用0x80获得头10

解码

解码的时候我们先获取高1字节来判断后面有几个字节,然后再继续解码

如果只有一个字节那么它就是ascii码,直接返回即可

如果是两个字节,也就是说 (*bytePtr & 0xe0) == 0xc0 的时候我们还要继续往后读一个字节,此时最高位上有5位有效位

value = *bytePtr & 0x1f 得到5位有效位

三字节和四字节同理,并且有效位逐个减小一位

之说以用0xe0与是因为头标志为110,之后才是有效的数据位 

 在我们读后面的字节的时候用 (*bytePtr & 0xc0) == 0x80 来判断它是正确编码

然后读取第6位最为有效位写入value中

先 *bytePtr & 0x3f 来获取低6位, 然后value 左移6位再或上得到的低6为即可

参考书籍 《基于c语言自制编程语言》作者 郑钢

### 如何在不同编程语言中使用UTF-8编码 #### C语言中的UTF-8编码处理 在C语言里,为了有效地操作UTF-8编码的数据,程序应当能够识别并解析由多字节组成的字符序列。这涉及到读取输入流(可能是来自文件或网络连接),并将这些数据按照UTF-8的规定解码成对应的Unicode码点[^1]。 对于具体的实现细节,在获取到UTF-8编码的字节流之后,开发者可以根据每个字符的第一个字节来判断其长度,并据此提取完整的字符信息。例如: ```c #include <stdio.h> int main() { unsigned char byte; while ((byte = getchar()) != EOF) { if (byte & 0x80) { /* 如果最高位为1,则可能是一个多字节字符 */ int bytes_needed = 0; if ((byte & 0xE0) == 0xC0) bytes_needed = 2; // 2-byte sequence else if ((byte & 0xF0) == 0xE0) bytes_needed = 3; // 3-byte sequence else if ((byte & 0xF8) == 0xF0) bytes_needed = 4; // 4-byte sequence printf("Detected %d-byte character\n", bytes_needed); // 这里省略了实际读取剩余部分以及组合成完整unicode code point的过程... } else { putchar(byte); // 单字节ASCII字符直接输出 } } } ``` 这段代码展示了如何初步检测一个多字节的UTF-8字符,并打印出它所需的总字节数量。当然,这只是基础框架的一部分;要完成整个过程还需要进一步完善逻辑以正确地组装各个组成部分形成最终的Unicode值。 #### 使用UTF-8编码转换工具 除了手动编写解析函数外,还可以利用现有的库来进行更复杂的任务,比如批量修改多个文本文件的内容编码方式。有一个叫做“utf-8编码转换工具”的资源可以帮助用户轻松地把各种类型的文档转码至UTF-8格式而不丢失任何原始内容的信息完整性[^2]。此工具不仅限于特定的操作系统平台,而且适用于广泛的编程环境下的源代码文件调整工作。 #### Go语言内置对UTF-8的支持 Go作为一种现代化的语言,自诞生之初便选择了UTF-8作为默认字符串表示形式之一。这种决策背后有着深刻的原因:一方面是因为三位创造者中有两位参与过UTF-8的设计开发;另一方面则是考虑到当时互联网的发展趋势——即越来越多的应用和服务都在向统一采用UTF-8迈进。因此,在Go应用程序内部可以直接操纵基于UTF-8编码的文字串而无需额外安装第三方包或其他依赖项[^3]。 #### Unity环境下.CS文件设置UTF-8编码 当涉及游戏引擎如Unity时,确保所有的脚本(.cs扩展名)都遵循相同的编码标准是非常重要的。通过简单的配置更改可以让所有新创建或是已存在的.cs文件自动保存为不含BOM标记的UTF-8格式。具体做法是在项目的根目录下找到或者新建一个名为`.editorconfig`的文件,并加入如下几行定义[^4]: ```ini root = true [*.{cs}] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ``` 上述设定将会告诉IDE和其他编辑器应该如何对待该项目内的C#源代码文件,从而保持一致性和兼容性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值