C 语言中的文件

有时候我们需要使用 C 语言读取外部文件中的数据,或者是将计算处理后的结果写入到外部文件中,这时候就需要对文件的操作了。

文件流

C 语言在处理文件的时候是将文件看成了字符的序列,也可以说成是字符流或者是文件流。也就是说不同的文件可能只是文件内容的组织格式不同,但是交给计算机处理的时候都是大致相似的。

文件类型

不同的文件有不同的类型,详细的可以看这篇文章

乱码问题

先看下边的程序:

#include <stdio.h>

int main()
{

    int a = 123456;

    FILE *fp = fopen("ASCII.txt","w");
    fprintf(fp,"%d",a);
    fclose(fp);

    FILE *fd = fopen("BIN.txt","w");
    fwrite(&a,2,1,fd);
    fclose(fd);

    return 0;

}

在 BIN.txt 文件中会出现不能理解的东西,这就是通常所说的乱码。

对于某个文件来说,程序或者进程首先读取文件存储位置上的二进制比特流,然后按照所选择的解码方式对流进行解读,然后将解释结果按照我们选择的方式呈现出来,比如文本编辑器工具。

通常情况下,ASCII 码一般会使用与之对应的解码方式,然后程序会 8 位 8 位地读取流从而进行显示。而二进制数据在计算机内部不是以 ASCII 码形式保存的,再用 ASCII 码的解码方式打开就会出现乱码。

文件缓冲

通常我们使用 printf 函数进行输出时,会习惯性的在后方加上 “\n”,除了使光标换行,还有一个比较重要的作用就是刷新缓冲区,如果在连续的 printf 语句后方都没有 “\n”,可能间隔一段时间才会输出,这也是因为缓冲区的存在,也就是说:

  • 缓冲区能够整合文件和内存的速度
  • 缓冲区有一定的空间大小
  • 刷新缓冲区能够输出当前缓冲区的内容

那么为什么要有缓冲区:

  • 内存的读取速度和文件的读取速度有很大的差别
  • 文件读写需要用到类如 open、read、write 等系统底层函数,而用户进程每次调用系统函数都要从用户态切换到内核态,执行完成后再返回用户态,这种切换是有成本的

文件的打开和关闭

FILE

FILE 是定义的结构体类型,能够记录缓冲区和文件读写状态,也就是说,对文件的操作可以认为是通过 FILE 完成的。

FILE 大致是这么个东西:

struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  _IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
# else
  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;
# endif
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

fopen/fclose

fopen

语法

#include <stdio.h>
FILE *fopen( const char *fname, const char *mode );

描述

  • fopen() 函数打开由 fname(文件名) 指定的文件,并返回一个关联该文件的流
  • 如果发生错误 fopen() 返回 NULL
  • mode(方式) 是用于决定文件的用途(例如 用于输入,输出,等等),mode 都包含:
"r"打开一个用于读取的文本文件
"w"创建一个用于写入的文本文件
"a"附加到一个文本文件
"rb"打开一个用于读取的二进制文件
"wb"创建一个用于写入的二进制文件
"ab"附加到一个二进制文件
"r+"打开一个用于读/写的文本文件
"w+"创建一个用于读/写的文本文件
"a+"打开一个用于读/写的文本文件
"rb+"打开一个用于读/写的二进制文件
"wb+"创建一个用于读/写的二进制文件
"ab+"打开一个用于读/写的二进制文件

上面的 mode 在某些场景下会有不同的处理方式:

mode作用当文件不存在时当文件存在时文件写入文件读取
r读取error打开文件noyes
w写入建立新文件覆盖原有文件yesno
a追加建立新文件在原有文件后追加yesno
r+读取/写入error打开文件yesno
w+写入/读取建立新文件覆盖原有文件yesno
a+读取/追加建立新文件在原有文件后追加yesno

但是,unix/linux 不区分文本和二进制文件。

fclose

语法

#include <stdio.h>
int fclose( FILE *stream );

描述

  • 函数fclose()关闭给出的文件流, 释放已关联到流的所有缓冲区
  • fclose() 执行成功时返回 0,否则返回 EOF

其它文件操作函数

clearerr

语法

#include <stdio.h>
void clearerr( FILE *stream );

描述

  • clearerr 函数重置错误标记和给出的流的 EOF 指针
  • 当发生错误时,你可以使用 perror() 判断实际上发生了何种错误

