在C语言中使用中文,本地化全攻略

系列文章目录


本地化

很多人认为C语言只支持ascii码,这是误解,你用printf(“这是中文”)同样可以输出中文,用fputs(“C语言本地化”)也可以向文件写入中文,那么C语言默认使用什么编码呢?这个问题不是那么简单,因为C标准并未规定使用什么编码,实际编码与操作系统、所在区域、编译器有很大的关系。另外可能你会发现虽然可以输出中文字符串,但并不能用char c=’中’来声明一个中文字符,如果用strlen(“C语言”)来求字符串长度,给出的值可能是5,因为编码是变长的,默认情况下C并不能很好的支持中文。如何让C语言很好的支持中文呢?这需要我们从基本编码概念讲起。

字符编码

计算机刚刚发明时,只支持ascii码,也就是说只支持英文,随着计算机在全球兴起,各国创建了属于自己的编码来显示本国文字,中文首先使用GB2132编码,它收录了6763个汉字,平日里我们工作学习大概只会用到3千个汉字,因此日常使用已经足够,GBK收录了21003个汉字,远超日常汉字使用需求,不管是日用还是商用都能轻松搞定,因此从windows95开始就将GBK作为默认汉字编码,而GB18030收录了27533个汉字,为汉字研究、古籍整理等领域提供了统一的信息平台基础,平时根本使用不到这种编码。这些中文编码本身兼容ascii,并采用变长方式记录,英文使用一个字节,常用汉字使用2个字节,罕见字使用四个字节。后来随着全球文化不断交流,人们迫切需要一种全球统一的编码能够统一世界各地字符,再也不会因为地域不同而出现乱码,这时Unicode字符集就诞生了,也称为统一码,万国码。新出来的操作系统其内核本身就支持Unicode,由万国码的名称就可以想象这是一个多么庞大的字符集,为了兼容所有地区的文字,也考虑到空间和性能,Unicode提供了3种编码方案:
utf-8 变长编码方案,使用1-6个字节来储存
utf-32 定长编码方案,始终使用4个字节来储存
utf-16 介于变长和定长之间的平衡方案,使用2个或4个字节来储存
utf-8由于是变长方案,类似GB2132和GBK量体裁衣,最节省空间,但要通过第一个字节决定采用几个字节储存,编码最复杂,且由于变长要定位文字,就得从第一个字符开始计算,性能最低。utf-32由于是定长方案,字节数固定因此无需解码,性能最高但最浪费空间。utf-16是个怪胎,它将常用的字放在编号0 ~ FFFF之间,不用进行编码转换,对于不常用字的都放在10000~10FFFF编号之后,因此自然的解决变长的问题。注意对于这3种编码,只有utf-8兼容ascii,utf-32和utf-16都不支持单字节,由于utf-8最省流量,兼容性好,后来解码性能也得到了很大改善,同时新出来的硬件也越来越强,性能已不成问题,因此很多纯文本、源代码、网页、配置文件等都采用utf-8编码,从而代替了原来简陋的ascii。再来看看utf-16,对于常见字2个字节已经完全够用,很少会用到4个字节,因此通常也将utf-16划分为定长,一些操作系统和代码编译器直接不支持4字节的utf-16。Unicode还分为大端和小端,大端就是将高位的字节放在低地址表示,后缀为BE;小端就是将高位的字节放在高地址表示,后缀为LE,没有指定后缀,即不知道其是大小端,所以其开始的两个字节表示该字节数组是大端还是小端,FE FF表示大端,FF FE表示小端。Windows内核使用utf-16,linux,mac,ios内核使用的是utf-8,我们就不去争论谁好谁坏了。另外虽然windows内核为utf-16,但为了更好的本地化,控制面板提供了区域选项,如果设置为简体就是GBK编码,在win10中,控制台和记事本默认编码为gbk,其它第三方软件就不好说了,它们默认编码各不相同。

