windows 字符串处理

windows 开发的必要知识我对字符编码做了一个应该还算可以的解释。之后我们来谈谈windows 的字符。

windows 的字符处理解决方案可以划分为两大类。一类是使用Win32API,另一类是C的运行时库。

CRT和Win32API

首先我们要弄清楚CRT和win32API的区别。总所周知c/c++是一门编程语言。但是我们通常口边说的c/c++其实一种标准。它规定了C/C++应该长什么样子。但是具体实现就是各个编译器厂商说的算了。所以就出现了Visual c++这个东西。它是Microsoft 公司实现的c/c++标准。举个例子吧,c/c++标准只是指出了基本数据类型的最小大小,而Visual c++规定了基本数据类型的确切大小。具体大小可以参考下面的msdn链接
https://msdn.microsoft.com/zh-cn/library/cc953fe1.aspx

理所当然Microsoft也实现了c/c++标准库。这些库在windows 上称为C运行时(CRT)。

而Win32API是我们的windows 内核留给外界控制其内部的函数。有了这些函数我们就可以在保证windows 内核安全的情况下访问它。

CRT字符管理

CRT中ANSI C标准部分两个字符类型charwchar_t
char :用双引号括起来的常量,如“general”表示其中每一字符为char型(8位)。这些字符串可以用string.h(或者ctring)头文件下的函数处理。如的strlen()

wchar_t:用双引号括起来的前缀加上 L 的常量,如L”wide character”,表示其中每一字符为wchar_t型(一般为16位)。这些字符串用wchar.h头文件中函数处理 。如wcslen()

对于wchar_t这个“后来”的数据类型。如果希望它是内置类型,也就是说不想是typedef short 就需要为编译器设定一个编译器开关
这里写图片描述
看见那个/Zc:wchar_t了吗,他是默认开启的。如果为/Zc:wchar_t-的话表示wchar_t 为typedef short

因为windows 中的CRT是Microsoft编写的所以除了c/c++标准以外还有一些windows 本土化的东西。

在CRT中非ANSI C标准部分( 也就Microsoft平台上独有的)
一定要包含 tchar.h头文件
该表头文件不是ANSI C标准的一部分,因此那里定义的每个函数和宏定义的前面都有一条底线。tchar.h为需要字符串参数的标准执行时期链接库函数提供了一系列的替代名称(例如,_tprintf和_tcslen)。有时这些名称也称为「通用」函数名称,因为它们既可以指向函数的Unicode版也可以指向非Unicode版。

如果用预编译指令定义了_UNICODE的标识符并且程序中包含了tchar.h表头文件,那么_tcslen就定义为wcslen

#define _tcslen wcslen
如果没有定义_UNICODE,则_tcslen定义为strlen:
#define _tcslen strlen

tchar.h还用一个新的数据型态TCHAR来解决两种字符数据型态的问题。如果定义了_UNICODE标识符,那么TCHAR就是wchar_t:
typedef wchar_t TCHAR ;
否则,TCHAR就是char:
typedef char TCHAR ;

如果定义了_UNICODE标识符,那么一个称作__T的宏就定义如下
#define __T(x) L##x
那一对井字号称为「粘贴符号(token paste)」,它将字母L添加到宏参数上。因此,如果宏参数是”Hello!”,则L##x就是L”Hello!”。

如果没有定义_UNICODE标识符,则__T宏只简单地定义如下:
#define __T(x) x
此外,还有两个宏与__T定义相同:

#define _T(x)__T(x)        
#define _TEXT(x)__T(x)    

同样的
tchar.h头文件中还有一个宏_MBCS负责把t开头的函数(宏)转换为处理多字节字符集的函数、
下面是我在msdn上拔下来的

图片来自微软

因为_mbcs多字节字符没没用到所以这片文章就不说了。

windwos中的字符管理

windwos中你可以像c/c++标准中一样处理字符串。但是也可以使用独特的windows 单一编码原则。只需要让Windows程序包括表头文件windows.h。该文件包括许多其它表头文件,包括windef.h,该文件中有许多在Windows中使用的基本型态定义,而且它本身也包括winnt.h。winnt.h处理基本的Unicode支持。
winnt.h定义了新的数据型态,称作CHARWCHAR