feof

语法

#include <stdio.h>
int feof( FILE *stream );

描述

  • 函数 feof() 在到达给出的文件流的文件尾时返回一个非零值

ferror

语法

#include <stdio.h>
int ferror( FILE *stream );

描述

  • ferror() 函数检查 stream(流) 中的错误, 如果没发生错误返回 0,否则返回非零
  • 如果发生错误, 使用 perror() 检测发生什么错误

fflush

语法

#include <stdio.h>
int fflush( FILE *stream );

描述

  • 如果给出的文件流是一个输出流,那么 fflush() 把输出到缓冲区的内容写入文件
  • 如果给出的文件流是输入类型的,那么 fflush() 会清除输入缓冲区
  • fflush() 在调试时很实用,特别是对于在程序中输出到屏幕前发生错误片段时,直接调用 fflush(STDOUT) 输出可以保证你的调试输出可以在正确的时间输出

fgetc

语法

#include <stdio.h>
int fgetc( FILE *stream );

描述

  • fgetc() 函数返回来自 stream(流) 中的下一个字符
  • 如果到达文件尾或者发生错误时返回 EOF

fgetpos

语法

#include <stdio.h>
int fgetpos( FILE *stream, fpos_t *position );

描述

  • fgetpos() 函数保存给出的文件流(stream)的位置指针到给出的位置变量(position)中
  • position变量是 fpos_t 类型的(它在stdio.h中定义)并且是可以控制在 FILE 中每个可能的位置对象
  • fgetpos() 执行成功时返回 0,失败时返回一个非零值

fgets

语法

#include <stdio.h>
char *fgets( char *str, int num, FILE *stream );

描述

  • 函数 fgets() 从给出的文件流中读取 [num - 1] 个字符并且把它们转储到 str(字符串) 中
  • fgets() 在到达行末时停止,在这种情况下,str(字符串) 将会被一个新行符结束
  • 如果 fgets() 达到 [num - 1] 个字符或者遇到 EOF, str(字符串) 将会以 null 结束
  • fgets() 成功时返回str(字符串),失败时返回 NULL.

fprintf

语法

#include <stdio.h>
int fprintf( FILE *stream, const char *format, ... );

描述

  • fprintf() 函数根据指定的 format(格式)(格式) 发送信息(参数)到由 stream(流) 指定的文件
  •  fprintf() 只能和 printf() 一样工作
  • fprintf() 的返回值是输出的字符数,发生错误时返回一个负值

fputc

语法

#include <stdio.h>
int fputc( int ch, FILE *stream );

描述

  • 函数 fputc() 把给出的字符ch写到给出的输出流

  • 返回值是字符,发生错误时返回值是 EOF

fputs

语法

#include <stdio.h>
int fputs( const char *str, FILE *stream );

描述

  • fputs() 函数把str(字符串)指向的字符写到给出的输出流

  • 成功时返回非负值, 失败时返回 EOF

fread

语法

#include <stdio.h>
int fread( void *buffer, size_t size, size_t num, FILE *stream );

描述

  • 函数 fread() 读取 [num] 个对象(每个对象大小为size(大小)指定的字节数),并把它们替换到由buffer(缓冲区)指定的数组

  • 数据来自给出的输入流

  • 函数的返回值是读取的内容数量

  • 使用 feof() 或 ferror() 判断到底发生哪个错误

freopen

语法

#include <stdio.h>
FILE *freopen( const char *fname, const char *mode, FILE *stream );

描述

  • freopen() 函数常用于再分配一个已存在的流给一个不同的文件和方式(mode)

  • 在调用本函数后,给出的文件流将会用mode(方式)指定的访问模式引用fname(文件名)

  • freopen() 的返回值是新的文件流,发生错误时返回NULL.

fscanf

语法

#include <stdio.h>
int fscanf( FILE *stream, const char *format, ... );

描述

  • 函数 fscanf() 以 scanf() 的执行方式从给出的文件流中读取数据

  • fscanf() 的返回值是事实上已赋值的变量的数,如果未进行任何分配时返回 EOF

fseek

语法

#include <stdio.h>
int fseek( FILE *stream, long offset, int origin );

描述

  • 函数 fseek() 为给出的流设置位置数据

  • origin 的值应该是下列值其中之一(在 stdio.h 中定义):