了解编码后下面来说说BOM,一个文本文件,可以是纯文本、网页、源码等,那么打开它的程序如何知道它采用什么编码呢?为了说明一个文件采用的是什么编码,在文件最开始的部分,可以有BOM,比如0xFE 0xFF表示UTF-16BE,0xFF 0xFE 0x00 0x00表示UTF-32LE。UTF-8原本是不需要BOM的,因为其自我同步的特性,但是为了明确说明这是UTF-8而不是让文本编辑器去猜,也可以加上UTF-8的BOM:0xEF 0xBB 0xBF。

C语言字符编码

C语言源代码采用什么格式取决于IDE环境,通常是utf-8或ANSI,什么是ANSI编码呢?相比unicode它是采取另一种思路,严格来说ANSI不是一种编码,而是一种替代方案,力求找到显示内容的最低编码需求,如果内容只有英文字符就使用ascii,如果发现汉字就替换成本地的GBK编码,如果发现既有汉字又有日语又有韩语是否会自动选择unicode这个没有试过。前面讲过C语言使用的编码和操作系统、区域选择、编译器都有关系,但有一个现象,通常源码采用什么格式的编码,运行时就使用这样的编码,因此我们可以通过源代码先看看这个IDE会使用什么编码。在Dev C++中测试的结果是,如果只有英文源码默认采用utf8,现在的编辑器很少使用纯ascii了,如果发现源码里面有汉字,则将编码改为ANSI,由于windows控制台默认也使用ANSI,因此可以显示标准输入输出中的汉字。

如果只需要向控制台输出一段字符串或者向文件中写入一段中文,那么使用标准输入输出函数即可,因为printf()和puts()可以识别字符串使用的编码,但如果要操作单个字符就不行了,因为GBA和GBK都是变长的,一个英文字母使用一个字节,而一个中文使用2-4个字节,用char c=’中’是不行的,因为char只能是一个字节。再来看看字符串,虽然可以放入中文,但却不能访问数组元素,因为数组元素也只能是一个char,数组元素个数和字符串个数是不对应的,这是历史遗留的问题,无法更改,否则新标准就不能兼容之前的代码了。要处理中文字符,只能另辟途径,一是将编码从变长改为定长,二是使用新的字符类型来处理定长编码,对于变长和定长,在计算机行业种还有一个术语称为窄字符和宽字符,从前面知识可以得知,能够显示各个国家的语言并且采用定长的只有utf-16和utf-32了,C语言为宽字符提供新的类型wchar,这个类型由wchar库提供,导入wchar.h后就可以使用宽字符了,宽字符使用utf-16还是utf-32由编译器决定,windows中宽字符使用utf-16,linux则使用utf-32,我们可以通过以下代码进行测试,如下:

#include <stdio.h>
#include <wchar.h>

int main()
{
   
	wchar_t wc1=L'a';
	wchar_t wc2=L'中';
	printf("%d,%d\n",sizeof(wc1),sizeof(wc2));
	printf("%x,%x\n",wc1,wc2);
	return 0;
}

上面代码使用wchar_t声明一个宽字符,宽字符前面要添加L,L是有个宏,它将后面的内容转为宽字符或宽字符串,这里使用sizeof()检测的结果是无论中文还是英文都是2字节,说明编码使用的是utf-16,后面一个printf()将编码以十六进制输出。至于utf-16和utf-32哪个好我们也不去争论了,巨头们都很任性且互不买账。

如果要显示一个宽字节符,需要换成putwchar()和wprintf()函数,如下:

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main()
{
   
	setlocale(LC_ALL, "");
	wchar_t wc1=L'a';
	wchar_t wc2=L'中';
	putwchar(wc1);
	putwchar(wc2);
	wprintf(L"%c%c",wc1,wc2);
	return 0;
}

这两个方法需要先调用setlocale()函数进行初始化,设置Unicode区域,setlocale()的格式为:
char* setlocale (int category, const char* locale)
category是类型,表示区域编码影响到的类型,类型有时钟、货币、字符排序等,通常设置为常数LC_ALL表示影响所有类型。locale表示区域,windows和linux表示区域的方式各不相同,例如windows表示简体中文用”chs”,linux用”zh_CN”,但有3个区域总是相同的:”C”表示中立地域,不表示任何一个地区,只对小数点进行了设置,是默认值;“”表示本地区域;NULL表示不指定区域仅仅返回区域信息,可以通过puts(setlocale(NULL,""))输出本地区域信息,中文简体为Chinese (Simplified)_China.936。如果不设置区域,默认区域为”C”,这个区域只能显示英文,无法显示任何中文字符,实际测试结果也是如此。由于setlocale()包含在locale.h中,因此要先导入locale库,下面我们再来看看宽字符串的定义和输出:

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main()
{
   
	setlocale(LC_ALL, "");
	wchar_t wstr[]=L"宽字符串";
	wprintf(L"%s,%c\n",wstr, wstr[1]);
	printf("%d",wcslen(wstr));
	return 0;
}

