☰
纯C语言手搓UTF-8处理函数:嵌入式开发中的字符编码实战
2026/10/22 15:54:43 网站建设 项目流程

1. 为什么要在C语言里手搓UTF-8处理函数

做嵌入式或者偏底层的开发,迟早会碰到一个绕不开的问题:字符串里出现了中文。很多从纯ASCII环境过来的开发者,第一次遇到中文乱码时,第一反应是“加个库吧”,但现实情况往往是——项目跑在资源受限的单片机上,或者编译环境根本不支持引入第三方依赖,甚至连标准库的某些函数都被裁剪掉了。这时候,你只能自己动手。

我这次要聊的,就是在一个完全不引入任何第三方库的前提下,用纯C语言实现一套UTF-8字符串的处理工具函数。核心目标很明确:能正确识别UTF-8编码的字符边界、能统计实际字符数(而不是字节数)、能安全地截取子串、能完成UTF-8与码点之间的转换。这些函数看起来简单,但真正写起来,坑比想象中多得多。

这套东西适合谁?如果你正在做嵌入式开发、协议解析、日志系统,或者任何需要手动处理多字节字符的场景,这篇文章里的思路和代码可以直接拿去用。即使你平时用高级语言,理解UTF-8的底层编码规则,也能帮你在遇到乱码问题时快速定位原因,而不是只会“换个编码试试”。

先说清楚一个前提:UTF-8是一种变长编码,一个字符可能占1到4个字节。ASCII字符占1字节,拉丁扩展、希腊字母等占2字节,中文、日文、韩文等常用字符占3字节,emoji和一些生僻字占4字节。这个规则决定了,所有基于字节的字符串操作(比如strlen、strcpy、strstr)在处理UTF-8时都可能出问题。strlen返回的是字节数,不是字符数;按字节截取可能把一个多字节字符切成两半,产生无效序列。我们要做的,就是绕过这些陷阱。

2. UTF-8编码规则回顾与核心设计思路

2.1 编码规则速查:从字节反推字符长度

在动手写代码之前,必须把UTF-8的编码规则吃透。很多人觉得这部分枯燥,但我可以负责任地说,规则记不牢,后面写出来的判断逻辑一定是错的。UTF-8的编码规则可以用一句话概括:根据首字节的高位模式,判断这个字符总共占几个字节。

具体规则如下表所示:

首字节范围总字节数后续字节范围码点范围
0x00-0x7F1无U+0000 - U+007F
0xC0-0xDF20x80-0xBFU+0080 - U+07FF
0xE0-0xEF30x80-0xBFU+0800 - U+FFFF
0xF0-0xF740x80-0xBFU+10000 - U+10FFFF

这里有几个关键点需要特别注意。第一,后续字节的高两位必须是10,也就是范围在0x80到0xBF之间。这个特征是判断一个字节是否为“续字节”的唯一依据。第二,首字节的高位模式决定了长度:110开头是2字节,1110开头是3字节,11110开头是4字节。第三,0xC0和0xC1这两个首字节是无效的,因为它们会导致过长的编码(overlong encoding),实际码点小于0x80却用了2字节表示,这是不允许的。

注意:在实际解析中,遇到0xC0或0xC1开头的字节,应该直接判定为非法序列。同样,0xF5到0xFF的首字节也是非法的,因为Unicode码点最大只到U+10FFFF。

2.2 为什么不直接用标准库的字符串函数

有人可能会问:标准库里有mbrtowc、mbsrtowcs这些多字节转换函数,为什么还要自己写?原因有几个。首先,这些函数依赖locale设置,而很多嵌入式环境的locale支持是残缺的,甚至根本没有。其次,mbrtowc的行为在不同平台上存在差异,尤其是在处理非法序列时,返回值和处理方式不统一。第三,自己实现意味着完全可控,可以针对特定场景做优化,比如不需要完整的码点转换,只需要快速跳过字符边界。

我个人的经验是,在资源受限的环境里,自己写一套精简的UTF-8工具函数,代码量通常不超过300行,编译后占用的空间极小,而且行为完全可预测。相比之下,引入一个第三方库可能带来几百KB的体积增加,还要处理许可证、移植性等问题。

2.3 整体函数规划:需要哪些工具函数