名称说明
SEEK_SET从文件的开始处开始搜索
SEEK_CUR从当前位置开始搜索
SEEK_END从文件的结束处开始搜索
  • fseek() 成功时返回 0,失败时返回非零
  • 可以使用 fseek() 移动超过一个文件,但是不能在开始处之前
  • 使用 fseek() 清除关联到流的 EOF 标记.

fsetpos

语法

#include <stdio.h>
int fsetpos( FILE *stream, const fpos_t *position );

描述

  • fsetpos() 函数把给出的流的位置指针移到由position对象指定的位置

  • fpos_t 是在 stdio.h 中定义的

  • fsetpos() 执行成功返回 0,失败时返回非零.

ftell

语法

#include <stdio.h>
long ftell( FILE *stream );

描述

  • ftell() 函数返回stream(流)当前的文件位置,如果发生错误返回 -1

fwrite

语法

#include <stdio.h>
int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

描述

  • fwrite() 函数从数组buffer(缓冲区)中, 写count个大小为size(大小)的对象到stream(流)指定的流

  • 返回值是已写的对象的数量

remove

语法

#include <stdio.h>
int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

描述

  • remove() 函数删除由fname(文件名)指定的文件

  • remove() 成功时返回 0,如果发生错误返回非零.

rename

语法

#include <stdio.h>
int rename( const char *oldfname, const char *newfname );

描述

  • 函数 rename() 更改文件oldfname的名称为newfname

  • rename() 成功时返回 0,错误时返回非零

rewind

语法

#include <stdio.h>
void rewind( FILE *stream );

描述

  • 函数 rewind() 把文件指针移到由stream(流)指定的开始处,同时清除和流相关的错误和 EOF 标记

setbuf

语法

#include <stdio.h>
void setbuf( FILE *stream, char *buffer );

描述

  • setbuf() 函数设置stream(流)使用buffer(缓冲区),如果buffer(缓冲区)是null,关闭缓冲

  • 如果使用非标准缓冲尺寸,它应该由BUFSIZ字符决定长度.

setvbuf

语法

#include <stdio.h>
int setvbuf( FILE *stream, char *buffer, int mode, size_t size );

描述

  • 函数 setvbuf() 设置用于stream(流)的缓冲区到buffer(缓冲区),其大小为size(大小)

  • mode(方式)可以是:

_IOFBF完全缓冲
_IOLBF线缓冲
_IONBF无缓存

ungetc

语法

#include <stdio.h>
int ungetc( int ch, FILE *stream );

描述

  • 函数 ungetc() 把字符ch放回到stream(流)中

注意事项

先看一个程序:

#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt","w+");
    char buf[26];
    char ch;

    if (NULL == fp)
    {
	printf("FILE OPEN ERROR!");
	return -1;    
    }

    for (int i = 0;i < 26;i++)
    {
	buf[i] = (char)fputc((char)(97+i),fp);
	printf("buf[%d] = %c\n",i,buf[i]);
    }
    
    rewind(fp);

    while((ch = fgetc(fp)) != EOF)
	printf("fgetc(fp) = %c\n",ch);

    fclose(fp);

    return 0;
}

结果为:

buf[0] = a
buf[1] = b
buf[2] = c
buf[3] = d
buf[4] = e
buf[5] = f
buf[6] = g
buf[7] = h
buf[8] = i
buf[9] = j
buf[10] = k
buf[11] = l
buf[12] = m
buf[13] = n
buf[14] = o
buf[15] = p
buf[16] = q
buf[17] = r
buf[18] = s
buf[19] = t
buf[20] = u
buf[21] = v
buf[22] = w
buf[23] = x
buf[24] = y
buf[25] = z
fgetc(fp) = a
fgetc(fp) = b
fgetc(fp) = c
fgetc(fp) = d
fgetc(fp) = e
fgetc(fp) = f
fgetc(fp) = g
fgetc(fp) = h
fgetc(fp) = i
fgetc(fp) = j
fgetc(fp) = k
fgetc(fp) = l
fgetc(fp) = m
fgetc(fp) = n
fgetc(fp) = o
fgetc(fp) = p
fgetc(fp) = q
fgetc(fp) = r
fgetc(fp) = s
fgetc(fp) = t
fgetc(fp) = u
fgetc(fp) = v
fgetc(fp) = w
fgetc(fp) = x
fgetc(fp) = y
fgetc(fp) = z