声明宽字符串后应该使用wcslen()函数获取字符串的长度,使用下标和指针运算都能正确定位,这就是采用定长的原因。除此之外,字符串的复制、连接等都有配套的宽字符串操作方法,相关方法可以查询C语言函数库。需要清楚的是,传统的字符串和相应的方法现在归于窄字符范畴,使用L前缀的字符串和带w的函数属于宽字符串范畴,它们的类型和处理方式都不相同,不能混用。

宽字符的实现原理

当我们用大写的L标记一个宽字符串时,这个字符串编码会以utf-16或utf-32储存,然而输出字符串时控制台或文本不一定是utf-16和utf-32,因此在输出时会转化为窄字符使用的编码,转化的依据是setlocale()中对宽字符编码的区域的设定,例如setlocale()将区域设定为本地,本地编码为简体中文,那么在windows中运行时会将zh-cn的unicode转为gbk,在linux中运行会转化为utf-8,实际上无论将控制台设置为什么编码,宽字符都能正常显示。这种编码的转换被写入到stout中,因此调用setlocale()后除了影响宽字符还影响窄字符,GCC就是一个例子,GCC对宽字符的支持很不友好,如果代码为ANSI或GBK,则以L开头的宽字符或宽字符串不能通过编译,因为linux下的GCC在默认情况下总是假设源代码的编码是utf-8,只有将源码格式设置为utf-8才能通过编译,下图是将使用了minGW的CDT源码格式改为utf-8:
在这里插入图片描述

如果将源码改为utf-8,则编译后输出的编码也为utf-8,还需要将控制台编码设置为utf-8才能显示窄字符,宽字符依然需要调用setlocale()设置区域,然而更改区域又会影响窄字符的显示,要同时显示窄字符和宽字符只能每次显示之前先设置区域,如下:

#include <stdio.h>
#include<Windows.h>
#include<locale.h>

int main()
{
   
	system("chcp 936");//将控制台编码设置为gbk
	puts("乱码");
	system("pause");
	system("chcp 65001");//将控制台编码设置为utf8
	printf("%s\n", "显示窄字符utf8");
	setlocale(LC_ALL, "");
	printf("%ls\n",L"显示宽字符utf16");
	
	setlocale(LC_ALL, "C");
	puts("还原区域显示窄字符");
	return 0;
}

在windows控制台中执行chcp可以设置控制台字符编码,这里使用system(“chcp 936”)来调用这个命令将控制台编码设置为gbk,由于gcc输出编码为utf-8,因此在gbk下呈现乱码,接着调用system(“chcp 65001”)将控制台编码设置为utf8发现能够正常显示。调用setlocale(LC_ALL, “”)后会同时影响宽字符和窄字符,因此输出窄字符又呈现乱码,在后面调用setlocale(LC_ALL, “C”)还原默认的区域后窄字符能够正确显示。这段代码还不能在CDT的控制台中运行,只能在windows的控制台中测试,因为eclipse控制台采用的编码和windows控制台又不同,使用minGW的CDT无论编译还是输出对本地化的支持都不是很友好。相反在高版本的VS(Visual Studio)中很少碰到显示乱码问题,调用setlocale()设置宽字符区域不会影响窄字符的显示,将控制台设置为utf-8或ANSI都能正确显示,因为对于窄字符无论如何设置VS都会尝试将编码转化为控制台使用的编码输出。不过现在我们明白宽字符的运作原理了,那就是编译时将文本转为unicode-16或unicode-32的宽字符,运行时将unicode-16或unicode-32转化为ANSI或utf-8的窄字符。运行时是否能正确显示,是否有乱码要看编译器将宽字符或窄字符呈现为控制台显示编码的能力。宽字符是对传统字符的扩展,在处理上采用完全不同的方案,因此在使用上窄字符和宽字符相互独立,C代码中可以同时存在两套编码,互不影响,对应的处理函数也不同。