typedef char CHAR ;        
typedef wchar_t WCHAR ;    // wc     

WCHAR定义后面的注释是匈牙利标记法的建议:一个基于WCHAR数据型态的变量可在前面附加上字母wc以说明一个宽字符。
winnt.h表头文件进而定义了可用做8位字符串指针的六种数据型态和四个可用做const 8位字符串指针的数据型态。这里精选了表头文件中一些实用的说明数据型态语句:

 typedef CHAR * PCHAR, * LPCH, * PCH, * NPSTR, * LPSTR, * PSTR ;        
typedef CONST CHAR * LPCCH, * PCCH, * LPCSTR, * PCSTR ;      

Tips:

前缀N和L表示「near」和「long」,指的是16位Windows中两种大小不同的指标。在Win32中near和long指标没有区别。其中cch为 const char 的意思

类似地,WINNT.H定义了六种可作为16位字符串指针的数据型态和四种可作为const 16位字符串指针的数据型态:

typedef WCHAR * PWCHAR, * LPWCH, * PWCH, * NWPSTR, * LPWSTR, * PWSTR ;        
typedef CONST WCHAR * LPCWCH, * PCWCH, * LPCWSTR, * PCWSTR ;        

Tips:
上述的CONST 在windows中有如下宏

#define CONST const

与tchar.h一样,winnt.h将TCHAR定义为一般的字符类型。如果定义了标识符UNICODE(没有底线),则TCHAR和指向TCHAR的指针就分别定义为WCHAR和指向WCHAR的指标;如果没有定义标识符UNICODE,则TCHAR和指向TCHAR的指针就分别定义为char和指向char的指标:

#ifdef  UNICODE 
typedef WCHAR TCHAR, * PTCHAR ;
typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR ; 
typedef LPCWSTR LPCTSTR ;      
#else
typedef char TCHAR, * PTCHAR ;  
typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR ; 
typedef LPCSTR LPCTSTR ;   
#endif

如果已经在某个表头文件或者其它表头文件中定义了TCHAR数据型态,那么WINNT.H和WCHAR.H表头文件都能防止其重复定义。不过,无论何时在程序中使用其它表头文件时,都应在所有其它表头文件之前包含WINDOWS.H。

winnt.h表头文件还定义了一个宏,该宏将L添加到字符串的第一个引号前。如果定义了UNICODE标识符,则一个称作 __TEXT的宏定义如下:
#define __TEXT(quote) L##quote
如果没有定义标识符UNICODE,则像这样定义__TEXT宏:
#define __TEXT(quote) quote
此外, TEXT宏可这样定义:
#define TEXT(quote) __TEXT(quote)
这与tchar.h中定义_TEXT宏的方法一样,只是不必操心底线。

Windows函数呼叫

win32 的链接库文件一般以32结尾,如USER32.DLL,而链接库中字符处理方面的函数都有两个版本(char 类型的和宽字符类型)。如MessageBox就有两个版本。幸运的是,您通常不必关心这个问题,程序中只需使用MessageBox即可。
当使用动态连接来编写windows 程序时,所谓函数“调用”实际会调用user32.dll中真正的函数。这就是所谓的动态连接
而user32.dll是由两个函数入口的,一个是char型的一个是宽字符类型的通过一系列类似tcahr.h中的宏定义最终会自动选择调用哪个函数。
MessageBox函数定义如下:
int WINAPI MessageBox (HWND, LPCSTR, LPCSTR, UINT) ;
函数的第二个、第三个参数是指向常数字符串的指针。而WINAPI它指定了一个呼叫约定,包括如何生产机械码以在堆栈中放置函数呼叫的参数。许多Windows函数呼叫声明为WINAPI。
下面是MessageBoxA在WINUSER.H中定义的方法。这与MessageBox早期的定义很相似:

WINUSERAPI int WINAPI MessageBoxA (HWND hWnd, LPCSTR lpText, 
                           LPCSTR lpCaption, UINT uType) ;

下面是MessageBoxW:

WINUSERAPI int WINAPI MessageBoxW (HWND hWnd, LPCWSTR lpText,

                           LPCWSTR lpCaption, UINT uType) ;