由上边的结果可以知道:

  • 和利用 malloc 进行内存分配所要进行的步骤一样,利用 FILE 进行的文件操作也要经过打开、判空、读写、关闭等步骤
  • 要根据所要进行的操作,选择合适的 mode 打开文件
  • 要利用好各个函数的返回值
  • 未关闭文件前对文件进行读取和写入操作时,要注意当前流的位置
  • 利用 fgetc 可以判断是否到文件尾,一般都会加在读取文件之前,作为结束条件
  • 可以利用 fgetc,gputc 函数进行单个字符的读取和写入
  • 同时可以利用 fputs,fgets 函数进行整行文本的读取和写入
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt","w+");
    char ch[10];

    if (NULL == fp)
    {
	printf("FILE OPEN ERROR!");
	return -1;    
    }

    for (int i = 0;i < 26;i++)
	fputs("abc\n",fp);
    
    rewind(fp);

    while(fgets(ch,10,fp) != NULL)
	printf("fgets(fp) = %s",ch);

    fclose(fp);

    return 0;
}

 只是要注意是 char ch[10],而不是 char *ch,如果定义指针的话,由于没有初始化,对应写入的地址是不确定的。

 

  • 也可以利用 fwrite,fread 函数进行整块文本的读取和写入(二进制操作)
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *fp = fopen("test.txt","w+");
    char ch[10];

    if (NULL == fp)
    {
	printf("FILE OPEN ERROR!");
	return -1;    
    }

    char *p = "abcdefg \n hijklmn \0 opqrst \t uvwxyz \\";
    
    fwrite(p,1,strlen(p),fp);

    rewind(fp);

    while(fgets(ch,10,fp) != NULL)
	printf("%s",ch);

    putchar(10);
    putchar(10);
    
    rewind(fp);

    fwrite(p,1,100,fp); 

    rewind(fp);

    while(fgets(ch,10,fp) != NULL)
	printf("%s",ch);

    putchar(10);

    fclose(fp);

    return 0;
}

结果为:

abcdefg 
 hijklmn 

abcdefg 
 hijklmn 	 uvwxyz \;0�����������
                                        

也就是说,使用 fread,fwrite 时,是按照块读取的,而并不关心块里的内容。并且 fread 的返回值有时候也会不是我们想象的那样:

#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt","r+");
    char ch[100];
    int n;

    if (NULL == fp)
    {
	printf("FILE OPEN ERROR!");
	return -1;    
    }

    n = fread(ch,1,100,fp);
    printf("n = %d\n",n);

    rewind(fp);

    n = fread(ch,100,1,fp);
    printf("n = %d\n",n);

    putchar(10);

    rewind(fp);

    n = 1;

    while(n != 0)
    {
        n = fread(ch,4,1,fp);
        printf("n = %d\n",n);
    }

    putchar(10);

    rewind(fp);

    n = 1;

    while(n != 0)
    {
        n = fread(ch,1,4,fp);
        printf("n = %d\n",n);
    }
    
    fclose(fp);

    return 0;
}

test.txt 中的内容为:

aaaaaaaaaa
bbbbbbbbbb
cccccccccc
dddddddddd
eeeeeeeeee

结果为:

n = 55
n = 0

n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 1
n = 0

n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 4
n = 3
n = 0

从上面的结果可以看出:

  • fread 的返回值是读取的内容数量,而该数组最大不会超过字节块数的值
  • 根据字节数和字节块数设置的不同,fread 的执行次数可能会不同
  • 一般情况下,设置字节数为 1,能够读取实际的文件大小
  • 字节数设置的过大,如果读不满该字节数返回为 0
  • 既然 fread,fwrite 是针对块进行读写的,那么就可以对文件进行一些特殊操作,即加密等
  • 之前提到过的 feof 函数也要慎重使用

feof 函数是读标志位 EOF 来判断文件是否结束的。换句话说就是该函数在读到文件尾部的时候再读一次,标志位才会置位,函数才会给出文件结束的返回值。

