C语言-文件系统详解

为什么使用文件

写通讯录程序时,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录,那么每次运行通讯录程序时都需要重新录入,会很麻烦。因此通讯录就应该把信息记录下来,只有主动删除数据时,数据才会不复存在,这就涉及数据持久化的问题。

一般数据持久化的方式有:把数据存放在磁盘文件、存放到数据库等方式。

什么是文件

磁盘上的文件是文件 :当把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中,这时候处理的就是磁盘上文件。

不过程序设计中,从功能角度来看,一般文件分为两种:程序文件、数据文件

(1)程序文件:windows环境中的源程序文件(.c)、目标文件(.obj)、可执行程序(.exe)。

(2)数据文件:文件内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本博客主要讲述数据文件。

文件的打开和关闭

 文件名作为文件的唯一标识,包含3部分:文件路径+文件名主干+文件后缀

如:C:\code\test.txt

另外,程序员应有的安全感就是把文件扩展名显示出来

 文件的名称是以显示文件扩展名为准的,如不勾选文件扩展名时,文件名称为file.txt

 如果勾选文件扩展名,那么文件名称为file.txt.txt,这时候如果想打开file.txt文件,那么无论如何都会失败。

 (1)文件类型指针

简称“文件指针”,每个被使用的文件,都在内存中开辟了相应的文件信息区,用来存放文件的相关信息(包括文件名、文件状态、文件位置等)。这些信息保存在FILE结构体变量中,FILE结构体类型是系统声明的。

struct _iobuf 
{
	char* _ptr;
	int   _cnt;
	char* _base;
	int   _flag;
	int   _file;
	int   charbuf;
	int   _bufsiz;
	char* _tmpfname;
};
typedef struct _iobuf FILE;

 不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充相应信息,使用时不必关心细节。

一般都是通过一个 FILE 的的指针来维护FILE结构变量,使用起来更加方便。

创建FILE*的指针变量:

FILE* pf;//文件指针变量
这就定义了pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过文件信息区中的信息就能够访问该文件,即通过文件指针变量能够找到与它关联的文件。

(2)文件的打开和关闭

文件在读写之前,应该先打开文件,在使用结束之后应该关闭文件。
在写程序时,打开文件的同时,都会返回一个FILE指针变量,指向该文件,这就机那里了指针和文件的关系。
ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。
    //打开文件
	FILE* open(const char* filename, const char* mode);
	//关闭文件
	int close(FILE * stream);

程序和文件读取、写入示意图:

打开方式有多种:

文件使用方式

含义

如果指定文件不存在

“r”(只读)

为了使用数据,打开一个已经存在的文本文件

出错

“w”(只写)

为了输出数据,打开一个文本文件

建立一个新的文件

“a”(追加)

向文本文件末尾添加数据

出错

“rb”(只读)

为了输入数据,打开一个二进制文件

出错

“wb”(只写)

为了输出数据,打开一个二进制文件

建立一个新的文件

“ab”(追加)

像一个二进制文件末尾添加数据

出错

“r+”(读写)

为了读和写,打开一个文本文件

出错

“w+”(读写)

为了读和写,建立一个新的文件

建立一个新的文件

“a+”(读写)

打开一个文件,在文件末尾进行读写

建立一个新的文件

“rb+”(读写)

为了读和写打开一个二进制文件

出错

“wb+”(读写)

为了读和写,新建一个新的二进制文件

建立一个新的文件

“ab+”(读写)

打开一个二进制文件,在文件末尾进行读和写

建立一个新的文件