在WINUSER.H中定义的相关宏

#ifdef UNICODE

#define MessageBox  MessageBoxW

#else

#define MessageBox  MessageBoxA

#endif


Note:
windwos 基本所有能自动识别char和宽字符的函数,底层都是通过上面的宏定义实现的。后缀A 表示的ASCII版本 ,也就是byte类型的。后缀W表示的是wide character 版本,也就是wchar_t。

CRT中的字符串处理

c/c++标准的字符串处理函数
窄字符需要包括包括在string.h或者cstring头文件中,宽字符需要包括string.h(或者cwchar或者wchar.h)。

以str为前缀的表示的是char类型的字符串处理函数,例如。
strcpy,strcat,strchr,strcmp,strlen

以wcs为前缀的表示wchar_t类型的字符串,例如
wcscpy,wcscat,wcschr,wcscmp,wcslen

以s为后缀函数表示这是安全版本的函数,也就是在使用函数时传递需要指定操作字符串的数量,例如
strcat_s、wcscat_s, strcpy_s、wcscpy_s、

这些安全版本的函数如果发现内存泄漏的话会设置c运行时变量errno,然后返回一个errno_t值来表示成功还是失败(只有为S_OK才表示正确)。但是如果我们在debug的时候这些函数会生成Debug Assertion Failed对话框。
errno的值有很多,下面是常见的。
S_OK:表示一切ok
EINVAL :error invalid的意思,表示参数为nullptr
STRUNCATE : 字符串被截断的意思,win32的安全函数在缓冲区不够的时候返回这个值
ERANAGE :缓冲区大小不够,所以缓冲区都被清空。_s的安全函数在缓冲区不够的时候返回这个值

非c/c++标准的字符串处理函数
这写函全部以下划线开头表示为windows 本土化的东西不能移植的。为了使用这些我们必须包含tchar.h头文件。

这个分类下包括了c/c++没有提到字符串操作和通用字符串处理函数。

以l为后缀的表示表示我们使用传递的区域设置而不适用当前线程的区域。下面是原话

The versions of these functions with the _l suffix are identical except that they use the locale parameter passed in instead of the current thread locale.

因为本人还没弄懂locale parameter是干什么的就不举例了(以后填坑)

以_tcs为前缀表示自动选择char 版函数还是wchar_t 版函数。
_tcscpy,_tcscat,_tcschr,_tcscmp,_tcslen

c++的标准库的string类

千万不要混用这些处理函数,下面举个例子。

char * pc = "Hello!" ;
iLength = strlen (pc) ;

iLength 为6,其实是6个char的ASCII字符+一个char的空字符;

wchar_t * pw = L"Hello!" ;
int iLength = strlen (pw) ;

编译器会出现警告,有的编译器视为错误(如VC++)
‘function’ : incompatible types - from ‘wchar_t*’ to ‘const char *’

如果非要用strlen函数计算宽字符”Hello!”可以表示为下面这样。
0x0048 0x0065 0x006C 0x006C 0x006F 0x0021
而在实际的x86cpu中是这有存储的(小端存储)
48 00 65 00 6C 00 6C 00 6F 00 21 00
也就是说上面宽字符的例子中iLength为1,这就出现了非常严重的问题。
所以我们在使用处理函数的时候一定要分清楚字符串是char还是wchar_t干什么的。

win32中的字符串处理

分为两大类

  • winows 同一编码维护的版本。请windows.h头文件,这些能自动选择处理char还是宽字符字符串。也存在安全隐患Microsoft建议我们使用相应的安全版本。
  • windows 安全版。请包括strsafe.h头文件,这些能自动选择处理char还是宽字符字符串。且不存在安全隐患。(他们是操作系统函数,用于代替:StrCat 、StrChr 、StrCmp 和StrCpy 等老旧代码)

此图片来自微软官网
其中l表示local的意思,至于玩什么是小写可能是因为内部调用的c/c++标准的字符串处理函数吧。
我们拿strlen举例,此函数返回的是char(byte)的个数。而lstrlen会根据TCHAR具体的类型选择返回窄字符长度还是宽字符个数。StringCchLength 也会根据TCHAR具体类型来返回窄字符还是宽字符个数,但是这个函数会防止缓冲区溢出,而StringCbLength返回的是字符串中有多少个byte(1 char==1 byte,1 wchar_t==2 byte),同样这个函数一会防止缓冲区溢出。

