Unicode 字符串—Windows 核心编程

本文深入探讨了在C编程中如何利用Unicode字符串,包括数据类型的定义、字符串函数的使用、以及与ANSI字符串的交互。重点介绍了如何通过TChar.h文件实现通用的ANSI/Unicode代码,以及Windows提供的Unicode数据类型和函数。此外,文章还提供了将文本串视为字符数组、使用通用数据类型和执行全局性替换的基本原则,并讨论了如何在Windows环境中有效地转换和操作Unicode与ANSI字符串。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

2.8.1 C 运行期库对Unicode的支持

为了利用U n i c o d e 字符串,定义了一些数据类型。标准的C 头文件S t r i n g . h 已经作了修改,以便定义一个名字为w c h a r _ t 的数据类型,它是一个U n i c o d e 字符的数据类型:

typedef unsigned short wchar_t;

例如,如果想要创建一个缓存,用于存放最多为9 9 个字符的U n i c o d e 字符串和一个结尾为零的字符,可以使用下面这个语句:

wchar_t szBuffer[100];

该语句创建了一个由1 0 0 个1 6 位值组成的数组。当然,标准的C 运行期字符串函数,如s t r c p y 、s t r c h r 和s t r c a t 等,只能对A N S I 字符串进行操作,不能正确地处理U n i c o d e 字符串。因此,ANSI C 也拥有一组补充函数。清单2 - 1 显示了一些标准的ANSI C 字符串函数,后面是它们的等价U n i c o d e 函数。

char * strcat(char *,const char *);
wchar_t * wcscat(wchar_t *,const wchar_t *);

清单2-1 标准的ANSI C 字符串函数和它们的等价U n i c o d e 函数

char * strchr(const char *,int);
wchar_t * wcschr(const wchar_t *,wchar_t);

int strcmp(const char *,const char *);
int wcscmp(const wchar_t *,const wchar_t *);

char * strcpy(char *,const char *);
wchar_t * wcscpy(wchar_t *,const wchar_t *);

size_t strlen(const char *);
size_t wcslen(const wchar_t *);

请注意所有的U n i c o d e 函数均以w c s 开头,w c s 是宽字符串的英文缩写。若要调用U n i c o d e函数,只需用前缀w c s 来取代A N S I 字符串函数的前缀s t r 即可。

注意大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是M i c r o s o f t 公司提供的C 运行期库与A N S I 的标准C 运行期库是一致的。ANSI C 规定,C运行期库支持U n i c o d e 字符和字符串。这意味着始终都可以调用C 运行期函数,以便对U n i c o d e 字符和字符串进行操作,即使是在Windows 98 上运行,也可以调用这些函数。换句话说,w c s c a t 、w c s l e n 和w c s t o k 等函数都能够在Windows 98 上很好地运行,这些都是必须关心的操作系统函数。

对于包含了对s t r 函数或w c s 函数进行显式调用的代码来说,无法非常容易地同时为A N S I 和U n i c o d e 对这些代码进行编译。本章前面说过,可以创建同时为A N S I 和U n i c o d e 进行编译的单个源代码文件。若要建立双重功能,必须包含T C h a r. h 文件,而不是包含S t r i n g . h 文件。

T C h a r. h 文件的唯一作用是帮助创建A N S I / U n i c o d e 通用源代码文件。它包含你应该用在源代码中的一组宏,而不应该直接调用s t r 函数或者w c s 函数。如果在编译源代码文件时定义了U N I C O D E ,这些宏就会引用w c s 这组函数。如果没有定义_ U N I C O D E ,那么这些宏将引用s t r这组宏。

例如,在T C h a r. h 中有一个宏称为_ t c s c p y 。如果在包含该头文件时没有定义_ U N I C O D E ,那么_ t c s c p y 就会扩展为A N S I 的s t r c p y 函数。但是如果定义了_UNICODE, _tcscpy 将扩展为U n i c o d e的w c s c p y 函数。拥有字符串参数的所有C 运行期函数都在T C h a r. h 文件中定义了一个通用宏。如果使用通用宏,而不是A N S I / U n i c o d e 的特定函数名,就能够顺利地创建可以为A N S I 或U n i c o d e进行编译的源代码。