基于实际使用场景,我规划了以下几个核心函数:

  • utf8_char_len:给定首字节,返回该字符的总字节数,非法首字节返回0。
  • utf8_validate:验证一个字节序列是否为合法的UTF-8字符串。
  • utf8_strlen:返回字符串中的实际字符数,而非字节数。
  • utf8_substr:按字符位置截取子串,避免切断多字节字符。
  • utf8_encode:将Unicode码点编码为UTF-8字节序列。
  • utf8_decode:从UTF-8字节序列中解码出一个码点。

这几个函数覆盖了绝大多数日常需求。接下来我会逐个拆解实现细节,并解释每个判断背后的逻辑。

3. 核心工具函数的实现与细节剖析

3.1 首字节判断:utf8_char_len的实现

这个函数是整个工具集的基础,它的输入是一个字节,输出是该字符的总字节数。实现逻辑非常直接,就是根据首字节的高位模式做判断。

#include <stdint.h> #include <stddef.h> /* * 根据UTF-8首字节返回字符的总字节数 * 非法首字节返回0 */ int utf8_char_len(uint8_t byte) { if (byte < 0x80) { return 1; } else if ((byte & 0xE0) == 0xC0) { return 2; } else if ((byte & 0xF0) == 0xE0) { return 3; } else if ((byte & 0xF8) == 0xF0) { return 4; } return 0; }

这里的位运算需要解释一下。byte & 0xE0保留高3位,判断是否等于0xC0(二进制11000000),这正好匹配110开头的模式。同理,byte & 0xF0保留高4位,判断是否等于0xE0(11100000),匹配1110开头。byte & 0xF8保留高5位,判断是否等于0xF0(11110000),匹配11110开头。

实操心得:这个函数只检查首字节,不验证后续字节是否合法。在实际使用中,我通常会在外层再做一次完整验证,避免处理到损坏的数据。另外,0xC0和0xC1虽然匹配110模式,但它们是非法首字节,严格来说应该排除。如果你的数据来源不可靠,建议加上byte >= 0xC2的判断。

3.2 完整序列验证:utf8_validate的边界处理

验证函数需要遍历整个字节序列,对每个字符检查首字节和后续字节的合法性。这里有几个容易出错的边界情况:字符串末尾不完整的字符、续字节出现在错误位置、以及过长的编码。

/* * 验证UTF-8字符串的合法性 * 合法返回1,非法返回0 */ int utf8_validate(const uint8_t *str, size_t len) { size_t i = 0; while (i < len) { uint8_t byte = str[i]; int char_len = utf8_char_len(byte); if (char_len == 0) { return 0; } /* 检查剩余字节是否足够 */ if (i + char_len > len) { return 0; } /* 检查后续字节是否都在0x80-0xBF范围内 */ for (int j = 1; j < char_len; j++) { if ((str[i + j] & 0xC0) != 0x80) { return 0; } } /* 检查过长的编码 */ if (char_len == 2 && byte < 0xC2) { return 0; } if (char_len == 3) { if (byte == 0xE0 && str[i + 1] < 0xA0) return 0; if (byte == 0xED && str[i + 1] > 0x9F) return 0; } if (char_len == 4) { if (byte == 0xF0 && str[i + 1] < 0x90) return 0; if (byte == 0xF4 && str[i + 1] > 0x8F) return 0; if (byte > 0xF4) return 0; } i += char_len; } return 1; }

这段代码里有几个细节值得展开说。首先是i + char_len > len的判断,这处理了字符串末尾被截断的情况。比如一个3字节字符只剩2个字节,这个判断会直接返回非法。

其次是过长的编码检查。char_len == 2 && byte < 0xC2排除了0xC0和0xC1。对于3字节字符,0xE0开头的字符,第二个字节必须大于等于0xA0,否则就是过长编码。0xED开头的字符,第二个字节必须小于等于0x9F,这是为了避免编码到UTF-16的代理区(U+D800到U+DFFF),这些码点在UTF-8中是非法的。

对于4字节字符,0xF0开头的第二个字节必须大于等于0x90,0xF4开头的第二个字节必须小于等于0x8F,且首字节不能大于0xF4。这些限制确保了码点不超过U+10FFFF。

注意:很多简化版的验证函数会忽略这些边界检查,导致接受一些非法序列。如果你的数据要用于网络传输或持久化存储,建议保留这些检查,避免后续处理出现意外。

3.3 字符计数:utf8_strlen与字节计数的区别