起一定要记住使用安全函数请包括strsafe.h头文件。
这写win32的安全函数 中的Cch表示为Count of character 而Cb表示count of byte。
它们的返回结果时HRESULT。它包含下面这些值
S_OK:一切ok
STRSAFE_E_INVALID_PARAMETER:将nullptr传入给了一个参数
STRSAFE_E_INSUFFICIENT_BUFFER:缓冲区太小。这个参数很重要,因为不同于_s结尾的安全函数,这些函数在缓冲区太小的时候执行的是截断,所以只有通过这返回结果才能知道我们的字符串有没有问题。这个值是一个负数所以通过SUCCEEDED/FAILED宏判断这个值话表示失败。

最后就是Ex函数了,每一个Ex函数都比原来的函数多出三个参数。例如:

HRESULT StringCchCatEx(
  _Inout_   LPTSTR  pszDest,
  _In_      size_t  cchDest,
  _In_      LPCTSTR pszSrc,
  _Out_opt_ LPTSTR  *ppszDestEnd,
  _Out_opt_ size_t  *pcchRemaining,
  _In_      DWORD   dwFlags
);

pcchRemaining
这个参数会返回我们缓冲区还有多少个位置没有被使用。切记’\0’是被算在已使用字符中的。
ppszDestEnd
这想缓冲区的末尾字符,也就是缓冲去最后一个字符(没截断的话就是’\0’)
dwFlags
这个表示设置如何填充多余为值(’\0’之后的空间)下面一个或多个值的组合
STRSAFE_FILL_BEHIND_NULL:剩余的空间使用’\0’来填充
STRSAFE_IGNORE_NULLS:把nullptr视为TEXT(“”),可以使用STRSAFE_FILL_BYTE宏来设置我们想要填充的东西

STRSAFE_FILL_ON_FAILURE: 如果因为截断产生的失败所有字符会被替换成填充符,其他的失败会处最后一个填充为’\0’以为其他都地方被替换成填充符
STRSAFE_NULL_ON_FAILURE:函数失败。第一个字符填充’\0’,从而编程一个空字符串
STRSAFE_NO_TRUNCATION:同上

Tips:
详细的字符处理函数用法情参考msdn
https://msdn.microsoft.com/en-us/library/windows/desktop/ff468909(v=vs.85).aspx

格式化字符串

所谓字符串格式话,也就是让字符串安装一定的样子输出而已。
所以在使用c/c++标准的输出函数时包含stdio.h头文件(宽窄字符在时同意个头文件)

CRT标准中的格式化输出函数


int printf (const char * szFormat, ...) ;·
//
int sprintf (char * szBuffer, const char * szFormat, ...) ;

第一个参数是字符缓冲区;后面是一个格式字符串。sprintf不是将格式化结果标准输出,而是将其存入szBuffer。该函数返回该字符串的长度。在文字模式程序设计中

