常用编码详解(转自VCKBASE)作者:李静南

 

常用编码详解

一、通用字符集(UCS)

ISO/IEC 10646-1 [ISO-10646]定义了一种多于8比特字节的字符集,称作通用字符集(UCS),它包含了世界上大多数可书写的字符系统。已定义了两种多8比特字节编码,对每一个字符采用四个8比特字节编码的称为UCS-4,对每一个字符采用两个8比特字节编码的称为UCS-2。它们仅能够对UCS的前64K字符进行编址,超出此范围的其它部分当前还没有分配编址。

二、基本多语言面(BMP)

ISO 10646 定义了一个31位的字符集。 然而,在这巨大的编码空间中,迄今为止只分配了前65534个码位 (0x0000 到 0xFFFD)。 这个UCS的16位子集称为 “基本多语言面 ”(Basic Multilingual Plane, BMP)。

三、Unicode编码

历史上, 有两个独立的, 创立单一字符集的尝试。 一个是国际标准化组织(ISO)的 ISO 10646 项目;另一个是由(一开始大多是美国的)多语言软件制造商组成的协会组织的 Unicode 项目。幸运的是, 1991年前后, 两个项目的参与者都认识到: 世界不需要两个不同的单一字符集。它们合并双方的工作成果,并为创立一个单一编码表而协同工作。两个项目仍都存在并独立地公布各自的标准, 但 Unicode 协会和 ISO/IEC JTC1/SC2 都同意保持 Unicode 和 ISO 10646 标准的码表兼容,并紧密地共同调整任何未来的扩展。Unicode 标准额外定义了许多与字符有关的语义符号学, 一般而言是对于实现高质量的印刷出版系统的更好的参考。

四、UTF-8编码

UCS-2和UCS-4编码很难在许多当前的应用和协议中使用,这些应用和协议假定字符为一个8或7比特的字节。即使新的可以处理16比特字符的系统,却不能处理UCS-4数据。这种情况导致一种称为UCS转换格式(UTF)的发展,它每一种有不同的特征。 UTF-8(RFC 2279),使用了8比特字节的所有位,保持全部US-ASCII取值范围的性质:US-ASCII字符用一个8比特字节编码,采用通常的US-ASCII值,因此,在此值下的任何一个8比特位字节仅仅代表一个US-ASCII字符,而不会为其他字符。它有如下的特性:

1)UTF-8向UCS-4,UCS-2两者中任一个进行相互转换比较容易。
2)多8比特字节序列的第一个8比特字节指明了系列中8比特字节的数目。
3)8比特字节值FE和FF永远不会出现。
4)在8比特字符流中字符边界从哪里开始较容易发现。

UTF-8定义:
在UTF-8中,字符采用1到6个8比特字节的序列进行编码。仅仅一个8比特字节的一个序列中,字节的高位为0,其他的7位用于字符值编码。n (n>1)个8比特字节的一个序列中,初始的8比特字节中高n位为1,接着一位为0,此字节余下的位包含被编码字符值的位。接着的所有8比特字节的最高位为1,接着下一位为0,余下每个字节6位包含被编码字符的位。

下表总结了这些不同的8比特字节类型格式。字母x指出此位来自于进行编码的UCS-4字符值。

 

   UCS-4范围(16进制)     UTF-8 系列(二进制)
0000 0000<->0000 007F 0xxxxxxx
0000 0080<->0000 07FF 110xxxxx 10xxxxxx
0000 0800<->0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx

0001 0000<->001F FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
0020 0000<->03FF FFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
0400 0000<->7FFF FFFF 1111110x 10xxxxxx ... 10xxxxxx

从UCS-4 到 UTF-8编码规则如下:
1)从字符值和上表第一列中决定需要的8比特字节数目。着重指出的是上表中的行是相互排斥的,也就是说,对于一个给定的UCS-4字符,仅仅有一个有效的编码。
2)按照上表中第二列每行那样准备8比特字节的高位。
3)将UCS字符值的位,从低位起填充在标记为x地方。从UTF8序列中最后一个字节填起,然后剩下的字符值依次放到前一个字节中,如此重复,直到所有标记位x的位都进行了填充。