但是,除了使用这些宏之外,还有一些操作是必须进行的。T C h a r. h 文件包含了另外一些宏.若要定义一个A N S I / U n i c o d e 通用的字符串数组,请使用下面的T C H A R 数据类型。如果定义了_ U N I C O D E ,T C H A R 将声明为下面的形式:

typedef wchar_t TCHAR;

如果没有定义_ U N I C O D E ,则T C H A R 将声明为下面的形式:

typedef char TCHAR;

使用该数据类型,可以像下面这样分配一个字符串:

TCHAR szString[100];

也可以创建对字符串的指针:

TCHAR *szError="Error";

不过上面这行代码存在一个问题。按照默认设置,M i c r o s o f t 公司的C + +编译器能够编译所有的字符串,就像它们是A N S I 字符串,而不是U n i c o d e 字符串。因此,如果没有定义_ U N I C O D E ,该编译器将能正确地编译这一行代码。但是,如果定义了_ U N I C O D E ,就会产生一个错误。若要生成一个U n i c o d e 字符串而不是A N S I 字符串,必须将该代码行改写为下面的样子:

TCHAR *szError=L"Error";

字符串(literal string )前面的大写字母L ,用于告诉编译器该字符串应该作为U n i c o d e 字符串来编译。当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。这种变更带来的问题是,现在只有当定义了_ U N I C O D E 时,程序才能成功地进行编译。我们需要另一个宏,以便有选择地在字符串的前面加上大写字母L 。这项工作由_ T E X T 宏来完成,_ T E X T 宏也在T C h a r. h 文件中做了定义。如果定义了_ U N I C O D E ,那么_ T E X T 定义为下面的形式:

#define _TEXT(x) L ## x

如果没有定义_ U N I C O D E ,_ T E X T 将定义为

#define _TEXT(x) x

使用该宏,可以改写上面这行代码,这样,无论是否定义了_ U N I C O D E 宏,它都能够正确地进行编译。如下所示:

TCHAR *szError=_TEXT("Error");

_ T E X T 宏也可以用于字符串。例如,若要检查一个字符串的第一个字符是否是大写字母J ,只需编写下面的代码即可:

if(szError[0]==_TEXT('J')){
//First character is a 'J'
...
}
else{
//First character is not a 'J'
...
}

2.8.2 Windows定义的Unicode数据类型

Wi n d o w s 头文件定义了表2 - 3 列出的数据类型。

表2-3 Uincode 数据类型

数据类型说明
W C H A RU n i c o d e 字符
P W S T R指向U n i c o d e 字符串的指针
P C W S T R指向一个恒定的U n i c o d e 字符串的指针

这些数据类型是指U n i c o d e 字符和字符串。Wi n d o w s 头文件也定义了A N S I / U n i c o d e 通用数据类型P T S T R 和P C T S T R 。这些数据类型既可以指A N S I 字符串,也可以指U n i c o d e 字符串,这取决于当编译程序模块时是否定义了U N I C O D E 宏。

请注意,这里的U N I C O D E 宏没有前置的下划线

。_ U N I C O D E 宏用于C 运行期

文件,而U N I C O D E 宏则用于

Wi n d o w s 头文件。当编译源代码

块时,通常必须同时定义这两个

宏。

 

2.9 成为符合ANSI和Unicode的应用程序

即使你不打算立即使用U n i c o d e ,最好也应该着手将你的应用程序转换成符合U n i c o d e 的应用程序。下面是应该遵循的一些基本原则:

• 将文本串视为字符数组,而不是c h a r s 数组或字节数组。
• 将通用数据类型(如T C H A R 和P T S T R )用于文本字符和字符串。
• 将显式数据类型(如B Y T E 和P B Y T E )用于字节、字节指针和数据缓存。
• 将T E X T 宏用于原义字符和字符串。
• 执行全局性替换(例如用P T S T R 替换P S T R )。
• 修改字符串运算问题。例如函数通常希望你在字符中传递一个缓存的大小,而不是字节。