在VS2019 解决方案下手动创建文件test.data

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("test.data", "r");

	//文件操作
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fputs("fopen test", pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

如果使用绝对路径打开文件,向test.data文件里面写东西

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("C:\Users\Delia\source\repos\bit4\test.data", "w");//使用绝对路径打开文件

	//文件操作
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fputs("fopen test", pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

路径会报错:

 这是因为路径没有使用转义字符,会把路径中的\r 、\b、\t当做转义字符来处理,路径就找不到了,路径需要加上转义字符

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");//路径加上转义字符

	//文件操作
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fputs("fopen test", pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

ctrl+F5运行之后, 打开test.data文件,写入成功:

文件的顺序读写

功能

函数名

适用于

字符输入函数(读取)

fgetc

所有输入流

字符输出函数(写入)

fputc

所有输出流

文本行输入函数(读取)

fgets

所有输入流

文本行输出函数(写入)

fputs

所有输出流

格式化输入函数(读取)

fscanf

所有输入流

格式化输出函数(写入)

fprintf

所有输出流

二进制输入(读取)

fread

文件

二进制输出(写入)

fwrite

文件

 流的含义:

 

 清除test.data的内容:使用w方式打开文件,并且不写入,会将文件中的内容清空

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");

	//文件操作
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

 ctrl+F5运行,打开test.data文件,内容被清空:

fputc

int fputc ( int character, FILE * stream );//写入成功,则返回写入的字符

fputc支持所有输出流,所以fputc向文件和屏幕上都可以写入内容

(1)向文件写入内容:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");

	//文件操作
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	fputc('h', pf);
	fputc('e', pf);
	fputc('l', pf);
	fputc('l', pf);
	fputc('o', pf);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

 ctrl+F5运行,打开test.data文件,结果为:

 (2)向屏幕输出信息:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	fputc('h', stdout);
	fputc('e', stdout);
	fputc('l', stdout);
	fputc('l', stdout);
	fputc('o', stdout);
}

ctrl+F5运行,将hello打印在屏幕上:

fgetc

int fgetc ( FILE * stream );//读取成功则返回读取到的字符

 fgetc支持所有输入流,fgetc从文件和屏幕上都可以读取内容

 (1)fgetc从文件里面读取字符,先手动将test.data里面的内容改为world

 读取文件内容前3个字符:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	//打开文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");//读取

	//文件操作
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	int ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);
	ret = fgetc(pf);
	printf("%c", ret);

	return 0;
}

 ctrl+F5运行,读取到前3个字符

(2)fgetc也可以从标准输入读取字符

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	int ret = fgetc(stdin);
	printf("%c", ret);
	ret = fgetc(stdin);
	printf("%c", ret);
	ret = fgetc(stdin);
	printf("%c", ret);

	return 0;
}

  ctrl+F5运行,输入字符串,读取到前3个字符

fputc和fgetc每读一次,相当于指针+1, 读取结束会返回EOF(-1)。

fputs

int fputs ( const char * str, FILE * stream );//以文本(ASCII码)方式写入成功则返回一个非负值

fputs向所有输出流写入文本行

(1)向文件test.data文件中写入字符串:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");
	if(pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//写文件-按照行来写
	fputs("abcd", pf);
	fputs("efg", pf);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

 ctrl+F5运行,两行字符串写入到test.data文件中。

 如果需要换行,写入的字符串末尾加上换行符即可:

    //写文件-按照行来写
	fputs("abcd\n", pf);
	fputs("efg", pf);

 

 fgets

 char * fgets ( char * str, int num, FILE * stream );//以文本方式读取字符,num是最大读取字符数,不定义时为99,最后一个为\0。

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	char arr[10] = { 0 };
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//读文件
	fgets(arr,4, pf);//实际上最多读取3个字符,最后一个字符是\0
	printf("%s\n", arr);
	
	fgets(arr, 4, pf);//实际上最多读取3个字符,最后一个字符是\0
	printf("%s\n", arr);

	fgets(arr, 4, pf);//实际上最多读取3个字符,最后一个字符是\0
	printf("%s\n", arr);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

 ctrl+F5运行,结果如下:

fprintf

 把格式化数据写入到流中

int fprintf ( FILE * stream, const char * format, ... )//读取成功就返回读取到的字符个数,否则返回负数

 返回值为int的原因有两个:字符是用ASCII码表示的;函数在读取失败会返回EOF(-1),char(1-255)没办法存-1,int可以存-1。

fprintf和printf对比,fprintf比printf多了一个参数:FILE流参数。

 使用fprintf把格式化数据写入到流中

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

struct S
{
	char c[20];
	int i;
	float f;
};

int main()
{
	struct S s = { "abcdef",10,5.5f };
	
    //对格式化的数据进行写文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}

	//写文件
	fprintf(pf, "%s %d %f", "hello", 5, 2.6);

    //关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

 ctrl+F5,查看运行结果:

 fscanf

 从流中读取格式化数据

int fscanf ( FILE * stream, const char * format, ... );//根据格式读取并存储流中的数据

 fscanf和scanf对比,fscanf比scanf多了一个参数:FILE流参数。

 使用fscanf把格式化数据读取到流中

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

struct S
{
	char c[20];
	int i;
	float f;
};

int main()
{
	struct S s = { "abcdef",10,5.5f };
	//对格式化的数据进行写文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");//读取
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}

	//写文件
	fscanf(pf, "%s %d %f", s.c, &(s.i), &(s.f));
	
	 //打印数据
	//printf("%s %d %f", s.c, s.i, s.f);
	fprintf(stdout,"%s %d %f", s.c, s.i, s.f);//使用fprintf函数把数据输出到标准屏幕,作用同上一句printf

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

 ctrl+F5,查看运行结果:

 fwirte

以二进制的形式把count个大小为size的数据写入到流中

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );//返回成功写入的元素个数

使用fwrite把数据块写入到流中:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

struct S
{
	char c[20];
	int i;
	float f;
};

int main()
{
	struct S s = { "abcdef",10,5.5f };
	//数据块进行写文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "w");//写入
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}

	//写文件
	fwrite(&s, sizeof(struct S), 1, pf);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

 ctrl+F5,打开test.data文件,发现字符串以二进制的形式和以文本的形式写进去的内容是一样的,都是abcdef,整数和浮点数以二进制的形式和以文本的形式写进去的内容是不一样的:

 

 fread

从流中读取count个大小为size的数据到buffer里面去

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );//返回成功读取的元素个数