窄字符和宽字符处理函数

由于C语言标准并未为宽字符制定统一的标准,这导致宽字符非常依赖编译器和库,一些编译器对宽字符支持很不友善,一些甚至根本不支持宽字符,但宽字符仍然是趋势所在,这是国际化所需的,在很多高级语言中已经不区分宽字符和窄字符,只关心字符所用的编码。如果你使用C语言只是利用它优越的性能写一些算法,那么你可以不用关心本地化问题;如果你的程序涉及到多字节文本处理、文本读写就不可能回避本地化问题,此时统一使用宽字符是一个很好的解决方案,混用宽字符和窄字符只会让你的代码更难维护,下面我将宽字符函数按功能分类并且对照窄字符函数列出,以供参考。注意左边带w的为宽字符处理函数,右边为窄字符函数,None表示没有对应的函数。

字符类型

iswalnum() isalnum() 测试字符是否为数字或字母
iswalpha() isalpha() 测试字符是否是字母
iswcntrl() iscntrl() 测试字符是否是控制符
iswdigit() isdigit() 测试字符是否为数字
iswgraph() isgraph() 测试字符是否是可见字符
iswlower() islower() 测试字符是否是小写字符
iswprint() isprint() 测试字符是否是可打印字符
iswpunct() ispunct() 测试字符是否是标点符号
iswspace() isspace() 测试字符是否是空白符号
iswupper() isupper() 测试字符是否是大写字符
iswxdigit() isxdigit() 测试字符是否是十六进制的数字

大小写转换

towlower() tolower() 把字符转换为小写
towupper() toupper() 把字符转换为大写
wcslwr() strlwr() 把字符串转换为小写
wcsupr() strupr() 把字符串转换为大写

数值转换

None atoi() 把字符串转换为整数
wcstol()/wcstoll() atol()/strtol()/strtoll() 把字符串转换为长整数
wcstoul()/wcstoull() strtoul()/strtoull() 把字符串转换为无符号长整形
wcstof() atof() 把字符串转换为单精度浮点
wcstod() strtod() 把字符串转换为双精度浮点

None itoa() 把整数转换为字符串
None ltoa()/lltoa() 把长整数转换为字符串
None ultoa()/ulltoa() 把无符号长整形转换为字符串
None gcvt()/ecvt()/fcvt() 将浮点转换为字符串

字符串操作

wcslen() strlen() 获得字符串的字符个数
wcscat() strcat() 把一个字符串接到另一个字符串的尾部
wcsncat() strncat() 把一个字符串接到另一个字符串的尾部且指定连接长度.
wcscpy() strcpy() 拷贝字符串
wcsncpy() strncpy() 拷贝字符串同时指定拷贝的字符个数
wcschr() strchr() 查找子字符的第一个位置
wcsrchr() strrchr() 从尾部开始查找子字符出现的第一个位置
wcspbrk() strpbrk() 从一字符字符串中查找另一字符串中任何一个字符第一次出现的位置
wcswcs() / wcsstr() strstr () 在一字符串中查找另一字符串第一次出现的位置
wcsspn() strspn() 返回包含第二个字符串的初始数目
wcscspn() strcspn() 返回不包含第二个字符串的的初始数目
wcscmp() strcmp() 比较两个字符串
wcsncmp() strncmp() 比较两个字符串还要指定比较的数目
wcscoll() strcoll() 根据LC_COLLATE比较字符串
wcstok() strtok() 根据标示符把宽字符串分解成一系列字符串

日期和时间转换

strftime() 根据指定的字符串格式和locale设置格式化日期和时间
strptime() 根据指定格式把字符串转换为时间值, 是strftime()的反过程
wcsftime() 根据指定的字符串格式和locale设置格式化日期和时间, 并返回宽字符串

输入和输出