utf8_strlen的实现非常直接,就是遍历字符串,每次跳过一个字符的长度,计数器加一。

/* * 返回UTF-8字符串的字符数(非字节数) * 遇到非法序列时停止计数 */ size_t utf8_strlen(const uint8_t *str, size_t len) { size_t count = 0; size_t i = 0; while (i < len) { int char_len = utf8_char_len(str[i]); if (char_len == 0 || i + char_len > len) { break; } count++; i += char_len; } return count; }

这个函数和strlen的区别,用一个例子就能说明白。字符串“你好”的UTF-8编码是6个字节,strlen返回6,而utf8_strlen返回2。在需要按字符对齐显示、计算光标位置、或者做文本截断时,必须用字符数而不是字节数。

实操心得:我在日志系统里踩过一个坑。当时用strlen计算日志行的长度来做缓冲区分配,结果遇到中文日志时,分配的空间是字节数,但后续按字符处理时越界了。后来统一改成utf8_strlen,问题才解决。这个教训是:只要字符串里可能出现非ASCII字符,就不要用strlen做逻辑判断。

3.4 安全截取:utf8_substr的实现与边界回退

按字符截取子串是实际项目中最常用的功能之一。比如显示摘要时,需要截取前N个字符,而不是前N个字节。实现思路是找到第N个字符的起始字节位置,然后复制对应的字节数。

/* * 从str中截取前char_count个字符,结果存入dst * dst_size为dst的缓冲区大小 * 返回实际复制的字节数,失败返回0 */ size_t utf8_substr(const uint8_t *str, size_t len, size_t char_count, uint8_t *dst, size_t dst_size) { size_t i = 0; size_t chars = 0; size_t start = 0; while (i < len && chars < char_count) { int char_len = utf8_char_len(str[i]); if (char_len == 0 || i + char_len > len) { break; } i += char_len; chars++; } size_t byte_len = i - start; if (byte_len + 1 > dst_size) { return 0; } for (size_t j = 0; j < byte_len; j++) { dst[j] = str[start + j]; } dst[byte_len] = '\0'; return byte_len; }

这个函数的关键在于,它只会在字符边界处停止,不会把一个多字节字符切成两半。如果字符串长度不足char_count个字符,就复制全部内容。如果目标缓冲区不够大,返回0表示失败。

注意:dst_size必须包含结尾的\0空间。我在实际使用中,通常会把dst_size设置为char_count * 4 + 1,因为UTF-8单个字符最多占4字节,这样能保证任何情况下都不会溢出。

3.5 码点编解码:utf8_encode与utf8_decode

这两个函数用于在Unicode码点和UTF-8字节序列之间转换。utf8_encode把码点写入字节数组,utf8_decode从字节数组读出码点。

/* * 将Unicode码点编码为UTF-8,结果存入buf * 返回写入的字节数,非法码点返回0 */ int utf8_encode(uint32_t codepoint, uint8_t *buf) { if (codepoint <= 0x7F) { buf[0] = (uint8_t)codepoint; return 1; } else if (codepoint <= 0x7FF) { buf[0] = 0xC0 | (codepoint >> 6); buf[1] = 0x80 | (codepoint & 0x3F); return 2; } else if (codepoint <= 0xFFFF) { if (codepoint >= 0xD800 && codepoint <= 0xDFFF) { return 0; /* 代理区,非法 */ } buf[0] = 0xE0 | (codepoint >> 12); buf[1] = 0x80 | ((codepoint >> 6) & 0x3F); buf[2] = 0x80 | (codepoint & 0x3F); return 3; } else if (codepoint <= 0x10FFFF) { buf[0] = 0xF0 | (codepoint >> 18); buf[1] = 0x80 | ((codepoint >> 12) & 0x3F); buf[2] = 0x80 | ((codepoint >> 6) & 0x3F); buf[3] = 0x80 | (codepoint & 0x3F); return 4; } return 0; } /* * 从UTF-8字节序列解码出一个码点 * 返回消耗的字节数,非法序列返回0 */ int utf8_decode(const uint8_t *str, size_t len, uint32_t *codepoint) { if (len == 0) return 0; uint8_t byte = str[0]; int char_len = utf8_char_len(byte); if (char_len == 0 || (size_t)char_len > len) { return 0; } uint32_t cp = 0; if (char_len == 1) { cp = byte; } else if (char_len == 2) { cp = byte & 0x1F; } else if (char_len == 3) { cp = byte & 0x0F; } else { cp = byte & 0x07; } for (int i = 1; i < char_len; i++) { if ((str[i] & 0xC0) != 0x80) { return 0; } cp = (cp << 6) | (str[i] & 0x3F); } *codepoint = cp; return char_len; }