使用fread从流中读取数据块

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

struct S
{
	char c[20];
	int i;
	float f;
};

int main()
{
	struct S s = { "abcdef",10,5.5f };
	//数据块进行写文件
	FILE* pf = fopen("C:\\Users\\Delia\\source\\repos\\bit4\\test.data", "r");//读取
	if (NULL == pf)
	{
		perror("fopen");
		return 1;
	}

	//读文件
	fread(&s, sizeof(struct S), 1, pf);
	printf("%s %d %f", s.c, s.i, s.f);

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

ctrl+F5运行,结果如下:

 sprintf

 把格式化数据写入到字符串

int sprintf ( char * str, const char * format, ... );//执行成功则返回写入的字符个数

 sprintf和printf对比,sprintf比printf多了一个参数:字符串

 

 sscanf

 从字符串读取格式化数据

int sscanf ( const char * s, const char * format, ...);//返回成功读取的字符的个数

 sscanf和scanf对比,sscanf比scanf多了一个参数:字符串

 使用sprintf 先把一个格式化的数据,转换成字符串,再使用sscanf从字符串中还原出一个结构体数据

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

struct S
{
	char c[20];
	int i;
	float f;
};

int main()
{
	struct S s = { "abcdef",10,5.5f };
	struct S tmp = { 0 };

	char buf[100] = { 0 };
	
	//sprintf 把一个格式化的数据,转换成字符串
	sprintf(buf, "%s %d %f", s.c, s.i, s.f);
	printf("%s\n", buf);//以字符串形式打印
	
	//sscanf从字符串中还原出一个结构体数据
	sscanf(buf, "%s %d %f", tmp.c, &(tmp.i), &(tmp.f));
	printf("%s %d %f\n", tmp.c,tmp.i,tmp.f);

	return 0;
}

ctrl+F5运行,结果如下:

对比scanf/fscanf/sscanf 和 printf/fprintf/sprintf

 

文件的随机读写

 文件指针在读文件时,定位文件内容不断发生变化,如果不想顺序读取呢?可以进行文件随机读写

fseek

根据文件指针的位置和偏移量来定位文件指针

int fseek ( FILE * stream, long int offset, int origin );//成功则返回0,否则返回非0

test.data中存放的是abcdef

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.data", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//读取文件
	int ch = fgetc(pf);
	printf("%c\n", ch);//a

	//调整文件指针
	fseek(pf, 1, SEEK_CUR);//当前指针指向b,向后偏移一位,指向c

	ch = fgetc(pf);
	printf("%c\n", ch);//c

	fclose(pf);
	pf = NULL;
	return 0;
}

 ctrl+F5运行,结果如下所示:

 ftell

返回文件指针相对于起始位置的偏移量

long int ftell ( FILE * stream );//若成功则返回指针位置的值
#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.data", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//读取文件
	int ch = fgetc(pf);
	printf("%c\n", ch);//a

	ch = fgetc(pf);
	printf("%c\n", ch);//b

	ch = fgetc(pf);
	printf("%c\n", ch);//c

	//调整文件指针
	fseek(pf, -2, SEEK_CUR);

	ch = fgetc(pf);
	printf("%c\n", ch);//b

	//计算指针相对于起始位置的偏移
	int ret = ftell(pf);
	printf("%d\n", ret);

	fclose(pf);
	pf = NULL;
	return 0;
}

 ctrl+F5运行,结果如下所示:

 rewind