wprintf() printf() 格式化输出到标准输出
wscanf() scanf() 从标准输入的格式化读入
fwprintf() fprintf() 格式化输出到文件
fwscanf() fscanf() 格式化文件读入
swprintf() sprintf() 格式化成字符串
swscanf() swscanf() 以字符串作格式化读入
vwprintf() vprintf () 使用stdarg参量表格式化输出到标准输出
vfwprintf() vfprintf() 使用stdarg参量表格式化输出到文件
vswprintf() vsprintf() 使用stdarg参量表格式写入到字符串
getwc()/getwchar() getc()/getchar() 从标准输入中读取字符
putwc()/putwchar() putc()/putchar() 将字符输出到标准输出
None gets() 获取字符串
None puts() 输出字符串
ungetwc() ungetc() 把字符放回到输入流中
fgetwc() fgetc() 从文件流中读入一个字符
fputwc() fputc() 把字符转输出到文件流
fgetws() fgets() 从文件流中读入一个字符串
fputws() fputs() 把字符串输出到文件流

内存操作

wmemcpy() memcpy() 内存快复制
wmemmove() memmove() 用于内存块重叠时的复制
wmemchr() memchr() 在内存块中查找字符
wmemcmp() memcmp() 比较内存块
wmemset() memset() 将某一块内存中的内容全部设置为指定的值

安全函数

wprintf_s() printf_s() 安全输出到标准输出流
wscanf_s() scanf_s() 从标准输入流安全输入
sprintf_s() swprintf_s() 安全输出到字符串
sscanf_s() swscanf_s() 安全输入到字符串
fwprintf_s () fprintf_s() 安全输出到文件流
fwscanf_s() fscanf_s() 从文件流安全输入
wcscpy_s() strcpy_s() 安全复制字符串
_wfopen_s() fopen_s() 安全打开文件

前面讲过宽字符和编译器有很大关系,因此这些列出的函数可能和你的编译器有出入,有的并不被支持,有的可能还未列出,但一些常用的宽字符函数,例如wprintf(), getwchar()是一样的,可以注意到宽字符没有对应的gets(),puts()方法,只能使用wprintf()和wscanf(),我想可能是因为不好处理中文空格这样的问题吧。printf()也提供了支持宽字符的格式”%lc”和”%ls”,例如:

#include <stdio.h>
#include<Windows.h>
#include<locale.h>

int main()
{
   
	setlocale(LC_ALL, "");
	printf("%s", "窄字符\n");
	printf("%ls",L"宽字符\n");
	return 0;
}

可以看出宽字符处理函数名就是将窄字符名中加上w或者将str替换成wcs,相应的头文件也是这样,例如ctype.h对应的宽字符库为wctype.h,string.h对应的宽字符库为cstring.h。

长度问题

洞悉了相关处理函数后,我们第一个要解决的问题就是长度问题,长度问题涉及字符长度和字符串长度、窄字符和宽字符长度、中文和英文长度。由于窄字符采用变长,因此有的书籍也将窄字符称为多字节字符,对于多字节字符来说英文和中文所需的字节长度是不同的,宽字符由于平台不同,字符长度也不同,我们通过下面代码进行测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
   
	setlocale(LC_ALL, "");
	printf("%d\n", MB_CUR_MAX);//窄字符的最大字符字节长度

	char c[] = "中";
	printf("%d,%d\n", mblen(c, sizeof(c)), sizeof(c)); //获得窄字符字节长度
	char e[] = "z";
	printf("%d,%d\n", mblen(e, sizeof(e)), sizeof(e)); //获得窄字符字节长度

	wchar_t w = L'z';
	printf("%d\n", sizeof(w));//获取宽字符英文字节长度
	w = L'中';
	printf("%d\n", sizeof(w));//获取宽字符中文字节长度

	char mbs[] = "窄字符abc";
	printf("%u,%u\n", strlen(mbs), sizeof(mbs));//获取窄字符串长度和字节长度

	wchar_t wcs[] = L"宽字符abc";
	printf("%u,%u\n", wcslen(wcs), sizeof(wcs));//获得宽字符串长度和字节长度

	char* p = "abcde";
	wchar_t* p1 = L"abcde";
	printf("%d,%d\n",strlen(p),wcslen(p1));//获得常量字符串长度
	printf("%d,%d\n", sizeof(p), sizeof(p1));//不能对常量字符串指针使用sizeof()求字节长度

	char p2[] = "";
	wchar_t p3[] = L"";
	printf("%d,%d\n", sizeof(p2), sizeof(p3)); //测试空字符串字节长度
	printf("%d, %d\n", sizeof(EOF), sizeof(WEOF));//测试结尾符号字节长度
	return(0);
}