这里我们仅仅实现Unicode到UTF8的转换,Unicode都是两个字节,定义为:
 
 
typedef usigned  short  WCHAR

//  输出的UTF8编码至多是3个字节。

int  UnicodeToUTF8(WCHAR ucs2, unsigned  char   * buffer)
{
    memset(buffer, 
0 4 );
    
if  (( 0x0000   <=  ucs2)  &&  (ucs2  <=   0x007f ))   //  one char of UTF8
    {
       buffer[
0 =  ( char )ucs2;
       
return   1 ;
    }
    
if  (( 0x0080   <=  ucs2)  &&  (ucs2  <=   0x07ff ))   //  two char of UTF8
    {
       buffer[
1 =   0x80   |   char (ucs2  &   0x003f );
       buffer[
0 =   0xc0   |   char ((ucs2  >>   6 &   0x001f );
       
return   2 ;
    }
    
if  (( 0x0800   <=  ucs2)  &&  (ucs2  <=   0xffff ))   //  three char of UTF8
    {
       buffer[
2 =   0x80   |   char (ucs2  &   0x003f );
       buffer[
1 =   0x80   |   char ((ucs2  >>   6 &   0x003f );
       buffer[
0 =   0xe0   |   char ((ucs2  >>   12 &   0x001f );
       
return   3 ;
    }
    
return   0 ;
}  


理论上,简单的通过用2个0值的8比特字节来扩展每个UCS-2字符,则从UCS-2到UTF-8编码的算法可以从上面得到。然而,从D800到DFFF 间的UCS-2值对(用Unicode说法是代理对),实际上是通过UTF-16来进行UCS-4字符转换,因此需要特别对待:UTF-16转换必须未完成,先转换到于UCS-4字符,然后按照上面过程进行转换。

从UTF-8到UCS-4解码过程如下:
1)初始化UCS-4字符4个8比特字节的所有位为0。
2)根据序列中8比特字节数和上表中第二列(标记为x位)来决定哪些位编码用于字符值。
3)从编码序列分配位到UCS-4字符。首先从序列最后一个8比特字节的最低位开始,接着向左进行,直到所有标记为x的位完成。如果UTF-8序列长度不大于3个8比特字节,解码过程可以直接赋予UCS-2。

 
 
WCHAR UTF8ToUnicode(unsigned  char   * buffer)
{
    WCHAR temp 
=   0 ;
    
if  (buffer[ 0 <   0x80 )                                    //  one char of UTF8
    { 
       temp 
=  buffer[ 0 ];
    }
    
if  (( 0xc0   <=  buffer[ 0 ])  &&  (buffer[ 0 <   0xe0 ))           //  two char of UTF8
    {
       temp 
=  buffer[ 0 &   0x1f ;
       temp 
=  temp  <<   6 ;
       temp 
=  temp  |  (buffer[ 1 &   0x3f );
    }
    
if  (( 0xe0   <=  buffer[ 0 ])  &&  (buffer[ 0 <   0xf0 ))           //  three char of UTF8
    {
       temp 
=  buffer[ 0 &   0x0f ;
       temp 
=  temp  <<   6 ;
       temp 
=  temp  |  (buffer[ 1 &   0x3f );
       temp 
=  temp  <<   6 ;
       temp 
=  temp  |  (buffer[ 2 &   0x3f );
    }
    
if  (( 0x80   <=  buffer[ 0 ])  &&  (buffer[ 0 <   0xc0 ))           //  not the first byte of UTF8 character
        return   0xfeff ;                                        //  0xfeff will never appear in usual

    
return  temp;                                             //  more than 3-bytes return 0
}


注意:上面解码算法的实际实现应该进行安全保护,以便处理解码无效的系列。例如:实现可能(错误)解码无效的UTF-8系列0xC0 0x80为字符U+0000,它可能导致安全问题或其他问题(比如把0当作数组结束标志)。更详细的算法和公式可以在[FSS_UTF], [UNICODE] 或[ISO-10646]附录R中找到。

五、UTF-7编码

UTF-7:A Mail-Safe Transformation Format of Unicode(RFC1642)。这是一种使用 7 位 ASCII 码对 Unicode 码进行转换的编码。它的设计目的仍然是为了在只能传递 7 为编码的邮件网关中传递信息。 UTF-7 对英语字母、数字和常见符号直接显示,而对其他符号用修正的 Base64 编码。符号 + 和 - 号控制编码过程的开始和暂停。所以乱码中如果夹有英文单词,并且相伴有 + 号和 - 号,这就有可能是 UTF-7 编码。

协议中定义的转换规则:
1)集合D中的Unicode字符可以直接的编码为ASCII的等值字节。集合O中的字符可以有有选择的的直接编码为ASCII的等值字节,但要记得其中的很多的字符在报头字段是不合法的,或者不能正确的穿过邮件网关。
2)通过在前面加上转换字符"+",任何一个Unicode序列都可以使用集合B(更改过的base64)中的字符编码。"+"意味着后面的字节将被作为更改过的BASE64字母表中的元素解析,直到遇到一个不是字母表中的字符为止。这些字符中会包含控制字符,比如回车和换行;因此,一个Unicode转换序列总是在一行上结束。注释:有两个特殊的情形:"+-"表示''+'',"+ …… --"表示有一个真正的''-''字符出现了。多数情况是没有''-''标记结束。
3)空格、tab、回车和换行字符可以直接使用ASCII等价字节表示。

那么我们就可以定义算法了,我们先定义字符集的相关数组:
 
 
typedef unsigned  char   byte

//  64 characters for base64 coding
byte  base64Chars[]  =   " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ " ;  

//  8 characters are safe just as base64 characters for MAIL gates
byte  safeChars[]  =   " ''(),-.:? " ;    

//  4 characters all means space
byte  spaceChars[]  =   "  /t/n/r " ;    

注:在编码处理时候,我们需要对一个字节判断属于哪类字符,以便确定处理规则,如果简单的使用范围比较的方式,效率很低,我们采用哈希表的思路:建立一个256长的数组,那么对于每一个字节的值,就可以定义一个类型。判断时候,对每个字符都直接取数组的值。

//  mask value defined for indentify the type of a byte
#define     BASE64        0x01
#define     SAFE        0x02
#define     SPACE        0x04
byte  byteType[ 256 ];         //  hash table used for find the type of a byte
bool  firstTime  =   true ;      //  the first time to use the lib, wait for init the table

// 注:为了解码base64编码部分的字符,需要一个哈希表,对一个base64字符都可以直接得到0-64之间的一个数:
byte base64Value[128];

这两个哈希表在使用前要初始化:

void  initUTF7Tables()
{
    
byte   * s;
    
if ( ! firstTime)
    
return ;
    
//  not necessary, but should do it to be robust
    memset(byteType,  0 256 );
    memset(base64Value, 
0 128 );
    
    
for (s = base64Chars;  * s != '' / 0 '' ; s ++ )
    {
       byteType[
* s]  |=  BASE64;
       base64Value[
* s]  =  s  -  base64Chars;  //  the offset, it is a 6bits value,0-64
    }
    
    
for (s = safeChars;  * s != '' / 0 '' ; s ++ )
       byteType[
* s]  |=  SAFE;
       
    
for (s = spaceChars;  * s != '' / 0 '' ; s ++ )
       byteType[
* s]  |=  SPACE; 

    firstTime 
=   false ;
}

UTF-7编码转换时候,是与当前字符是与状态有关的,也就是说:
1)正处于Base64编码状态中
2)正处于直接编码状态中
3)现在UTF-7的缓冲区里,当前的字符是转换开关"+"

所以要定义相关的字段:

 
 
//  the state of current character 
#define     IN_ASCII    0
#define     IN_BASE64    1
#define  AFTER_PLUS    2

在使用规则2进行编码时候,需要使用base64的方法,也就需要2个全局的辅助变量:

 
 
int  state;                  //  state in which  we are working
int  nbits;                  //  number of bits in the bit buffer
unsigned  long  bitBuffer;    //  used for base64 coding

把一个Unicode字符转化为一个UTF-7序列:返回写到缓冲区里的字节数目,函数影响了state,nbits,bitBuffer三个全局变量。这里先实现了一个简单的辅助函数,功能是把一个Unicode字符转变后写到提供的缓冲区中,返回写入的字节个数。在开始编码Unicode字符数组中第一个字符的时候,state,nbits,bitBuffer三个全局变量需要被初始化:

 
 
state  =  IN_ASCII;
nbits 
=   0 ;
bitBuffer 
=   0 ;

int  UnicodeToUTF7(WCHAR ucs2,  byte   * buffer)
{
    
byte   * head  =  buffer;
    
int  index;    
    
    
//  is an ASCII and is a byte in char set defined
     if  (((ucs2  &   0xff80 ==   0 ))  &&  (byteType[( byte )u2]  &  (BASE64 | SAFE | SPACE))) 
    {    
       
byte  temp  =  ( byte )ucs2;
       
       
if  (state  ==  IN_BASE64)  //  should switch out from base64 coding here
       {
          
if  (nbits  >   0 )        //  if some bits in buffer, then output them
          {
             index 
=  (bitBuffer  <<  ( 6   -  nbits))  &   0x3f ;
             
* s ++   =  base64[index];
          }
          
if  ((byteType[temp]  &  BASE64)  ||  (temp  ==   '' - '' ))
             
* s ++   =   '' - '' ;
      state 
=  IN_ASCII;
       }
       
* s ++   =  temp;
       
       
if  (temp  ==   '' + '' )
          
* s ++   =   '' - '' ;
    }
    
else
    {
       
if  (state  ==  IN_ASCII) 
       {
          
* s ++   =   '' + '' ;
          state 
=  IN_BASE64;           //  begins base64 coding here
          nbits  =   0 ;
          bitBuffer 
=   0 ;
       }
       bitBuffer 
<<=   16 ;
       bitBuffer 
|=  ucs2;
       nbits 
+=   16 ;
       
       
while (nbits  >=   6
       {
          nbits 
-=   6 ;
          index 
=  (bitBuffer  >>  nbits)  &   0x3f ;    //  output the high 6 bits
           * s ++   =  base64[index];
       }
    }
    
return  (s  -  head);
}


说明:对于合法的Unicode字符数组,可以通过逐个输入数组中的字符,连续调用上面的函数,得到一个UTF-7字节序列。需要说明的是:最后一个Unicode字符应该是上面三个字节数组中某个字符的等值。

下面,我们实现一个简单的说明函数,功能是:输入一个UTF-7字节,可能得到并返回一个合法Unicode字符;也可能不能得到,比如遇到''+''或者因为还没有完成一个字符的拼装,这时返回一个标志字符0xfeff,这个字符常用来标志Unicode编码。

注:函数影响了state,nbits,bitBuffer三个全局变量。在开始处理第一个字节时候,变量需要被初始化为:

state  =  IN_ASCII;
nbits 
=   0 ;
bitBuffer 
=   0 ;

#define  RET0 0xfeff

WCHAR UTF7ToUnicode(
byte  c)
{
    
if (state  ==  IN_ASCII) 
    {
       
if  (c  ==   '' + ''
       {
          state 
=  AFTER_PLUS;
          
return  RET0;
       } 
       
else  
        
return  (WCHAR)c;
    }
    
if  (state  ==  AFTER_PLUS) 
    {
       
if  (c  ==   '' - ''
       {
          
return  (WCHAR) '' + '' ;
       } 
       
else  
       {
          state 
=  IN_BASE64;
          nbits 
=   0 ;
          bitBuffer 
=   0 ;   //  it is not necessary
          
//  don''t return yet, continue to the IN_BASE64 mode
       }
     }
     
    
//  state == Base64 
     if  (byteType[c]  &  BASE64) 
    {
       bitBuffer 
<<=   6 ;
       bitBuffer 
|=  base64Value[c];
       nbits 
+=   6 ;
       
if  (nbits  >=   16
       {
          nbits 
-=   16 ;
      
return  (WCHAR)((bitBuffer  >>  nbits)  &   0x0000ffff );
       }
       
return  RET0;
    }
    
//  encount a byte which is not in base64 character set, switch out of base64 coding
    state  =  IN_ASCII;
    
if  (c  !=   '' - ''
    {
       
return  (WCHAR)c;
    }
    
return  RET0;
}


说明:对于一个UTF-7序列,可以通过连续输入字节并调用上面的函数,判断返回值,得到一个Unicode字符数组。

六、GB2312编码中汉字的确定

最早,表示汉字的区位码中,分为94个区,每个区94个汉字,1-15区是西文字符,图形等,16-5为一级汉字,56-87为二级汉字,87区以上为新字用。而我们在Windows默认的编码,GB2312(1981年国家颁布的《信息交换用汉字编码字符集基本集》)国标码,和区位码的换算为:

国标码 = 区位码 + 2020H

而在汉字在计算机内表示的时候为保证ASCII码和汉字编码的不混淆,又做了一个换算:

汉字机内码 = 国标码 + 8080H

所以,真正的在Windows上的GB2312汉字编码是机内码,从上边的两个公式可以得到的就是:

汉字机内码 = 区位码 + a0a0H

一个汉字的编码最少要a0a0H,因此我们在CString中辨别汉字的时候可以认为:当一个字符的编码大于a0的时候它应该是汉字的一个部分。但是也有特殊的情况的,不是每个汉字的两个字节编码都是大于a0H的,例如‘镕’的编码是 ‘E946’,后面的部分就不满足大于a0H的条件。

七、Windows下多字节编码和Unicode的转换

Windows提供了API函数,可以把Unicode字符数组转换为GB2312字符串。其中,Unicode数组在传入时候最后一个为0,也就是所谓的null termidated字符串。在函数内部得到要返回字节串的大小,请求空间,进行真正的转换操作,指针在外部使用后释放,或者在类中加如其他的操作来处理,比如析构函数中释放。返回值为写到字节串里数目。

	
int  StringEncode::UnicodeToGB2312( char   ** dest,  const  WCHAR  * src)
{
    
char *  buffer;
    
int  size  =  ::WideCharToMultiByte(CP_ACP,  0 , src,  - 1 , NULL,  0 , NULL, NULL);  
    
//  null termidated wchar''s buffer
    buffer  =   new   char [size];
    
int  ret  =  ::WideCharToMultiByte(CP_ACP, NULL, src,  - 1 , buffer, size  +   1 , NULL, NULL);

    
if  ( * dest  !=   0 )
        delete 
* dest;
    
* dest  =  buffer;

    
return  ret;
}


注:其中见到有人在使用的时候,申请缓冲区空间时候是申请了(zise + 1)个来,最后一个字节写''/0'',结束字符串。但是在我调试时候发现:系统给的size已经包含了一个写入''/0''的字节,而且最后得到的串中,''/0''是已经被系统API写入了。(也许我的实验有错误,有待验证)。把Unicode字符数组转换为UTF-8和UTF-7的方法类似,只要是WideCharToMultiByte函数的第一个表示代码页参数改为CP_UTF7(65000)和CP_UTF8(65001)。

同样道理,把多字节转换为Unicode字符数组,也有相应的函数。和上面的函数类似,可以通过先提供一个空缓冲区而先得到需要的大小,然后开辟空间得到最后的字符数组。但是考虑到效率,可以适当牺牲一些空间,提供一个足够大的字符数组,数组大小在极端的情况下(全是ASCII)是和字节数组大小一样的。

 
 
int  StringEncode::Gb2312ToUnicode(WCHAR  ** dest,  const   char   * src)
{
    
int  length  =  strlen(src);                 //  null terminated buffer
    WCHAR  * buffer  =   new  WCHAR[length  +   1 ];    //  WCHAR means unsinged short, 2 bytes
                                           
//  provide enough buffer size for Unicodes

    
int  ret  =  ::MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, src, length, buffer, length);
    buffer[ret] 
=   0 ;

    
if  ( * dest  !=   0 )
        delete 
* dest;
    
* dest  =  buffer;

    
return  ret;
}


注:删除以前的缓冲区时候的操作,其实没有必要判断是不是为空,因为删除空指针是没有问题的,因为delete内部提供了这样的机制。

八、URL 解码

用IE发送GET请求的时候,URL是用UTF-8编码的,当对截包数据分析时候就需要对数据解码,下面的函数是一个简单的实现:

CString CTestUrlDlg::UrlToString(CString url)
{
    CString  str 
=   "" ;
    
int  n  =  url.GetLength();
    url.MakeLower();
    BYTE a, b1, b2;
    
for  ( int  i = 0 ; i <   3 )     //  后面没有两个字符了,错误
                 return   "" ;
            b1 
=  charToHex( url.GetAt(i + 1 ) );
            b1 
=  (b1  <<   4 &   0xf0 ;
            b2 
=  charToHex( url.GetAt(i + 2 ) )  &   0x0f ;
            a 
=  b1  |  b2;
            str 
+=  a;
            i 
+=   2 ;
        }
        
else
        {
            str 
+=  url.GetAt(i);
        }
    }
    
return  str;
    
    }
    
static  WCHAR UTF8ToUnicode(unsigned  char   * buf,  int   & t)
{
    WCHAR temp 
=   0 ;
    unsigned 
char   * buffer  =  buf  +  t;
    
if  (buffer[ 0 <   0x80 )                                    //  one char of UTF8
    { 
       temp 
=  buffer[ 0 ];
       t 
+=   1 ;
    }
    
if  (( 0xc0   <=  buffer[ 0 ])  &&  (buffer[ 0 <   0xe0 ))           //  two char of UTF8
    {
       temp 
=  buffer[ 0 &   0x1f ;
       temp 
=  temp  <<   6 ;
       temp 
=  temp  |  (buffer[ 1 &   0x3f );
       t 
+=   2 ;
    }
    
if  (( 0xe0   <=  buffer[ 0 ])  &&  (buffer[ 0 <   0xf0 ))           //  three char of UTF8
    {
       temp 
=  buffer[ 0 &   0x0f ;
       temp 
=  temp  <<   6 ;
       temp 
=  temp  |  (buffer[ 1 &   0x3f );
       temp 
=  temp  <<   6 ;
       temp 
=  temp  |  (buffer[ 2 &   0x3f );
       t 
+=   3 ;
    }
    
if  (( 0x80   <=  buffer[ 0 ])  &&  (buffer[ 0 <   0xc0 ))           //  not the first byte of UTF8 character
        return   0xfeff ;                                        //  0xfeff will never appear in usual

    
return  temp;                                             //  more than 3-bytes return 0
}


static  unsigned  char  charToHex( char  c)
{
    unsigned 
char  d;
    
if  ((c  >=   '' 0 '' &&  (c  <=   '' 9 '' ))
        d 
=  c  -   '' 0 '' ;
    
else   if  ((c  >=   '' a '' &&  (c  <=   '' f '' ))
    {
        d 
=  c  -   '' a ''   +   10 ;
    }
    
else   if  ((c  >=   '' A '' &&  (c  <=   '' F '' ))
    {
        d 
=  c  -   '' A ''   +   10 ;
    }
    
else
        d 
=   0 ;

    
return  d;    
}


static   void  UnicodeToGB2312( const  WCHAR unicode,  char *  buffer)
{
//     int size = ::WideCharToMultiByte(CP_ACP, 0, unicode, -1, NULL, 0, NULL, NULL);  

    
int  ret  =  ::WideCharToMultiByte(CP_ACP, NULL,  & unicode,  - 1 , buffer,  3 , NULL, NULL);
}

CString CTestUrlDlg::Uft8ToGB(CString url)
{
    CString  str 
=   "" ;
    
char  buffer[ 3 ];
    WCHAR unicode;
    
    unsigned 
char   *  p  =  (unsigned  char   * )(LPCTSTR)url;
    
int  n  =  url.GetLength();
    
int  t  =   0 ;
    
while  (t  <  n)
    {
        unicode 
=  UTF8ToUnicode(p, t);
        UnicodeToGB2312(unicode, buffer);
        buffer[
2 =   0 ;
        str 
+=  buffer;
    }

    
return  str;
}


示例:
 
 
CString str  =   " /MFC%E8%8B%B1%E6%96%87%E6%89%8B%E5%86%8C.chm " ;
CString ret 
=  UrlToString(str);
ret 
=  Uft8ToGB(ret);    //  MFC英文手册.chm



九、总结
常见算法还有MIME等,由于篇幅限制,并且网上已经有很多帖子,在此不再赘述。
对于本文,由于个人能力有限,难免有疏漏的地方,还望指教,共同进步。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值