让文件指针的位置回到文件的起始位置

void rewind ( FILE * stream );
#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	FILE* pf = fopen("test.data", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//读取文件
	int ch = fgetc(pf);
	printf("%c\n", ch);//a

	ch = fgetc(pf);
	printf("%c\n", ch);//b

	ch = fgetc(pf);
	printf("%c\n", ch);//c

	//调整文件指针
	fseek(pf, -2, SEEK_CUR);

	ch = fgetc(pf);
	printf("%c\n", ch);//b

	//计算指针相对于起始位置的偏移
	int ret = ftell(pf);
	printf("%d\n", ret);

	//让文件指针回到起始位置
	rewind(pf);
	ch = fgetc(pf);
	printf("%c\n", ch);//

	fclose(pf);
	pf = NULL;
	return 0;
}

 ctrl+F5运行,结果如下所示:

文本文件和二进制文件

 根据数据的组织形式,数据文件被称为文本文件或者二进制文件:数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件;如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是文本文件。

一个数据在内存中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节,31 30 30 30 30),而二进制形式输出,则在磁盘上只占4个字节(10 27 00 00)

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
int main()
{
	int a = 10000;
	FILE* pf = fopen("test.data", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//写文件
	fwrite(&a, sizeof(int), 1, pf);
	return 0;
}

 VS2019解决方案-源文件-添加-现有项,打开test.data文件

 右击文件名-打开方式-二进制编辑器-确定

二进制打开文本文件:

 

文本读取结束的判定

feof 

feof用于判断当文件读取结束的时候,是读取失败结束,还是遇到文件尾结束。 

int feof ( FILE * stream );//返回非0则遇到文件尾结束,返回0则读取失败结束

所以使用feof时,文件一定读取结束了,因此不能用feof的返回值判断文件是否结束。 

新建test1.data文本,向test.data里面存放的内容:

 将test.data中的内容写入到test1.data中:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>

int main()
{
	FILE* pfread = fopen("test.data", "r");
	if (pfread == NULL)
	{
		return 1;
	}
	FILE* pfwrite = fopen("test1.data", "w");
	if (pfread == NULL)
	{
		fclose(pfread);
		pfread = NULL;
		return 1;
	}

	//文件打开成功
	//读写文件
	int ch = 0;
	while ((ch = fgetc(pfread)) != EOF)
	{
		//写文件
		fputc(ch, pfwrite);
	}

	if (feof(pfread))
	{
		printf("遇到文件结束标志,文件正常结束\n");
	}
	else if(ferror(pfread))
	{
		printf("文件读取失败结束\n");
	}

	//关闭文件
	fclose(pfread);
	pfread = NULL;
	fclose(pfwrite);
	pfwrite = NULL;

	return 0;
}

 ctrl+F5运行,结果如下:

且test.data的内容已经全部拷贝到test1.data中:

由此可以看出 ,feof返回非0,且遇到文件尾结束的。

 

文本文件读取结束和二进制文件读取结束的判断不同:

(1)文本文件读取是否结束,判断返回值是否为EOF(fgetc),或NULL(fgets)。上面代码就是判断文本文件读取是否结束。

        fgetc函数在读取结束的时候,会返回EOF;正常读取的时候,返回的是读取到的字符的ASCII码值。

        fgets函数在读取结束的时候,会返回NULL,正常读取的时候,返回存放字符串的空间起始地址。

(2)二进制文件的读取结束判断,判断返回值是否小于实际要读的个数

        fread函数在读取的时候,返回的是实际读取到的完整元素的个数。如果发现读取到的完整元素的个数小于指定的元素个数,这就是最后一次读取了。

文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理数据文件,缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
下面代码可以证明缓冲区的存在:
#define  _CRT_SECURE_NO_WARNINGS  1
#include<stdio.h>
#include<windows.h>
int main()
{
	FILE* pf = fopen("test.data", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区

	printf("睡眠10秒-已经写数据了,打开test.data文件,发现文件没有内容\n");
	Sleep(10000);

	printf("刷新缓冲区\n");
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)

	printf("再睡眠10秒-此时,再次打开test.data文件,文件有内容了\n");
	Sleep(10000);

	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;

	return 0;
}

ctrl+F5运行,打开test.data,文件为空

 

 过10秒后,打开test.data,发现内容已经写入进去了,是从缓冲区写到文件的。

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值