在代码printf("%d\n", MB_CUR_MAX);中,MB_CUR_MAX是stdlib提供的宏,它表示窄字符中最大字符长度,这个宏和操作系统默认语言编码有关,对于GBK来说是2个字符。接下来两行代码用实际字符测试窄字符的长度,由于中文不能用一个char表示,因此窄字符都用字符串表示,字符串后面会多一个“\0”,因此stdlib库提供了更加方便的mblen()函数来检测字符长度,它有两个参数:一个参数是字符串地址,另一个参数是字符串字节长度,测试一下看看是否和MB_CUR_MAX显示的结果相同。从结果可以看到,对于变长的窄字符,英文使用1个字节储存,中文使用2个字节储存。接下来我们测试宽字符字节长度,由于宽字符采用定长,因此中英文的字符字节长度是一样的。最后我们测试字符串长度,可以看到窄字符由于采用变长,其字符串的长度和字节长度都不能作为字符个数的依据,因此包含中英文的窄字符串是无法截取其中内容的,这就是为什么窄字符的函数只适合处理单字节ascii码的原因。宽字符由于采用定长,其字符串长度和字节数都和字符个数相关,可以通过数组索引截取内容。接着我们来测试下字符串常量,结果是字符个数可以正常获取了,但用sizeof()获得的是指针变量的字节大小,因为p不是字符串的集合名称。最后我们测试一下空字符串字节大小,宽字符结束符号也是\0,但它占用两个字节,用L’\0’表示,因此空字符串也占据2个字节。表示文件尾的窄字符常量为EOF,占用2个字节,宽字符常量为WEOF,占用4个字节。通过一系列详细的测试,可以看到窄字符和宽字符都能储存中文,但由于窄字符是变长字符,当包含中文时处理起来会遇到问题,不仅无法使用窄字符处理函数,还因为编译器自动转码能力,能否正常显示也是一个问题。宽字符由于是定长,加上配套完整的处理函数,定位和处理起来都不会有问题,当通过setlocale()函数明确指定区域后,编译器都能正常的转换编码。

截取宽字符串

在字符串处理中,最常见的操作是截取字符串和连接字符串,宽字符由于采用定长,截取字符串的思路和截取数组相同,最笨的方法莫过于使用循环将字符一个个抠出来组成新的字符串,如同下面代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
   
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"牛奶与coffee";
	int startIndex = 3, len = 6;

	wchar_t str2[20] = L"";
	int i = 0;
	for (i = 0; i < len; i++)  str2[i] = str1[i+startIndex];
	wprintf(L"%s\n",str2);
	return(0);
}

这段代码一旦被老程序员看到,你的水平他就心知肚明了,因为这样写违背了C语言简洁高效的原则,简洁是指代码简明,高效指性能优越。现成的方法摆在面前不用非要绕着弯子重新造车,只能说明你基本函数库都没有学好,简洁高效的代码应该是这样:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<locale.h>

int main()
{
   
	setlocale(LC_ALL, "");
	wchar_t str1[] = L"牛奶与coffee";
	wchar_t str2[20] = L"";
	wcsncpy(str2, str1 + 3, 6);
	wprintf(L"%s\n",str2);
	return(0);
}

如果不需要生成新的字符串,那么可以直接写成:
wprintf(L"%s\n",str1+3);
当然这只适合截取到尾部的情况,我这里要表达的意思是:请善用指针!

连接宽字符串

将两个字符串连接起来也是最常用的操作之一,通过上面截取宽字符串的例子,你可能已经放弃了循环操作字符这种低效的方式&#x

  • 14
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值