编码的逻辑是解码的逆过程。解码时,先根据首字节确定长度,然后从首字节中提取有效位,再依次从后续字节中提取6位,拼接成完整码点。编码时,根据码点范围确定字节数,然后按位拆分填入对应的字节中。

实操心得:utf8_decode返回消耗的字节数,这样调用者可以方便地遍历字符串。如果返回0,表示遇到非法序列,调用者可以选择跳过1个字节继续,或者直接报错。我在解析网络协议时,通常选择跳过1个字节继续解析,这样即使数据中间有损坏,也能尽量恢复后续内容。

4. 完整实操流程与测试验证

4.1 搭建测试环境与编译配置

这套代码不依赖任何第三方库,只需要标准的C编译环境。我平时用gcc做快速验证,编译命令很简单:

gcc -std=c99 -Wall -Wextra -O2 -o utf8_test utf8_test.c

-std=c99确保使用C99标准,-Wall -Wextra打开所有警告,-O2开启优化。在嵌入式环境里,我通常会用-Os优化体积,并加上-ffunction-sections -fdata-sections配合链接器的--gc-sections来剔除未使用的函数。

测试代码需要覆盖以下几类用例:纯ASCII字符串、2字节字符(如希腊字母)、3字节字符(如中文)、4字节字符(如emoji)、混合字符串、非法序列、截断序列。我写了一个简单的测试框架,用宏来断言结果。

#include <stdio.h> #include <string.h> #include <assert.h> #define TEST_ASSERT(cond, msg) do { \ if (!(cond)) { \ printf("FAIL: %s\n", msg); \ return 1; \ } \ } while(0) int main(void) { /* 测试utf8_char_len */ TEST_ASSERT(utf8_char_len('A') == 1, "ASCII length"); TEST_ASSERT(utf8_char_len(0xC3) == 2, "2-byte length"); TEST_ASSERT(utf8_char_len(0xE4) == 3, "3-byte length"); TEST_ASSERT(utf8_char_len(0xF0) == 4, "4-byte length"); TEST_ASSERT(utf8_char_len(0x80) == 0, "invalid continuation"); /* 测试utf8_strlen */ const uint8_t *s1 = (const uint8_t *)"hello"; TEST_ASSERT(utf8_strlen(s1, strlen((const char *)s1)) == 5, "ASCII strlen"); const uint8_t *s2 = (const uint8_t *)"你好世界"; TEST_ASSERT(utf8_strlen(s2, strlen((const char *)s2)) == 4, "Chinese strlen"); /* 测试utf8_validate */ TEST_ASSERT(utf8_validate(s1, strlen((const char *)s1)) == 1, "valid ASCII"); TEST_ASSERT(utf8_validate(s2, strlen((const char *)s2)) == 1, "valid Chinese"); uint8_t invalid[] = {0xE4, 0xBD, 0x80, 0x80}; TEST_ASSERT(utf8_validate(invalid, 4) == 0, "invalid sequence"); /* 测试utf8_substr */ uint8_t buf[64]; size_t n = utf8_substr(s2, strlen((const char *)s2), 2, buf, sizeof(buf)); TEST_ASSERT(n == 6, "substr byte count"); TEST_ASSERT(memcmp(buf, "你好", 6) == 0, "substr content"); /* 测试utf8_encode/decode */ uint8_t enc[4]; int elen = utf8_encode(0x4F60, enc); TEST_ASSERT(elen == 3, "encode length"); uint32_t cp; int dlen = utf8_decode(enc, 3, &cp); TEST_ASSERT(dlen == 3, "decode length"); TEST_ASSERT(cp == 0x4F60, "decode codepoint"); printf("All tests passed.\n"); return 0; }

4.2 关键测试用例与预期结果

为了让你更直观地理解每个函数的行为,我把几个典型用例的输入输出整理成表格:

测试项输入预期输出说明
utf8_char_len0x41 ('A')1ASCII字符
utf8_char_len0xC322字节首字节
utf8_char_len0xE433字节首字节
utf8_char_len0xF044字节首字节
utf8_char_len0x800续字节,非法首字节
utf8_strlen"hello"5纯ASCII
utf8_strlen"你好"2两个3字节字符
utf8_validate{0xE4,0xBD,0x80}1合法3字节序列
utf8_validate{0xE4,0xBD}0截断序列
utf8_substr"你好世界", 2"你好"截取前2字符
utf8_encode0x4F603字节编码为E4 BD A0
utf8_decodeE4 BD A00x4F60解码回码点

这些用例覆盖了正常流程和主要的边界情况。在实际项目中,我还会增加压力测试,比如随机生成大量合法和非法序列,验证函数的稳定性和性能。

4.3 性能考量与优化建议

这套函数的性能瓶颈主要在utf8_strlen和utf8_validate上,因为它们需要遍历整个字符串。在PC环境下,处理几MB的文本毫无压力。但在嵌入式环境里,如果字符串很长且调用频繁,可以考虑以下优化:

第一,对于utf8_strlen,如果字符串只包含ASCII字符,可以用strlen快速判断。具体做法是先调用strlen,然后检查返回的字节数是否等于字符数。如果相等,说明全是ASCII,直接返回。这个技巧在日志处理中特别有用,因为大部分日志行是纯ASCII的。

第二,对于utf8_validate,可以在解析协议时边解析边验证,避免单独遍历一遍。比如在读取网络包时,每读一个字段就验证一次,而不是等整个包收完再验证。

第三,如果目标平台支持,可以用查表法替代位运算判断首字节长度。一个256字节的查找表,每个元素存储对应的字符长度,这样utf8_char_len就变成了一次数组访问。不过在现代编译器上,位运算版本已经足够快,查表法的优势主要体现在老旧的8位单片机上。

注意:优化之前一定要先测量。我见过不少项目,开发者凭感觉优化,结果引入了bug,性能却没有明显提升。先用clock()或者硬件定时器测量热点函数的耗时,再决定是否优化。

5. 常见问题与排查技巧实录

5.1 乱码问题的排查思路

遇到乱码时,不要急着改代码,先确认数据的实际字节内容。我常用的方法是把字符串的每个字节以十六进制打印出来,然后对照UTF-8编码表分析。

void dump_hex(const uint8_t *str, size_t len) { for (size_t i = 0; i < len; i++) { printf("%02X ", str[i]); } printf("\n"); }

比如“你好”的正确编码是E4 BD A0 E5 A5 BD。如果打印出来是C4 E3 BA C3,那说明数据源用的是GBK编码,不是UTF-8。如果打印出来是E4 BD A0 E5 A5,少了一个字节,说明数据在传输或存储过程中被截断了。

排查步骤可以总结为:先看字节序列是否符合UTF-8的编码模式,再看是否有截断或多余字节,最后检查数据源和目标的编码设置是否一致。

5.2 缓冲区溢出的预防措施

处理UTF-8字符串时,缓冲区溢出是最危险的问题。因为一个字符可能占4个字节,如果按字符数分配缓冲区,很容易不够。我的原则是:任何接收UTF-8字符串的缓冲区,大小都按字符数 * 4 + 1来分配。如果无法预知字符数,就按字节数分配,并在复制时严格检查边界。

utf8_substr函数里的byte_len + 1 > dst_size判断就是最后一道防线。但更好的做法是在调用前就确保缓冲区足够大。我通常会在函数文档里明确写出缓冲区大小的要求,并在调试版本里加上断言。

5.3 跨平台兼容性注意事项

char类型在不同平台上可能是有符号的,也可能是无符号的。在处理UTF-8字节时,建议统一使用uint8_t,避免符号扩展带来的问题。比如str[i] & 0xC0,如果str[i]是有符号的char且值为0xE4,符号扩展后会变成0xFFFFFFE4,与0xC0做位运算的结果就不对了。

另外,size_t在32位和64位平台上的长度不同,打印时要用%zu而不是%d。这些细节在跨平台移植时经常引发难以排查的bug。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
中文显示为乱码数据源非UTF-8编码打印字节序列转换编码或确认数据源
字符计数偏大用了strlen而非utf8_strlen检查计数函数替换为utf8_strlen
截取后末尾乱码按字节截断检查截取位置使用utf8_substr
验证函数返回非法数据被截断检查字节数补全数据或跳过
4字节字符处理错误首字节判断遗漏检查utf8_char_len补充0xF0分支
缓冲区溢出按字符数分配空间检查分配大小按字节数*4+1分配

