C++截取定长utf8字符及字符集与字符编码总结

之前有个需求要求从一段utf8编码的文字中截取前n个字符,由于文字中包含既有中文又有数字字母等,这些字符所占的字节数都不相同,所以直接截取前M个字符可能会导致乱码问题。在完成截取之前,需要先了解utf8编码字符的存储规则。后面简单介绍一下常见的字符集及字符编码

utf8编码规则

在理解utf8编码规则之前先说一下utf16和utf32的编码规则,utf32规定所有的unicode字符集上的所有字符都是用32bit来存储,即可存储的字符数可达2^32个,接近40亿,这明显可以容纳已有的所有字符。utf16编码使用2个字节存储,超过此范围的使用其他特殊技巧处理。使用utf16和utf32有一个问题,就是会受到计算机存储大端规则和小端规则的影响。所以utf16又有utf16_LE和utf16_BE之分。为了解决这个问题,需要在文本文件的开头定义一个BOM(Byte order Mark),这是一个非打印标志,用此标志来标志当前文本存储是大端存储还是小端存储。

utf8是一种针对unicode的可变长度字符编码,也是一种前缀码,它可以表示unicode标准中的任何字符。且与ascii码兼容,这使得我们处理ascii字符的时候不许做特殊修改。

utf8使用1~6个字符为每个字符编码,但实际使用中只使用了4个。

utf8编码规则:

(1)对于单字节字符,字节的第一位为0,后面7位为这个字符的unicode码,因此对于ascii码,unicode与ascii保持一致。
(2)对于n字节符号(n>1),第一个字节的前n位都为1,第n+1位为0,其他字节的前两位都为10,其余位为实际的unicode位。

规则:

unicode范围 二进制表示范围
U-00000000 - U-0000007F 0xxxxxxx
U-00000080 - U-000007FF 110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

利用上述utf8编码规则实现C++截取定长字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const string utf8Cut(const string &src, int utf8Len) {
string ret;
int utf8LenCnt = 0;
int srcIdx = 0;
int srcLen = src.length();
int cutLen = 0;
unsigned char tmp;
while (utf8LenCnt < utf8Len && srcIdx < srcLen) {
tmp = (unsigned char)src[srcIdx];
if (tmp >= 252)
cutLen = 6;
else if (tmp >= 248)
cutLen = 5;
else if (tmp >= 240)
cutLen = 4;
else if (tmp >= 224)
cutLen = 3;
else if (tmp >= 192)
cutLen = 2;
else if (tmp >= 65 && tmp <=90)
cutLen = 1;
else
cutLen = 1;
ret += src.substr(srcIdx, cutLen);
srcIdx += cutLen;
++utf8LenCnt;
}
return ret;
}

字符集与字符编码

字符集是一个系统所支持的字符的集合,常见的字符集包括ascii字符集,GB2312字符集,GBK字符集以及unicode字符集。其中ascii字符集包括所有的英文字母数字以及控制字符等等。

字符编码是用来定义计算机中某个字符的存储和传输规则。

区别:字符集仅仅是一个字符集合,与计算机的存储和传输没有关系,而字符编码规定了计算机中字符的编码规则。

常见的字符集及字符编码

  • ascii字符集及字符编码:使用一个字符编码,包括所有的字母数字,换行以及控制字符等。
  • GB*字符集及编码:是一种多字节编码标准,GB表示国标,包括GB2312,GBK等编码,2312是标准号,GB2312涵盖了绝大多数的中文,但是一些生僻字不包含在其中,GBK是对GB2312的扩展.
  • BIG*:常见的为BIG5字符集及编码,BIG5中收录了大部分的中文繁体,常用语香港台湾等地区。
  • unicode字符集及编码:unicode又称为统一码,几乎涵盖了所有国家和地区的所有字符,并还在不断的扩充新字符。unicode是一个字符集,而不是编码规则,我们常见的utf8,utf16以及utf32是unicode常见的字符编码.

Comment and share

C中编码问题

in C++学习

C中编码问题

编码介绍

在代码中我们通常不可避免的出现一些中文,这个时候我们就要考虑到中文的编码格式,如果不注意可能会导致乱码或者信息失真等问题。我们常用的中文编码有GBK,gb2312,Unicode等等。具体详细的介绍看下面几篇文章:

C语言编码转换

在C语言中,如果需要讲编码进行转换,可以使用iconv系列函数。
头文件以及常用函数:

1
2
3
4
5
6
7
8
#include <iconv.h>
typedef void* iconv_t;
extern iconv_t iconv_open(const char* to_code, const char* from_code);
extern size_t iconv(iconv_t cd, char** restrict inbuf, size_t* in_left_buf, char** restrict outbuf, size_t* out_left_buf);
extern int iconv_close(iconv_t cd);

iconv_open

函数说明

此函数说明将要进行哪两种编码的转换,并返回一个转化句柄。

参数说明

  • tocode:目标编码
  • fromcode : 原编码

iconv

1
extern size_t iconv(iconv_t cd, char** restrict inbuf, size_t* in_left_buf, char** restrict outbuf, size_t* out_left_buf);

函数说明

此函数用于从inbuf中读取数据并将转换到指定编码的的数据输出到outbuf中,若转换成功,则输出本次转化的字节数,否则返回sizeof_t(-1)

参数说明

  • cd : 转换描述符,由iconv_open获得
  • inbuf:输入缓冲区
  • in_left_buf :输入缓冲区还未转换的字符数
  • outbuf : 输出缓冲区
  • out_len_buf:输出缓冲区的剩余空间.

iconv_close

1
extern int iconv_close(iconv_t cd);

用于关闭iconv_open打开的文件描述符

举例转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <string>
#include <iconv.h>
#include <cstring>
#include <errno.h>
using namespace std;
string convertCode(const string& p_str, const char* from, const char* to) {
char * sin, * sout;
int lenin, lenout, ret;
const int BUF_LEN = 10240;
char bufOut[BUF_LEN];
string result("");
memset(bufOut, 0x0, sizeof(bufOut));
iconv_t cd;
if ((cd = iconv_open(to, from)) == (iconv_t)(-1)) {
std::cout << "open iconv error" << std::endl;
return "";
}
lenin = p_str.length();
lenout = BUF_LEN;
sin = (char*)p_str.c_str();
sout = bufOut;
// std::cout << sin << std::endl;
//std::cout << lenin << std::endl;
//std::cout << lenout << std::endl;
ret = iconv(cd, &sin, static_cast<size_t * >(&lenin), &sout, static_cast<size_t * >(&lenout));
//errno:84:Invalid or incomplate multibyte or wide character
if (-1 == ret) {
std::cout << strerror(errno) << std::endl;
if (errno != 84) {
return "";
}
}
std::cout << "bufout:" << bufOut << std::endl;
std::cout << "bufout end" << std::endl;
iconv_close(cd);
result.assign(bufOut, BUF_LEN - lenout);
return result;
}
int main() {
string s = "哈哈";
std::cout << s.length() << std::endl;
s = convertCode(s, "gbk", "utf-8//IGNORE");
//std::cout << s << std::endl;
std::cout << s.length() << std::endl;
}

iconv函数出现段错误的原因

使用iconv函数进行转换的时候可能会出现段错误,这里出现这个错误的主要原因是注意看iconv函数的函数原型:

1
extern size_t iconv(iconv_t cd, char** restrict inbuf, size_t* in_left_buf, char** restrict outbuf, size_t* out_left_buf);

长度为size_t的指针,int指针转换为size_t指针在一些系统的转换过程会出现问题,导致长度出现错误,内存越界,出现段错误。错误信息如下:

1
2
3
4
5
Program received signal SIGSEGV, Segmentation fault.
from_gbk (irreversible=0x7fffffffb188, outend=0x61d7c0 "", outptrp=<synthetic pointer>,
inend=0xa7ffffffdb76 <error: Cannot access memory at address 0xa7ffffffdb76>,
inptrp=0x7fffffffb2e8, step_data=0x6157d0, step=0x615030) at ../iconv/loop.c:325
325 ../iconv/loop.c: No such file or directory.

size_t与int类型

size_t类型是在stddef.h文件中定义。size_t的类型与操作系统相关,在32位架构中被普遍定义为:

1
typedef unsigned int size_t;

在64为机器中被定义为:

1
typedef unsigned long size_t;

int类型在32和64为机器上的长度都是4位,long在32位机器为4位,在64位机器为8位。所以在64为机器上,size_t和int指针转换的过程中一定会出现问题,在32为系统中的正整数指针不会指针,但是负整数也会出现问题。

Comment and share

  • page 1 of 1

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China