#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt","w+");
    char buf[10];
    char ch;

    if (NULL == fp)
    {
	printf("FILE OPEN ERROR!");
	return -1;    
    }

    for (int i = 0;i < 10;i++)
    {
	buf[i] = (char)fputc((char)(97+i),fp);
	printf("buf[%d] = %c\n",i,buf[i]);
    }
    
    printf("***********************\n");
    
    rewind(fp);

    while((ch = fgetc(fp)) != EOF)
	printf("fgetc(fp) = %c\n",ch);

    printf("***********************\n");
    
    rewind(fp);
    
    while(!feof(fp))
	printf("fgetc(fp) = %c\n",fgetc(fp));

    printf("***********************\n");
    
    rewind(fp);

    while((ch = fgetc(fp)) && !feof(fp))
	printf("fgetc(fp) = %c\n",ch);

    printf("***********************\n");
    
    fclose(fp);

    return 0;
}

从上边的结果可以看出:

  • 中间一种输出方式是有问题的,多输出了一次
  • 第一,第三中输出方式是正确的,但建议采用第一种方式,简单
  • feof 函数的返回值是个 int

再看看如果是整行读取的结果:

#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt","r+");
    char buf[10];

    if (NULL == fp)
    {
	printf("FILE OPEN ERROR!");
	return -1;    
    }

    while(fgets(buf,10,fp))
	printf("fgets(fp) = %s\n",buf);

    printf("***********************\n");

    rewind(fp);

    while(!feof(fp))
	printf("fgets(fp) = %s\n",fgets(buf,10,fp));

    printf("***********************\n");

    rewind(fp);

    while(fgets(buf,10,fp) && !feof(fp))
	printf("fgets(fp) = %s\n",buf);

    printf("***********************\n");
    
    fclose(fp);

    return 0;
}

test.txt 中的内容为:

aaaaaaa
bbbbbbb
ccccccc
ddddddd
eeeeeee

结果为:

fgets(fp) = aaaaaaa

fgets(fp) = bbbbbbb

fgets(fp) = ccccccc

fgets(fp) = ddddddd

fgets(fp) = eeeeeee

***********************
fgets(fp) = aaaaaaa

fgets(fp) = bbbbbbb

fgets(fp) = ccccccc

fgets(fp) = ddddddd

fgets(fp) = eeeeeee

fgets(fp) = (null)
***********************
fgets(fp) = aaaaaaa

fgets(fp) = bbbbbbb

fgets(fp) = ccccccc

fgets(fp) = ddddddd

fgets(fp) = eeeeeee

***********************

可以看出,中间一种输出方式是有问题的,多输出了一次。

  • windows 和 linux 的换行也是不同的

先看下边的程序:

//Linux 中,wb,w,rb,r 的任意组合方式运行结果都是一样的
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt","wb");
    char ch = '\n';

    if (NULL == fp)
    {
	printf("FILE OPEN ERROR!");
	return -1;    
    }
    
    fputc('a',fp);
    fputc('b',fp);
    fputc(ch,fp);

    fclose(fp);
    
    fp = fopen("test.txt","rb");
    while((ch = fgetc(fp)) != EOF)
	printf("bit = %x\n",ch);

    fclose(fp);

    return 0;
}

结果为:

bit = 61
bit = 62
bit = a

再看下边的程序:

//Windows 中,wb-rb,wb-r,w-r 的输出结果与 Linux 相同,但是 w-rb 的输出结果就有差别了
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt","w");
    char ch = '\n';

    if (NULL == fp)
    {
    printf("FILE OPEN ERROR!");
    return -1;
    }

    fputc('a',fp);
    fputc('b',fp);
    fputc(ch,fp);

    fclose(fp);

    fp = fopen("test.txt","rb");
    while((ch = fgetc(fp)) != EOF)
    printf("bit = %x\n",ch);

    fclose(fp);

    return 0;
}

结果为:

bit = 61
bit = 62
bit = d
bit = a
  • windows 文本方式写入时,遇到“\n”(0A),自动将其换为“\r\n”(0D0A),然后再写入文件
  • windows 文本方式读取时,遇到“\r\n”(0D0A),自动将其换为“\n”(0A),然后再读入缓存
  • 二进制读写时,不存在转换,直接将缓存区中的数据写入文件
  • 以 wb 方式写文件时,写进文件的"\n",windows 的文本编辑工具不能识别
  • 以 rb 方式读文件时,windows 会将“\n“读成”\r\n“,可能会给程序带来影响
  • 这一点在跨平台开发的时候尤其要注意
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值