`printf ("The sum of %i and %i is %i", 5, 3, 5+3) ;

的功能相同于

char szBuffer [100] ;

sprintf (szBuffer, "The sum of %i and %i is %i", 5, 3, 5+3) ;


puts (szBuffer) ;

几乎每个人都经历过,当格式字符串与被格式化的变量不合时,可能使printf执行错误并可能造成程序当掉。使用sprintf时,您不但要担心这些,而且还有一个新的负担:您定义的字符串缓冲区必须足够大以存放结果。Microsoft专用函数_snprintf解决了这一问题,此函数引进了另一个参数,表示以字符计算的缓冲区大小。

vsprintf是sprintf的一个变形,它只有三个参数。vsprintf用于执行有多个参数的自订函数,类似printf格式。vsprintf的前两个参数与sprintf相同:一个用于保存结果的字符缓冲区和一个格式字符串。第三个参数是指向格式化参数数组的指针。实际上,该指针指向在堆栈中供函数呼叫的变量。va_list、va_start和va_end宏(在STDARG.H中定义)帮助我们处理堆栈指针。本章最后的SCRNSIZE程序展示了使用这些宏的方法。使用vsprintf函数,sprintf函数可以这样编写:

int sprintf (char * szBuffer, const char * szFormat, …)

{

int     iReturn ;

va_list pArgs ;

va_start (pArgs, szFormat) ;

iReturn = vsprintf (szBuffer, szFormat, pArgs) ;

va_end (pArgs) ;

return iReturn ;

}

va_start宏将pArg设置为指向一个堆栈变量,该变量地址在堆栈参数szFormat的上面。

由于许多Windows早期程序使用了sprintf和vsprintf,最终导致Microsoft向Windows API中增添了两个相似的函数。Windows的wsprintf和wvsprintf函数在功能上与sprintf和vsprintf相同,但它们不能处理浮点格式。

当然,随着宽字符的发表,sprintf类型的函数增加许多,使得函数名称变得极为混乱。表2-1列出了Microsoft的C执行时期链接库和Windows支持的所有sprintf函数。

ASCII宽字符常规
参数为普通参数
标准版sprintfswprintf_stprintf
最大长度版_snprintf_snwprintf_sntprintf
win32版wsprintfAwsprintfWwsprintf
参数为数组指针
标准版vsprintfvswprintfvstprintf
最大长度版_vsnprintf_vsnwprintf_vsntprintf
win32版wvsprintfAwvsprintfWwvsprintf

更多好用的字符串格式化函数请参考shlwapi.h头文件,可以参考下面两个链接
http://www.cnblogs.com/ubunoon/archive/2009/11/13/Shlwapi-Instruction.html

https://msdn.microsoft.com/zh-cn/library/ms538658.aspx

字符串的比较

win32API有两个重要的API比较函数CompareString和CompareStringOrdinal函数。

int CompareString(
  _In_ LCID    Locale,
  _In_ DWORD   dwCmpFlags,
  _In_ LPCTSTR lpString1,
  _In_ int     cchCount1,
  _In_ LPCTSTR lpString2,
  _In_ int     cchCount2
);

Locale
使用指定的区域设置ID(local id,ICID,也叫ocale identifiers)来比较两个字符串。
什么意思呢?这个ID是一个32位值,用来表示一个区域。也称作“本地化策略集”、“本地环境”,是表达程序用户地区方面的软件设定。区域设置的内容包括:数据格式、货币金额格式、小数点符号、千分位符号、度量衡单位、通货符号、日期写法、日历类型、文字排序、姓名格式、地址等等。如果获得线程的区域id可以使用getThreadLocal函数。
通过查看区域设置我们可以确定不同地区的文字排序格式。比如中国的区域ID为 zh-cn(0x0804),所以如果“中国”和“中华”比较的话会安拼音的数学进行比较的。

dwCmpFlags
一些以linguistic ,norm ,sort开头的宏可以参考https://msdn.microsoft.com/en-us/library/windows/desktop/dd317761(v=vs.85).aspx

还有一个CompareStringEx函数

Note For compatibility with Unicode, your applications should prefer CompareStringEx or the Unicode version of CompareString. Another reason for preferring CompareStringEx is that Microsoft is migrating toward the use of locale names instead of locale identifiers for new locales, for interoperability reasons. Any application that will be run only on Windows Vista and later should use CompareStringEx.

为了兼容Uncode,应用应该使用CompareStringEx 或者CompareStringW。使用CompareStringEx 的另一个原因是微软正在使用locale names 代替ocale identifiers称为新的lcid。因为interoperability (协作性?), 只运行在Windows Vista和之后的版本应该使用CompareStringEx

我们再来看看CompareStringOrdinal函数

WINBASEAPI
int
WINAPI
CompareStringOrdinal(
    _In_NLS_string_(cchCount1) LPCWCH lpString1,
    _In_ int cchCount1,
    _In_NLS_string_(cchCount2) LPCWCH lpString2,
    _In_ int cchCount2,
    _In_ BOOL bIgnoreCase
    );

首先你可以看见它只接受wch(wchar_t)类型的字符换哦。然后这个函数是按照码位比较的,也就是字符存储的而己之大小

CompareStringOrdinal和CompareString和CRT中的*cmp函数返回值( <0, ==0, and >0 )有点不一样

CSTR_LESS_THAN: lpString1 小于lpString2.定义为1
CSTR_EQUAL.:相同.定义为2
CSTR_GREATER_THAN.: lpString1 大于于lpString2.定义为3
0:调用失败
GetLastError可以得到下面的错误
ERROR_INVALID_FLAGS.dwCmpFlags参数有问题
ERROR_INVALID_PARAMETER.参数值无效

Unicode和ASCII字符转换

MultiByteToWideChar
可以把多字节字符转换为宽字符(Unicode就是一种宽字符)

WINBASEAPI
_Success_(return != 0)
         _When_((cbMultiByte == -1) && (cchWideChar != 0), _Post_equal_to_(_String_length_(lpWideCharStr)+1))
int
WINAPI
MultiByteToWideChar(
    _In_ UINT CodePage,
    _In_ DWORD dwFlags,
    _In_NLS_string_(cbMultiByte) LPCCH lpMultiByteStr,
    _In_ int cbMultiByte,
    _Out_writes_to_opt_(cchWideChar, return) LPWSTR lpWideCharStr,
    _In_ int cchWideChar
    );

CodePage
想被转换的多字节字符是那种类型的代码页,有很多参数可以参考下面的链接
https://msdn.microsoft.com/en-us/library/windows/desktop/dd319072(v=vs.85).aspx

Note The ANSI code pages can be different on different computers, or can be changed for a single computer, leading to data corruption. For the most consistent results, applications should use Unicode, such as UTF-8 or UTF-16, instead of a specific code page, unless legacy standards or data formats prevent the use of Unicode. If using Unicode is not possible, applications should tag the data stream with the appropriate encoding name when protocols allow it. HTML and XML files allow tagging, but text files do not.

在每一台计算机上的ANSI code pages可能是不同的,它可以被单独的计算机所更改,导致数据的损坏。所以为了一直性的结果,应用应该是用如UTF-8 or UTF-16的Unicode标准代替code pages,除非遗留的标准或者数据格式不能使用Unicode。如果我们不能使用Uncode。应用程序应该使用协议运行的编码名标记数据流。HTML 和 XML运行标记但是文本文件不允许标记

dwFlags
而外的的控制,比如影响重音符号什么的。

lpMultiByteStr
带转换的字符。
cbMultiByte
带转换字符的字节数,传入-1表示自动检查字符串多长

lpWideCharStr
转换完的字符串放在哪个缓冲区
cchWideChar
缓冲区的大小,传入0表示函数返回需要多少个wchar_t

所以一般情况下这样使用这个函数

  1. lpWideCharStr先闯入nullptr,cchWideChar传入0,cbMultiByte传入-1,函数返回缓冲区的大小A
  2. 分配一块sizeof(wcahr_t)*A大小的内存
  3. MultiByteToWideChar,设置上上面的参数,返回宽字符
  4. 使用转换完的Unicode字符
  5. 释放内存

同样的WideCharToMultiByte的参数和上面一样所以就不介绍了

【参考】
winodws 程序设计 第五版

C/C++ Language and Standard Libraries
https://msdn.microsoft.com/en-us/library/hh875057.aspx

Strings
https://msdn.microsoft.com/en-us/library/windows/desktop/ms646979(v=vs.85).aspx

CRT Alphabetical Function Reference
https://msdn.microsoft.com/en-us/library/634ca0c2.aspx

Tchar.h 中的一般文本映射
https://msdn.microsoft.com/zh-cn/library/c426s321.aspx

About Strings
https://msdn.microsoft.com/en-us/library/windows/desktop/ms647465(v=vs.85).aspx

Unicode in Visual C++ 2
https://msdn.microsoft.com/en-us/library/cc194799.aspx

通用 Windows 平台应用中不支持的 CRT 函数
https://msdn.microsoft.com/library/windows/apps/jj606124.aspx

[转]C++ Unicode SBCS 函数对照表
http://www.cnblogs.com/PiaoDbg/archive/2012/03/04/2379336.html

va_start和va_end使用详解
http://www.cnblogs.com/hanyonglu/archive/2011/05/07/2039916.html

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值