实操心得:我在一个串口通信项目里遇到过一个问题,接收到的中文偶尔会变成乱码。排查后发现,串口中断是按字节触发的,而我的解析代码在缓冲区还没收完一个完整字符时就开始处理了。后来改成先判断缓冲区里是否有完整的UTF-8字符,再进行处理,问题就消失了。这个经验说明,UTF-8处理必须考虑数据流的边界,不能假设一次就能拿到完整字符。

6. 实际项目中的扩展应用

6.1 在协议解析中的应用

在很多自定义二进制协议里,字符串字段通常用长度前缀加UTF-8内容的方式编码。解析时,先读取长度字段,然后验证接下来的字节是否是合法的UTF-8序列。如果验证失败,可以选择丢弃整个包,或者记录错误后继续解析后续字段。

我通常会把utf8_validate和utf8_strlen组合使用:先用utf8_validate确认数据合法,再用utf8_strlen获取字符数用于显示或存储。这样即使遇到恶意构造的数据,也不会导致程序崩溃。

6.2 在日志系统中的应用

日志系统经常需要按字符截断过长的消息。比如限制每条日志最多显示50个字符,超出的部分用省略号代替。这时候utf8_substr就派上用场了。需要注意的是,省略号本身也占字符,所以实际截取的长度要减去省略号的长度。

另外,日志的格式化输出也要注意。printf的%s是按字节输出的,对于UTF-8字符串来说通常没问题,因为终端会自动识别。但如果要对齐列宽,就必须用字符数而不是字节数来计算填充空格的数量。

6.3 在嵌入式显示中的应用

在LCD或OLED屏幕上显示中文时,通常需要先取字模。字模的索引是Unicode码点,所以需要先用utf8_decode把UTF-8序列转换成码点,再根据码点查找字模数据。这个流程在嵌入式GUI里非常常见。

如果屏幕缓冲区有限,还需要做分页显示。这时候utf8_substr可以用来截取当前页需要显示的字符,避免一次性加载全部内容。

6.4 与宽字符函数的对比

C标准库提供了wchar_t和相关的宽字符函数,但在嵌入式环境里,wchar_t的宽度不确定(可能是2字节或4字节),而且宽字符函数的实现往往依赖locale,移植性差。相比之下,UTF-8工具函数完全自包含,行为可预测,更适合资源受限和跨平台的场景。

当然,如果你的项目已经在用宽字符,也没必要强行替换。关键是理解两者的适用场景:宽字符适合在内存中统一处理,UTF-8适合在存储和传输中使用。很多系统会在内部用宽字符,在I/O边界做转换。

7. 我在这套工具函数上踩过的坑

第一个坑是忽略了char的符号问题。早期版本里,我用char来接收字节,在x86上测试没问题,移植到ARM平台后,遇到高位为1的字节时,符号扩展导致判断全部失效。后来统一改成uint8_t,问题才解决。这个教训是:处理二进制数据时,永远不要用char,用uint8_t或unsigned char。

第二个坑是验证函数写得太宽松。最初版本的utf8_validate只检查了首字节和续字节的模式,没有检查过长编码和代理区。结果在处理一些第三方接口返回的数据时,接受了非法序列,后续解码出了错误的码点。后来补上了边界检查,才彻底解决。

第三个坑是utf8_substr的缓冲区大小计算。有一次调用时,我传的dst_size是字符数而不是字节数,导致缓冲区溢出,覆盖了相邻的变量。幸好是在调试阶段发现的,没有造成严重后果。从那以后,我在所有涉及缓冲区的函数里都加上了断言,并且在文档里用醒目的方式标注缓冲区大小的要求。

第四个坑是性能问题。在一个高频调用的日志函数里,我每次都调用utf8_strlen来计算字符数,导致CPU占用率偏高。后来加了一个快速路径:先用strlen获取字节数,如果字节数小于128或者等于字符数,就直接返回,避免了完整遍历。这个优化把日志函数的耗时降低了大约40%。

这套UTF-8工具函数看起来简单,但真正写好、用对,需要对编码规则有扎实的理解,并且在实践中不断打磨。希望这些代码和经验能帮你少走一些弯路。如果你也在做类似的事情,欢迎交流你的实现思路和踩坑经历。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询