这意味着你不应该传递s i z e o f ( s z B u ff e r ) ,而应该传递(s i z e o f ( s z B u ff e r ) / s i z e o f ( T C H A R )。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来分配内存。这就是说,应该调用malloc(nCharacters *sizeof(TCHAR)),而不是调用m a l l o c( n C h a r a c t e r s )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器将不发出任何警告。

2.9 成为符合ANSI和Unicode的应用程序

即使你不打算立即使用U n i c o d e ,最好也应该着手将你的应用程序转换成符合U n i c o d e 的应用程序。下面是应该遵循的一些基本原则:

• 将文本串视为字符数组,而不是c h a r s 数组或字节数组。
• 将通用数据类型(如T C H A R 和P T S T R )用于文本字符和字符串。
• 将显式数据类型(如B Y T E 和P B Y T E )用于字节、字节指针和数据缓存。
• 将T E X T 宏用于原义字符和字符串。
• 执行全局性替换(例如用P T S T R 替换P S T R )。
• 修改字符串运算问题。例如函数通常希望你在字符中传递一个缓存的大小,而不是字节。

这意味着你不应该传递s i z e o f ( s z B u ff e r ) ,而应该传递(s i z e o f ( s z B u ff e r ) / s i z e o f ( T C H A R )。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来分配内存。这就是说,应该调用malloc(nCharacters *sizeof(TCHAR)),而不是调用m a l l o c( n C h a r a c t e r s )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器将不发出任何警告。

2.9.1 Windows字符串函数

Wi n d o w s 也提供了一组用于对U n i c o d e 字符串进行操作的函数,表2 - 4 对它们进行了描述。

表2-4 对U n i c o d e 字符串进行操作的函数

函数

描述

l s t r c a t将一个字符串置于另一个字符串的结尾处
l s t r c m p对两个字符串进行区分大小写的比较
l s t r c m p i对两个字符串进行不区分大小写的比较
l s t r c p y将一个字符串拷贝到内存中的另一个位置
l s t r l e n返回字符串的长度(按字符数来计量)

这些函数是作为宏来实现的,这些宏既可以调用函数的U n i c o d e 版本,也可以调用函数的A N S I 版本,这要根据编译源代码模块时是否已经定义了U N I C O D E 而定。例如,如果没有定义U N I C O D E ,l s t r c a t 函数将扩展为l s t r c a t A 。如果定义了U N I C O D E ,l s t r c a t 将扩展为l s t r c a t W 。

有两个字符串函数,即l s t r c m p 和l s t r c m p i ,它们的行为特性与等价的C 运行期函数是不同的。C 运行期函数s t r c m p 、s t r c m p i 、w c s c m p 和w c s c m p i 只是对字符串中的代码点的值进行比较,这就是说,这些函数将忽略实际字符的含义,只是将第一个字符串中的每个字符的数值与第二个字符串中的字符的数值进行比较。而Wi n d o w s 函数l s t r c m p 和l s t r c m p i 是作为对Wi n d o w s 函数C o m p a r e S t r i n g 的调用来实现的。

int CompareString(
    LCID lcid,
    DWORD fdwStyle,
    PCWSTR pString1,
    int cch1,
    PCTSTR pString2,
    int cch2);

该函数对两个U n i c o d e 字符串进行比较。C o m p a r e S t r i n g 的第一个参数用于设定语言I D(L C I D ),这是个3 2 位值,用于标识一种特定的语言。C o m p a r e S t r i n g 使用这个L C I D 来比较这两个字符串,方法是对照一种特定的语言来查看它们的字符的含义。这种操作方法比C 运行期函数简单地进行数值比较更有意义。

当l s t r c m p 函数系列中的任何一个函数调用C o m p a r e S t r i n g 时,该函数便将调用Wi n d o w s 的G e t T h r e a d S t r i n g 函数的结果作为第一个参数来传递:

LCID GetThreadLocale();

每次创建一个线程时,它就被赋予一种语言。函数将返回该线程的当前语言设置。

C o m p a r e S t r i n g 的第二个参数用于标识一些标志,这些标志用来修改该函数比较两个字符串时所用的方法。表2 - 5 显示了可以使用的标志。

表2-5 CompareString 的标志及含义

标志含义
N O R M _ I G N O R E C A S E忽略字母的大小写
N O R M _ I G N O R E K A N AT Y P E不区分平假名与片假名字符
N O R M _ I G N O R E N O N S PA C E忽略无间隔字符
N O R M _ I G N O R E S Y M B O L S忽略符号
N O R M _ I G N O R E W I D T H不区分单字节字符与作为双字节字符的同一个字符
S O RT _ S T R I N G S O RT将标点符号作为普通符号来处理

当l s t r c m p 调用C o m p a r e S t r i n g 时,它传递0 作为f d w S t y l e 的参数。但是,当l s t r c m p i 调用C o m p a r e S t r i n g 时,它就传递N O R M _ I G N O R E C A S E 。C o m p a r e S t r i n g 的其余4 个参数用于设定两个字符串和它们各自的长度。如果为c c h 1 参数传递- 1 ,那么该函数将认为p S t r i n g 1 字符串是以0结尾,并计算该字符串的长度。对于p S t r i n g 2 字符串来说,参数c c h 2 的作用也是一样。

其他C 运行期函数没有为U n i c o d e 字符串的操作提供很好的支持。例如,t o l o w e r 和t o u p p e r函数无法正确地转换带有重音符号的字符。为了弥补C 运行期库中的这些不足,必须调用下面这些Wi n d o w s 函数,以便转换U n i c o d e 字符串的大小写字母。这些函数也可以正确地用于A N S I字符串。

头两个函数:

PTSTR CharLower(PTSTR pszString);
PTSTR CharUpper(PTSTR pszString);

既可以转换单个字符,也可以转换以0 结尾的整个字符串。若要转换整个字符串,只需要传递字符串的地址即可。若要转换单个字符,必须像下面这样传递各个字符:
将单个字符转换成一个P T S T R ,便可调用该函数,将一个值传递给它,在这个值中,较低的1 6 位包含了该字符,较高的1 6 位包含0 。当该函数看到较高位是0 时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3 2 位值,较低的1 6 位中是已经转换的字符。

TCHAR cLowerCase=CharLower((PTSTR szString[0]);

将单个字符转换成一个P T S T R ,便可调用该函数,将一个值传递给它,在这个值中,较低的1 6 位包含了该字符,较高的1 6 位包含0 。当该函数看到较高位是0 时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3 2 位值,较低的1 6 位中是已经转换的字符。

下面两个函数与前面两个函数很相似,差别在于它们用于转换缓存中包含的字符(该缓存不必以0 结尾):

DWORD CharLowerBuff(
   /* pointer to buffer containing characters to process */
   PTSTR pszString, 
   /* number of bytes or characters to process */
   DWORD cchLength  
);
DWORD CharUpperBuff(
   /* pointer to buffer containing characters to process */
   LPTSTR lpsz,
   /* number of characters to process */
   DWORD cchLength
);

其他的C 运行期函数,如i s a l p h a 、i s l o w e r 和i s u p p e r ,返回一个值,指明某个字符是字母字符、小写字母还是大写字母。Windows API 提供了一些函数,也能返回这些信息,但是Wi n d o w s 函数也要考虑用户在控制面板中指定的语言:

BOOL IsCharAlpha(TCHAR ch);
BOOL IsCharAlphaNumeric(TCHAR ch);
BOOL IsCharLower(TCHAR ch);
BOOL IsCharUpper(TCHAR ch);

p r i n t f 函数家族是要介绍的最后一组C 运行期函数。如果在定义了_ U N I C O D E 的情况下编译你的源代码模块,那么p r i n t f 函数家族便希望所有字符和字符串参数代表U n i c o d e 字符和字符串。但是,如果在没有定义_ U N I C O D E 的情况下编译你的源代码模块,p r i n t f 函数家族便希望传递给它的所有字符和字符串都是A N S I 字符和字符串。

M i c r o s o f t 公司已经给C 运行期的p r i n t f 函数家族增加了一些特殊的域类型。其中有些域类型尚未被ANSI C 采用。新类型使你能够很容易地对A N S I 和U n i c o d e 字符和字符串进行混合和匹配。操作系统的w s p r i n t f 函数也得到了增强。下面是一些例子(请注意大写S 和小写s 的使用):

char szA[100];             //An ANSI string buffer
WCHAR szW[100];            //A Unicode string buffer

//Normal sprintf:all strings are ANSI
sprintf(szA, "%s","ANSI Str");

//Converts Unicode string to ANSI
sprintf(szA,"%S",L"Unicode Str");

//Normal swprintf:all strings are Unicode
swprintf(szW,L"%s",L"Unicode Str");

//Converts ANSI string to Unicode
swprintf(szW,L"%S", "ANSI Str");
2.9.2 资源 --确定文本是ANSI型 还是 Unicode型

对于许多用来打开文本文件和处理这些文件的应用程序(如编译器)来说,打开一个文件后,应用程序就能方便地确定该文本文件是包含A N S I 字符还是U n i c o d e 字符。I s Te x t U n i c o d e 函数能够帮助进行这种区分:

DWORD IsTextUnicode(CONST PVOID pvBuffer, int cb,PINT pResult);

文本文件存在的问题是,它们的内容没有严格和明确的规则,因此很难确定该文件是包含A N S I 字符还是U n i c o d e 字符。I s Te x t U n i c o d e 使用一系列统计方法和定性方法,以便猜测缓存的内容。由于这不是一种确切的科学方法,因此I s Te x t U n i c o d e 有可能返回不正确的结果。

第一个参数p v B u ff e r 用于标识要测试的缓存的地址。该数据是个无效指针,因为你不知道你拥有的是A N S I 字符数组还是U n i c o d e 
字符数组。

第二个参数c b 用于设定p v B u ff e r 指向的字节数。同样,由于你不知道缓存中放的是什么,因此c b 是个字节数,而不是字符数。请注意,不必设定缓存的整个长度。当然,I s Te x t U n i c o d e能够测试的字节越多,得到的结果越准确。

第三个参数p R e s u l t 是个整数的地址,必须在调用I s Te x t U n i c o d e 之前对它进行初始化。对该整数进行初始化后,就可以指明你要I s Te x t U n i c o d e 执行哪些测试。也可以为该参数传递N U L L ,在这种情况下,I s Te x t U n i c o d e 将执行它能够进行的所有测试(详细说明请参见Platform SDK 文档)。

如果I s Te x t U n i c o d e 认为缓存包含U n i c o d e 文本,便返回T R U E ,否则返回FA L S E 。确实是这样,尽管M i c r o s o f t将该函数的原型规定为返回D W O R D ,但是它实际上返回一个布尔值。如果在p R e s u l t 参数指向的整数中必须进行特定的测试,该函数就会在返回之前设定整数中的信息位,以反映每个测试的结果。

2.9.4 在Unicode与ANSI之间转换字符串

Wi n d o w s 函数M u l t i B y t e To Wi d e C h a r 用于将多字节字符串转换成宽字符串。下面显示了M u l t i B y t e To Wi d e C h a r 函数。

int MultiByteToWideChar(
    UINT CodePage,          //code page
    DWORD dwFlags,          //character-type options
    LPCSTR lpMultiByteStr,  //address of string to map
    int cchMultiByte,       //number of bytes in string
    LPWSTR lpWideCharStr,   //address of wide-character buffer
    int cchWideChar         //size of buffer
);

u C o d e P a g e 参数用于标识一个与多字节字符串相关的代码页号。d w F l a g s 参数用于设定另一个控件,它可以用重音符号之类的区分标记来影响字符。这些标志通常并不使用,在d w F l a g s参数中传递0 。p M u l t i B y t e S t r 参数用于设定要转换的字符串,c c h M u l t i B y t e 参数用于指明该字符串的长度(按字节计算)。如果为c c h M u l t i B y t e 参数传递- 1 ,那么该函数用于确定源字符串的长度。

转换后产生的U n i c o d e 版本字符串将被写入内存中的缓存,其地址由p Wi d e C h a r S t r 参数指定。必须在c c h Wi d e C h a r 参数中设定该缓存的最大值(以字符为计量单位)。如果调用M u l t i B y t e To Wi d e C h a r ,给c c h Wi d e C h a r 参数传递0 ,那么该参数将不执行字符串的转换,而是返回为使转换取得成功所需要的缓存的值。一般来说,可以通过下列步骤将多字节字符串转换成U n i c o d e 等价字符串:

1) 调用M u l t i B y t e To Wi d e C h a r 函数,为p Wi d e C h a r S t r 参数传递N U L L ,为c c h Wi d e C h a r 参数传递0 。
2) 分配足够的内存块,用于存放转换后的U n i c o d e 字符串。该内存块的大小由前面对M u l t B y t e To Wi d e C h a r 的调用返回。
3) 再次调用M u l t i B y t e To Wi d e C h a r ,这次将缓存的地址作为p Wi d e C h a r S t r 参数来传递,并传递第一次调用M u l t i B y t e To Wi d e C h a r 时返回的缓存大小,作为c c h Wi d e c h a r 参数。
4. 使用转换后的字符串。
5) 释放U n i c o d e 字符串占用的内存块。
函数Wi d e C h a r To M u l t i B y t e 将宽字符串转换成等价的多字节字符串,如下所示:

int WideCharToMultiByte(
  UINT CodePage,         // code page
  DWORD dwFlags,         // performance and mapping flags
  LPCWSTR lpWideCharStr, // address of wide-character string
  int cchWideChar,       // number of characters in string
  LPSTR lpMultiByteStr,  // address of buffer for new string
  int cchMultiByte,      // size of buffer
  LPCSTR lpDefaultChar,  // address of default for unmappable 
                         // characters
  LPBOOL lpUsedDefaultChar   // address of flag set when default 
                             // char. used
);

该函数与M u l t i B i t e To Wi d e C h a r 函数相似。同样,u C o d e P a g e 参数用于标识与新转换的字符串相关的代码页。d w F l a g s 则设定用于转换的其他控件。这些标志能够作用于带有区分符号的字符和系统不能转换的字符。通常不需要为字符串的转换而拥有这种程度的控制手段,你将为d w F l a g s 参数传递0 。

p Wi d e C h a r S t r 参数用于设定要转换的字符串的内存地址,c c h Wi d e C h a r 参数用于指明该字符串的长度(用字符数来计量)。如果你为c c h Wi d e C h a r 参数传递- 1 ,那么该函数用于确定源字符串的长度。

转换产生的多字节版本的字符串被写入由p M u l t i B y t e S t r 参数指明的缓存。必须在c c h M u l t i B y t e参数中设定该缓存的最大值(用字节来计量)。如果传递0 作为Wi d e C h a r To M u l t i B y t e 函数的c c h M u l t i B y t e 参数,那么该函数将返回目标缓存需要的大小值。通常可以使用将多字节字符串转换成宽字节字符串时介绍的一系列类似的事件,将宽字节字符串转换成多字节字符串。

你会发现,Wi d e C h a r To M u l t i B y t e 函数接受的参数比M u l t i B y t e To Wi d e C h a r 函数要多2 个,即p D e f a u l t C h a r 和p f U s e d D e f a u l t C h a r 。只有当Wi d e C h a r To M u l t i B y t e 函数遇到一个宽字节字符,而该字符在u C o d e P a g e 参数标识的代码页中并没有它的表示法时,Wi d e C h a r To M u l t i B y t e 函数才使用这两个参数。如果宽字节字符不能被转换,该函数便使用p D e f a u l t C h a r 参数指向的字符。如果该参数是N U L L (这是大多数情况下的参数值),那么该函数使用系统的默认字符。该默认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。

p f U s e d D e f a u l t C h a r 参数指向一个布尔变量,如果宽字符串中至少有一个字符不能转换成等价多字节字符,那么函数就将该变量置为T R U E 。如果所有字符均被成功地转换,那么该函数就将该变量置为FA L S E 。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该变量。同样,通常为该测试传递N U L L 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值