我的研究之文件

文件对于C语言编程来说是非常重要的,因为计算机无非就那几样东西 内存 硬盘,文件对应的就是硬盘,硬盘有多重要,文件就有多重要了。这里的文件是指标准I/O和linux下的文件,

这里先说标准I/O,如果不清楚标准C 标准I/O等等的,请先移步我转载的另一篇博客“GUN C 标准C .....的区别与联系”。


ANSI C 和 早期C相比最大的优点之一就是它在规范里所包含的函数库。每个ANSI 编译器必须支持一组规定的函数,并具备规范所要求的接口,而且按照规定的行为工作。这种情况较之早期的C是个巨大的改进。以前,不同的编译器可以通过修改或扩展普通函数库的功能来“改善”他们,但往往限制了可移植性。ANSI编译器并未禁止在他们的函数库的基础上增加其他函数。但是,标准函数必须根据标准所定义的方式执行。如果你关心可移植性,只要避免使用任何非标准函数就可以了。

为了后面更好的研究,我们先学习两个非常有用的函数,他们用于报告错误以及对错误作出反应。


错误报告:perror函数。

                ANSI C 函数库的许多函数调用操作系统来完成某些任务,I/O函数尤其如此。调用失败后,操作系统的提示无法传给用户。这样多不方便,败都不知道败在哪里了,太憋屈了。所以,标准库函数在一个外部整型变量errno(在errno.h中定义)中保存错误代码后将这个信息传递给用户程序,给出错误信息提示。

perror 函数定义于stdio.h中,所以使用时记得#include<stdio.h>,原型如下:

void perror( char  const  *message);  如果message 不是NULL并且指向一个非空的字符串,perror函数就打印出这个字符串,后面跟一个分号和空格,然后打印出一条用于解释errno当前错误代码的信息。举个例子更直观些:

#include<stdio.h>
int main( void )
{
FILE *fp;
fp= fopen ( "~/noexitfile" , "r+" );
if (NULL==fp)
{
perror ( "~/noexitfile" );
}
return0;
}
执行后提示的错误为:~/noexitfile: No such file or directory           所以说上面的一段话说白了就是,前半段输出什么自己决定,冒号是系统帮忙的,后半端是系统决定的。


注:perror函数以一种简单  统一的方式报告错误,所以它的最大优点就是简单易用。良好的编程实践要求任何可能产生错误的操作都应该在执行之后进行检查,确定它是否成功执行。即使是那些十拿九稳不会失败的操作也应该进行检查,因为他们迟早可能失败。这种检查需要稍许额外的工作,但与你可能付出的大量调试时间相比,它们还是非常值得的。


注意:只有当一个库函数失败时,errno才会被设置,但函数成功运行时,errno的值不会被修改。这意味着:1.我们不能通过测试errno的值来判断是否有错误发生,反之,只有被调用的函数提示有错误发生时检查errno的值才有意义。2.如果有两个及以上库函数失败时,errno的值只会记录上一个函数(即最后一个报错的函数)的错误码,以前报错的错误码就会被覆盖,所以,检错要及时。


另一个有用的函数是exit,它用于终止一个程序的执行,原型定义于stdlib.h,如下:

void exit( int status );

功 能: 关闭所有文件,终止正在执行的进程。

exit(1)表示异常退出.这个1是返回给操作系统的。
exit(x)(x不为0)都表示异常退出
exit(0)表示正常退出
例如:

int  main()

{

     int status;
     printf("Entererrno:\n");                                                 
     status = getchar();
     exit(status-'0');
     return 0;
}

status 参数返回给操作系统,用于提示程序是否正常完成。这个值和mian函数返回的整型状态值相同。预定义符号EXIT_SUCCESS 和 EXIT_FAILURE 分别提示程序的终止是成功还是失败。虽然程序也可以使用其他的值,但他们的具体含义将取决于编译器。


当程序发现错误情况使它无法继续执行下去时,这个函数尤其有用。你经常会在调用perrno之后再调用exit终止程序。尽管终止程序并非处理所有错误的正确方法,但和一个注定失败的程序继续执行下去后再失败相比,这种做法更好一些。


注意:这个函数没有返回值。当exit函数结束时,程序已经消失,所以它无处可返。


好了,现在开始研究咱们的文件I/O。

首先要说明的是头文件stdio.h包含了与ANSI函数库的I/O部分有关的声明。尽管不包含这个头文件也可以使用某些I/O函数,但绝大多数I/O函数在使用前都需要包含这个头文件。



流:流是对I/O的抽象(特定的I/O细节对我们程序员是隐藏的),这类似与linux中的文件描述符,他们的相同点是均是对I/O的抽象,不同点是流是以物理的形式表现抽象,文件描述符是以数据结构的形式表现抽象,所以流可以选择为缓冲,而文件描述符却是完全不缓冲的。


注意:

1.绝大多数流是完全缓冲的,这意味着“读取”和“写入”实际上是从一块被称为缓冲区的内存区域来回复制数据。从内存中来回复制数据是非常快的。

2.用于输出流的缓冲区只有当它写满时才会被刷新(flush  物理写入)到设备或文件中。一次性把写满的缓冲区写入和逐片把程序产生的输出分别写入相比效率更高。

3.输入缓冲区当它为空时通过从设备或文件读取下一块较大的输入,重新填充缓冲区。


刷新函数:fflush  。  在处理流时,fflush函数较为有用,它迫使一个输出流的缓冲区内的数据进行物理写入,不管它是不是已经写满。原型如下:

int fflush( FILE *stream );

例如,这个函数常常与printf()函数一块使用,调用printf后再调用fflush函数保证调试信息实际打印出来,而不是保存在缓冲区中直到以后才打印。

fflush(stdin)刷新标准输入缓冲区,把输入缓冲区里的东西丢弃[ 非标准]
fflush(stdout)刷新标准输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上
printf(".............");后面加flush(stdout);可提高打印效率


流分为两种类型,文本流和二进制流。文本流的有些特性在不同的系统中可能不同。其中之一就是文本行的最大长度。标准规定至少允许254个字符,另一个是文本行的结束方式。例如,DOS中文本文件约定以一个回车符和换行符结尾。但是,UNIX系统只使用一个换行符结尾。对于不同系统中的这种不同,由库函数来负责进行翻译。例如在DOS中,输出时,文本中的换行符被写成一对回车/换行符,在输入时,文本中的回车符被丢弃。这种不必考虑文本的外部形式而操纵文本的能力简化了可移植程序的创建。

二进制流中的字节将完全根据程序编写他们的形式写入到文件或设备中,而且完全根据他们从文件或设备读取的形式读入到程序中,他们未作任何改变。这种类型的流适用于非文本数据。


文件;

stdio.h所包含的声明之一就是FILE结构。请不要把它和存储于磁盘上的数据文件相混淆。FILE是一个数据结构,用于访问一个流。如果你同时激活了几个流,每个流都有一个相应的FILE与它关联。为了在流上执行一些操作,调用一些合适的函数时需要向它们传递一个与这个流关联的FILE参数。


注:1.一个程序同时最多能够打开多少个文件呢?它和编译器有关,但可以保证你能够同时打开至少FOPEN_MAX个文件。这个常量包括了三个标准流,它的值至少是8.

       2.常量FILENAME_MAX是一个整型值,用于提示一个字符数组应该多大以便容纳编译器所支持的最长合法文件名。如果对文件名的长度没有一个实际的限制,那这个常量的值就是文件名的推荐最大长度。

       3.EOF 对于文件来说也是一个非常重要的常量,它的详解请移步我的一篇博客。


使用文件I/O的一般流程为:

1.为文件声明一个指针变量,其类型为 FILE *,这个指针指向这个FILE结构,当文件处于活动状态时由流使用。

2.调用fopen函数打开流(要指定打开方式),如果fopen和操作系统验证文件或设备确实存在(在有些操作系统中,还验证是否允许执行你所指定的访问方式)的话,就初始化FILE结构。

3.根据需要对文件进行操作,读取或写入。

4.调用fclose函数关闭流。关闭一个流可以防止与它相关连的文件被再次访问,保证任何存储于缓冲区的数据被正确的写入到文件中,并且释放FILE结构使它可以用于另外的文件。


标准流的I/O更为简单,因为他们并不需要打开和关闭。(???????)


I/O函数以三种基本的形式处理数据:单个字符   文本行  二进制数据。

对于每种形式,都有一组特定的函数对他们进行处理(俗称函数家族)。一组函数中的每个都执行相同的基本任务,只是方式稍有不同,他们的区别在于获得输入的来源和输出写入的地方不同。这些变种用于执行下面的任务:

1.只用于stdin或stdout。

2.随作为参数的流使用。  需要一个流参数的函数将接受stdin或stdout作为它的参数。

3.使用内存中的字符串而不是流。


执行字符  文本  二进制的I/O函数家族:


数据类型           输入                    输出                             描述

字符              getchar家族        putchar家族                   读取(写入)单个字符

         

文本行          gets家族             puts家族                        未格式化的输入(输出)

                    scanf家族           printf家族                        格式化的输入(输出)


二进制数据   fread家族            fwrite家族                        读取(写入)二进制数据


每个家族的函数:


家族名                  目的                       可用于所有流                  只用于stdin和stdout                 内存中的字符串

getchar              字符输入                  fgetc  getc                                getchar                  对指针使用下标引用或间接访问操作从内存获得一个字符(或向内存写入一个字符)


putchar              字符输出                  fputc  putc                                 putchar                对指针使用下标引用或间接访问操作从内存获得一个字符(或向内存写入一个字符)


gets                   文本行输入                   fgets                                    gets                     使用strcpy函数从内存读取文本行(或向内存写入文本行)


puts                  文本行输出                   fputs                                      puts                    使用strcpy函数从内存读取文本行(或向内存写入文本行)


scanf               格式化输入                  fscanf                                      scanf                    sscanf


printf                 格式化输出                 fprintf                                      printf                      sprintf


打开流:

fopen函数用于打开一个特定的文件,并把一个流和这个文件相关连。原型如下:

           FILE  *fopen( char const *name,char const *mode);

name :要打开的文件或设备的名字。

注:fopen把文件名作为一个字符串而不是为路径名 驱动器字母 文件名等各准备一个参数,是因为创建文件名的规则在不同的系统中可能各不相同(?不太理解)。

FILE *变量的名字是程序用来保存fopen的返回值,他并不影响那个文件被打开。

mode 以r w 或a (文本),rb  wb 或 ab(二进制)开头,分别表示打开流用于读取  写入或者添加。

注意:如果一个文件打开是用于读取的,那么它必须是原先已经存在的。

          如果一个文件打开是用于写入的,如果他原先已经存在,那么他原来的内容就会被删除,如果不存在就会创建一个新文件。

         如果一个文件打开是用于添加的,如果他原先已经存在,那么他原来的内容并不会被删除,会在原来的内容后面继续写,如果不存在就会创建一个新文件。

          无论那一种情况,写入的时候只能从文件的尾部写入。

          mode中的“a+”表示该文件打开用于更新,并且流既允许读也允许写。


如果fopen函数执行成功,他返回一个指向FILE结构的指针,该结构代表这个新创建的流。如果函数执行失败,他就返回一个NULL指针,errno会提示问题的性质。

注意:你应该始终检查fopen函数的返回值!如果程序不检查错误,这个NULL指针就会传给后续的I/O函数,他们将对这个指针执行间接访问,并将失败。


这里谈到fopen函数就不得不谈freopen函数了,freopen(包含在stdio.h头文件中)是用于重定向输入输出流的函数。该函数可以在不改变代码原貌的情况下改变输入输出环境,但使用时应当保证流是可靠的。原型如下:

FILE *freopen(const char *filename, const char *mode, FILE *stream);

最后一个参数(stream)就是要打开并重定向到filename(即将打开的流)的流,它可能是一个先前从fopen函数返回的流,也可能是标准流stdin stdout或stderr。这个函数会首先试图关闭这个流,然后用指定的文件和模式重新打开一个流,如果打开失败,函数就返回一个NULL值,如果打开成功,函数就返回它的第3个参数值。

举个例子:

int main()
{

    FILE *stream;

    if((stream = freopen("./text.txt","w",stdout)) == NULL){
     
        perror("./text.txt:");
        exit( EXIT_FAILURE );
    
    }   

    printf("this is stdout output\n");

    return 0;

}

这个例子将标准输出重定向到文件text.txt的流中了,所以字符串“this is stdout output”会被打印到文件text.txt中。


关闭流


函数fclose用来关闭流,原型如下:


int fclose( FILE *f );


注:对于输出流,fclose函数在文件关闭之前刷新缓冲区。如果它执行成功,fclose返回零值,否则返回EOF。


注意:程序也应该对fclose函数的返回值进行检查,因为打开的流很可能在fclose之前因为bug而发生修改或者有可能是NULL。


好了,现在该一一介绍这些I/O了,

字符I/O:

getchar家族的函数原型如下:

    int fgetc(FILE *stream)
    int getc(FILE *stream )
    int getchar( void )

每个函数从流中读取下一个字符,并把它作为函数返回值返回。如果流中不存在更多的字符,函数就返回常量值EOF。

注意:1.getchar始终从标准输入读取。
      2.这些函数都用于读取字符,但他们都返回int型值而不是char型值。这是考虑到EOF,因为如果返回值是char型,那么在256个字符中必须有一个被指定用于表示EOF,如果这个字符出现在文件内部,那么这个字符以后的内容将不会被读取。让函数返回一个int型值就能解决这个问题,EOF被定义为整型,它的值在任何可能出现的字符范围之外。

putchar家族:

int fputc( int character,FILE *stream );
int putc( int character,FILE *stream  );
int putchar( int character );

注意:
     1.第一个参数是要被打印的字符,在打印之前,函数会将这个整型参数裁剪为一个无符号字符型值,所以  putchar('abc'); 只打印一个字符(至于是那一个,不同的编译器可能不同)。
     2.如果由于任何原因(如写入到一个已被关闭的流)导致函数失败,他们就返回EOF。
    3.putchar始终输出到标准输出。
    4.fgetc和fputc都是真正的函数,但getc putc getchar和putchar都是通过#define指令定义的宏。

getchar 由 实现:#define getchar() getc(stdin)。getchar有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的 字符被存放在键盘 缓冲区中. 直到用户按回车为止(回车 字符也放在缓冲区中).当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符.getchar函数的返回值是用户输入的字符的ASCII码,如出错返回-1,且将用户输入的字符 回显屏幕.如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键.
getch与getchar基本功能相同,差别是getch直接从键盘获取键值,不等待用户按回车,只要用户按一个键,getch就立刻返回,getch返回值是用户输入的ASCⅡ码,出错返回-1.输入的字符不会 回显屏幕上.getch函数常用于 程序调试中,在调试时,在关键位置显示有关的结果以待查看,然后用getch函数暂停程序运行,当按 任意键后程序继续运行.


还有一个比较有意思的函数就是:撤销字符I/O函数:ungetc 详情如下:


如果你读到一个你不想要的字符,但又不能丢掉,那该怎么办呢?这时ungetc就派上用场了,它的原型如下:


int ungetc(int character, FILE *stream);


ungetc把一个字符退回到流中,这样他可以在以后被重新读入。

注意:1.每个流都允许至少一个字符被退回。如果一个流允许退回多个字符,那么这些字符再次被读取的顺序就以退回时的反序进行。
2.把字符退回到流中和写入到流中并不相同,与一个流相关联的外部存储并不受ungetc的影响。
3.退回字符和流的当前位置有关,所以如果用fseek  fsetpos  rewind  函数改变了流的位置,所有退回的字符都将被丢弃。

举个例子好理解一些:

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

#define NDEBUG
#include <assert.h>

#include <ctype.h>

#define MAX_LINE_LENGTH         10

int
read_int()
{
        int value;
        int ch;
        value = 0;

        while(( ch = getchar() ) != EOF && isdigit( ch ) ){
                value *= 10;
                value += ch - '0';
            
        }   
            
        ungetc( ch, stdin );
        return value;
}

int main()                                                                          
{
        char buffer[MAX_LINE_LENGTH];        
        int value = read_int();
        printf("you input numbers is:%d\n",value);

        if(fgets( buffer, MAX_LINE_LENGTH, stdin) != NULL )
                printf("you last input is:%s",buffer);  

        return 0;
}

输入569d 回车后自己看看什么结果吧?


未格式化的行I/O  终于到了行I/O了,那会非常有意思

行I/O可以用两种方式执行:未格式化的或格式化的。这两种形式都用来操纵字符串。区别在于未格式化的简单读取和写入字符串,而格式化的则执行数字和其他变量的内部和外部表示形式之间的转换。这里我们先讨论未格式化的。


gets和puts函数家族是用于操作字符串而不是单个字符。这些特征使他们在那些处理一行行文本输入的程序中非常有用。这些函数的原型如下:

char *fgets( char *buffer,int buffer_size,FILE *stream );

char *gets( char *buffer );


int fputs( char const *buffer, FILE *stream );

int puts( char const *buffer );


fgets从指定的stream读取字符并把他们复制到buffer中,当他读取一个换行符并存储到缓冲区之后就不再读取。如果缓冲区中的字符数达到buffer_size-1个时他也停止读取。在这种情况下并不会出现数据丢失,因为下一次调用fgets将从流的下一个字符开始读取。在任何一种情况下,一个NUL字节将被添加到缓冲区所存储数据的末尾,使他成为一个字符串。
如果在任何字符读取前就达到了文件尾,缓冲区就未进行修改,fgets函数返回一个NULL指针,否则,fgets函数返回他的第一个参数(指向缓冲区的指针),这个返回值通常只用于检查是否达到了文件尾。

传递给fputs的缓冲区必须包含一个字符串,他的字符被写入到流中,这个字符串预期以NUL字节结尾,所以这个函数没有一个缓冲区长度参数。这个字符串是逐字写入的:如果他不包含一个换行符就不会写入换行符,如果他包含了好几个换行符,所有的换行符都会被写入。所以,当fgets每次都读取一整行时 ,fputs却可以一次写入一行的一部分,也可以一次写入一整行,甚至可以一次写入好几行。如果写入时出现了错误,fputs返回常量值EOF,否则他将返回一个非负值。

注意:1.缓冲区长度的正确值通常是根据需要执行的处理过程的本质而做出的折衷,但是即使溢出他的缓冲区,fgets也绝不引起错误。(个人理解:也就是说fgets是安全的)

2.fgets无法把字符串读入到一个长度小于两个字符的缓冲区,因为其中一个字符需要为NUL字节保留。

gets和puts函数几乎和fgets与fputs相同,之所以存在他们是为了允许向后兼容。他们之间的一个主要的功能性区别在于当gets读取一行输入时,他并不在缓冲区中存储结尾的换行符。当puts写入一个字符串时,他在字符串写入之后向输出再添加一个换行符。

注意:gets与fgets的另一个区别是,gets没有缓存区长度参数,因此gets无法判断缓存区的长度。这样的话如果一个长行读到一个短缓冲区中,多出来的字符将被写入到缓冲区后面的内存位置,这将破坏一个或多个不相关变量的值。这个事实导致gets函数只适用于玩具程序,因为唯一防止缓冲区溢出的方法就是声明一个巨大的缓冲区,但不管他有多大,下一个输入行仍有可能比缓冲区更大,尤其是当标准输入重定向到一个文件时。这一点一定要注意。
另外,gets只用于标准输入。


有了这个函数家族后就非常好玩了,你可以将一个文件的内容拷贝到另一个文件,当你实现这些功能的时候真的感觉非常兴奋,是原封不动的拷过去哦!例子程序如下:

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

#define NDEBUG
#include <assert.h>

#include <ctype.h>

#define MAX_LINE_LENGTH         10

void
copylines( FILE *input,FILE *output )
{
        char buffer[MAX_LINE_LENGTH];

        while(( fgets( buffer,MAX_LINE_LENGTH,input ) != NULL ))
                fputs( buffer,output );
            
}

int main()                                                                          
{
        FILE *input,*output;
        if((input = fopen("./text1.txt","r")) == NULL ){
            
            perror("open ./text1.txt error:");
            exit(EXIT_FAILURE);
            
        }   
            
        if((output = fopen("./text2.txt","w")) == NULL ){
     
            perror("open ./text2.txt error:");
            exit(EXIT_FAILURE);
     
        }   
     
        copylines(input,output);
        return 0;
}                        

格式化的行I/O
终于来到我们的格式化的行I/O的世界了,这里将比以前更加美妙。其实从某种意义上说这个名字并不准确,因为scanf和printf函数家族并不仅限于单行,他们也可以在行的一部分或多行上执行I/O操作。下面先来看scanf函数家族
scanf函数家族的原型如下:
int fscanf( FILE *stream”,char const *format,… ) ;
int scanf( char const *format,…);
int sscanf( char const *string,char const *format,…);

每个原型中的省略号表示一个可变长度的指针列表,从输入转换而来的值逐个存储到这些指针参数所指向的内存位置。
这些函数都从输入源读取字符并根据format字符串给出的格式代码对他们进行转换。fscanf的输入源就是作为参数给出的流,scanf从标准输入读取,而sscanf则从第一个参数所给出的字符串中读取字符。
当格式化字符串到达末尾或者读取的输入不再匹配格式字符串所指定的类型时,输入就停止。在任何一种情况下,被转换的输入值的数目作为函数的返回值返回。如果在任何输入值被转换之前文件就已达到尾部,函数就返回常量值EOF。

现在知道为什么scanf函数的参数前面为什么要加一个&符号了吧,由于C的传值参数传递机制,把一个内存位置作为参数传递给函数的唯一方法就是传递一个指向该位置的指针。

注意:为了能让这些函数正常运行,指针参数的类型必须是对应格式代码的正确类型。函数无法验证他们的指针参数是否为正确类型,所以函数就假定他们是正确的,于是继续执行并使用他们。如果指针参数的类型不正确,那么结果值就会是垃圾,而临近的变量有可能在处理过程中被改写。

对于scanf函数家族来说最重要的部分就是他的“格式代码”部分,所以下面专门对他的格式代码进行一下研究。
scanf函数家族中的format字符串参数可能包含下列内容:
空白字符:他们与输入中的零个或多个空白字符匹配,在处理过程中将被忽略。
格式代码:他们指定函数如何解释接下来的输入字符。
其他字符:当任何其他字符出现在格式字符串时,下一个输入字符必须与他匹配。如果匹配,该输入字符随后就被丢弃,如果不匹配,函数就不再读取直接返回。
scanf函数家族的格式代码都以一个百分号开头,后面可以是(1).一个可选的星号,(2).一个可选到宽度,(3).一个可选的限定符,(4).格式代码。星号将使转换后的值被丢弃而不是进行存储。这个技巧可以用于跳过不需要的输入字符。宽度用于限制被读取的用于转换的输入字符的个数,如果未给出宽度,函数就连续读入字符直到遇见输入中的下一个空白字符。限定符用于修改有些格式代码的含义,具体如下:
h:(d,i,n):short
(o,u,x):unsigned  short
l:(d,i,n):long
(o,u,x):unsigned  long
(e,f,g):double
L:(e,f,g):long double
注意:1.限定符的目的是为了指定参数的长度,如果有些时候省略限定符会导致变量长度变长或变短 ,那省略了就是个错误。

* 是scanf函数中的一种修饰符           表示输入项输入后不转送给任何向量            
%s     是格式  控制符        %s用在输入函数中表示输入一个字符串  遇到空格回车  结束 
两者一起用              表示将 当前的一个字符串输入后不送给任何变量  执行后光标到了这个字符串后面          也就相当于跳到了下一个字符串前面             一般字符串之间都是用空格  隔离的  所以
是跳到了下一个空白字符处

2.使用适当的限定符使程序更加具有可移植性。
格式代码就是一个单字符,用于指定输入字符如何被解释。具体如下:
c(char *):读取和存储单个字符,前导的空白字符并不跳过。如果给出宽度,就读取和存储这个数目的字符,字符后面不会添加一个NUL字节 ,参数必须指向一个足够大的字符数组。
i,d(int *):一个可选的有符号整数被转换,d把输入解释为十进制数,i根据它的第一个字符决定值的基数(?)。
u,o,x(unsigned *):一个可选的有符号整数被转换,但它按照无符号数存储。u,值被解释为十进制数;o,值被解释为八进制数;x,值被解释为十六进制数。x与X同义。
e,f,g(float*):期待一个浮点值,他的形式必须像一个浮点型字面值常量,但小数点并非必须。E和G分别和e和g同义
s(char*):读取字符串,参数必须指向一个足够大的字符数组。当发现空白时输入就停止,字符串后面会自动加上NUL终止符。
[xxx](char*):根据给定组合的字符从输入中读取一串字符,参数必须指向一个足够大的字符数组。当遇到第一个不在给定组合中出现的字符时,输入就停止。字符串后面会自动加上NUL终止符。代码%[abc]表示字符组合包括a,b和c。%[^abc]表示字符组合为a,b,c之外的所有字符。右方括号也可以出现在字符列表中,但他必须是列表的第一个字符。至于横杠是否用于指定某个范围的字符(例如%[a-z]),则因编译器而异。
p( void* ):输入预期为一串字符,诸如那些由printf函数的%p格式代码所产生的输出。他的转换方式因编译器而异,但转换结果将和按照上面描述的进行打印所产生的字符的值是相同的。(?)
n(int*):到目前为止通过这个scanf函数的调用从输入读取的字符数被返回。%n转换的字符并不计算在scanf函数的返回值之内,他本身并不消耗任何输入。(?)
%:这个代码与输入中的一个%相匹配,该%符号将被丢弃。(?)

上面没整明白的先放一下,下面先移步printf家族
printf函数家族用于创建格式化的输出,原型如下:
int fprintf( FILE *stream,char const *format,…);
int printf( char const *format,…);
int sprintf( char *buffer,char const *format,…);
使用printf,结果输出送到标准输出。使用fprintf,你可以使用任何输出流,而sprintf把他的结果作为一个NUL结尾的字符串存储到指定的buffer缓冲区而不是写入到流中。
这3个函数的返回值是实际打印或存储的字符数。

注意:
1.sprintf是一个潜在的错误根源,缓冲区的大小并不是sprintf函数的一个参数,所以要输出的结果很长时就可能溢出缓冲区,改写缓冲区内存后面位置的数据。要杜绝这个问题可以有下面两个方法:
(1).声明一个很大的缓冲区,哎,其实还好不到哪里去。
(2).预估要输出的字符串的长度,并且用格式代码中一个可选字段来限制,这样感觉就会好了很多。


2.另一个错误来源是函数的参数类型与对应到格式代码不匹配,通常这个错误将导致输出结果是垃圾,也可能导致程序失败。和scanf函数家族一样,这些函数无法验证一个值是否具有格式代码所表示的正确类型,所以保证他们相互匹配是程序员的责任。

与scanf一样,printf家族最重要的也是他的格式代码,下面就重点研究一下:
printf函数原型中的format字符串可能包含格式代码,使输出根据指定的方式格式化,至于其他字符则原样输出。

格式代码由一个百分号开头,后面跟(1).零个或多个标志字符,用于修改有些转换的执行方式,(2).一个可选的最小字段宽度,(3).一个可选的精度,(4).一个可选的修改符,(5).转换类型。

格式代码具体如下:
c(int):参数被裁剪为unsigned char类型并作为字符进行打印。
d,i(int):参数作为一个十进制整数打印。如果给出了精度而且值的位数少于精度位数,前面就用0填充。
u,o,x,X(unsigned int):参数作为一个无符号值打印,u使用十进制,o使用八进制,x或X使用十六进制,两者的区别是x约定使用abcdef,而X约定使用ABCDEF。
f(double):参数按照常规的浮点格式打印,精度字段决定小数点后面的位数,缺省值是6。
e,E(double):参数根据指数形式打印。例如,6.023000e23是使用代码e, 6.023000E23是使用代码E。小数点后面的位数由精度字段决定,缺省值是6。
g,G(double):参数以%f或%e(如G则%E)的格式打印,取决于他的值。如果指数大于等于-4但小于精度字段就使用%f格式,否则使用指数格式。(?)
s(char*):打印一个字符串。
p(void*):指针值被转换为一串因编译器而异的可打印字符。这个代码主要是和scanf中的%p代码组合使用。
n(int*):这个代码是独特的,因为他并不产生任何输出,相反,到目前为止函数所产生的输出字符数目将被保存到对应的参数中。
%:打印一个%字符。

标志字符具体如下:
-:使值在字段中左对齐,缺省情况下是右对齐。
0:当数值为右对齐时,缺省情况下是使用空格填充值左边未使用的列。这个标志表示用0来填充,他可用于d,i,u,o,x,X,e,E,f,g和G代码。使用d,i,u,o,x和X代码时,如果给出了精度字段,零标志就被忽略,如果格式代码中出现了负号标志,零标志也没有效果。
+:如果值非负会给他加上一个+号,缺省情况下+号不会显示,如果值为负就会显示一个负号。
空格:只用于转换有符号值的代码,这个标志把一个空格添加到一个值的开始位置。注意这个标志和正号标志是相互排斥的,如果两个同时给出,空格标志便会被忽略。
#:选择某些代码的另一种转换形式,具体为:
o:保证产生的值以一个零开头。
x,X:在非零值前面加0x前缀(%X则为0X)。

e,E,f:确保结果始终包含一个小数点,即使他后面没有数字。
g,G:和上面的e,E和f代码相同,另外缀尾的0并不从小数中去除。
注意:
1.字符宽度是一个十进制整数,用于指定将出现在结果中的最小字符数,如果值的字符数少于字段宽度,就对他进行填充,如果大于就原样输出。
2.对于d,i,u,o,x和X类型的转换,精度字段指定将出现在结果中的最小的数字个数并覆盖零标志。如果转换后的值的位数小于宽度,就在他的前面插入零。如果大于则原样输出。这和字符串就又不一样了。如果值为零且精度也为零,则转换结果就不会产生数字。
对于e,E,f类型的转换,精度决定将出现在小数点之后的数字位数。
对于g和G类型的转换,他指定将出现在结果中的最大有效位数。
当使用s类型的转换时,精度指定将被转换的最多字符数。如果小于精度数就原样输出,如果大于就截取为精度数。这个和宽度限制正好反了一下。
精度以一个句点开头,后面跟一个可选的十进制整数,如果未给出整数,精度的缺省值为零。
3.如果用于表示字段宽度和(或)精度的十进制整数由一个星号代替,那么printf的下一个参数(必须是个整数)就提供宽度和(或)精度。因此,这些值可以通过计算获得而不必预先指定。

好了,现在该说修改符了,他们用于指定整数和浮点数参数的准确长度,具体如下:
h(d,i,u,o,x,X):一个(可能是无符号)short型整数
h(n):一个指向short型整数的指针。
l(d,i,u,o,x,X):一个(可能是无符号)long型整数
l(n): 一个指向long型整数的指针。
L(e,E,f,g,G):一个long double型值。

注意:1.在int和short int长度相等的环境里,h修改符没有效果。
2.在int和long int长度相等的机器上,l修改符并无效果。但在所有其他的机器上,需要使用l修改符,因为这些机器上的长整型分为两部分传递给运行时堆栈。如果这个修改符并未给出,那就只有第一部分被提取用于转换。这样,不仅转换将产生不正确的结果,而且这个值的第二部分被解释为一个单独的参数,这样就破坏了后续参数和他们的格式代码之间的对应关系。因此,当打印长整数值时,最好坚持使用l修改符。这样不但安全而且也可以提高可移植性。

%g要研究一下,到现在还是不懂。

该进入二进制I/O的世界了。把数据写到文件效率最高的方法是用二进制形式写入。二进制输出避免了在数值转换为字符串的过程中所涉及的开销和精度损失。但二进制数据并非人眼所能阅读,所以这个技巧只有当数据被另一个程序按顺序读取时才能使用。

fread函数用于读取二进制数据,fwrite函数用于写入二进制数据。他们的原型如下:
size_t fread( void *buffer,size_t size,size_t count, FILE *stream );

size_t fread( void *buffer,size_t size,
size_t count, FILE *stream );

buffer是一个指向用于保存数据的内存位置的指针,size是缓冲区中每个元素的字节数,count是读取或写入的元素数,当然stream是数据读取或写入的流。
buffer参数被解释为一个或多个值的数组,count参数指定数组中有多少个值,所以读取或写入一个标量时,count的值应为1。
函数的返回值是实际读取或写入的元素(并非字节)数目。如果输入过程中遇到了文件尾或者输出过程中出现了错误,这个数字可能比请求的元素数目要小。
说了这么多,感觉理解的还是不够清楚,那就举个例子来说:
将一个浮点数组的第2-5个元素写到一个文件上,程序如下:
float data[10]
fwrite(&data[2],sizeof(float),4,fp)

看了这个例子后就有些明白了吧。

注意:使用二进制I/O的基本问题是,他只能用于读在同一系统上已写的数据。现在很多系统通过网络互联,常常在一个系统上写的数据要在另一个系统上进行处理。在这种环境下,这两个函数可能就不能正常工作,其原因是:
1.在一个结构中,同一成员的偏移量可能因编译器和系统而异。这意味着即使在同一个系统上,一个结构的二进制存放方式也可能因编译器选项的不同而不同。
2.用来存储多字节整数和浮点值的二进制格式在不同的机器体系结构间也可能不同。
在不同系统之间交换二进制数据的实际解决方法是使用较高级的协议。


文件操纵函数
    有两个函数用于操纵文件但不执行任何输入/输出操作。他们的原型如下所示:
int remove( char const *filename );
int rename( char const *oldname,char const *newname );

如果执行成功,这两个函数都返回零值。如果失败,他们都返回非零值。
remove 函数删除一个指定的文件,如果当remove被调用时文件处于打开状态,其结果取决于编译器。
rename 函数用于改变一个文件的名字,从oldname改为newname。如果已经有一个名为newname的文件存在,其结果取决于编译器。如果这个函数失败,文件仍然可以用原来的名字进行访问。

临时文件(linux c)
    通常,程序需要以文件的形式使用临时存储,也许是存储计算的中间结果,或者是在实际操作前所做的文件拷贝备份。总之吧,是需要的。
linux c的临时文件一共涉及到4个函数:tmpnam tmpfile mktemp mkstemp ,下面一个个详细介绍和进行比较:
临时文件的大量使用隐藏了他的一个缺点,我们必须小心来确保程序会选择一个唯一的名字来使用临时文件,否则多线程/多进程情况下就会相互影响。
tmpnam函数就是在这个时候应运而生的,他用来生成一个唯一的临时文件名。原型如下:
char *tmpnam( cahr *name );
函数会返回一个与现存的文件不同的可用的文件名。如果传递给函数的参数为NULL,那么文件名就会被写入到一个静态数组中,然后返回这个静态数组的指针。如果不为空,参数便假定是一个指向长度至少为L_tmpnam (我测了一下,这个值在linux64上是20)的字符数组的指针,然后文件名就会写到这个数组中,返回指向这个数组的指针。

注意:一个程序中,tmpnam至多可以被调用TMP_MAX(我测了一下,这个值在linux64上是238328)次,而且每次都会生成一个不同的文件名。

文件名现在唯一了,但还有一个问题,那就是还是不能避免多个程序可能打开同一个临时文件的风险,至少有这种风险,如果临时文件被创建后就直接打开就完全没有这种风险了,这时tmpfile就应运而生了。它的原型如下:
FILE *tmpfile( void );
这个函数创建了一个文件,并且以wb+模式打开(这使它可用于二进制和文本数据),返回一个指向唯一的临时文件的流指针,没有输入参数。
在所有到文件的引用被关闭以后,这个文件会被自动删除。如果发生错误,tmpfile会返回一个空指针,并且设置errno变量。

注意:如果临时文件必须以其他模式打开或者由一个程序打开但由另一个程序读取,就不适合用tmpfile函数创建。在这些情况下,必须使用fopen函数,而且当结果文件不再需要时必须使用remove函数显式地删除。
说了这么多,还是用看一个例子后会理解的较为深刻一些的:

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

int main()                                                                      
{

       char tmpname[L_tmpnam];
        char *filename;
        FILE *tmpfp;
        filename = tmpnam(tmpname);
        printf( "Temporary file name is: %s\n", tmpname);
        printf( "Temporary file name is: %s\n", filename);
        printf( "Temporary file name is: %ld\n", TMP_MAX );
        tmpfp = tmpfile();
        if(tmpfp)
                printf("Opened a temporary file OK\n" );
        else
                perror("tmpfile");
            
        return 0;
}                               

如果在执行上面函数的时候会报下面的警告:
/tmp/ccrI2PVL.o: In function `main':
testc.c:(.text+0x56): warning: the use of `tmpnam' is dangerous, better use `mkstemp'
那mkstemp是个什么函数呢,继续研究mktemp mkstemp。
mktemp mkstemp函数的原型如下:
char *mktemp(char *template);
int mkstemp(char *template); (均包含在include <stdlib.h>中)
mktemp用法与tmpnam差不多,只是名字部分确定时,template后面六个字符必须为X,例如tmpXXXXXX。(注意是必须是X,换成Y或Z等什么的都不可以)。另外还有一个不同就是传入的参数不能为NULL,这个我试过。例子如下:
#include <stdlib.h>
#include <stdio.h>
//char *mktemp(char *template);

int main(int argc,char *argv[])
{
        char filename[] = "temp-XXXXXX";
        char *name;
        if((name = mktemp(filename)) == NULL)
        {
                perror("mktemp");
                exit(1);
        }

        printf("name:%s,filename:%s\n",name,filename);
        exit(0);
}

mkstemp函数在创建与打开临时文件方面与tmpfile相类似。文件名由与mktemp相同的方式生成的,但是返回的结果是一个打开的,底层文件描述符,以可读写模式和0600 权限来打开该文件。如果文件打开失败则返回NULL,并把错误代码存在errno中。 例子如下:
#include <stdlib.h>

int main(int argc,char *argv[])
{
        int fd;
        char tmpfile[] = "temp-XXXXXX";

        if((fd = mkstemp(tmpfile))< 0)
        {
                perror("mkstemp");
                exit(1);
        }

        close(fd);
        exit(0);
}

注意:参数template所指的文件名称字符串必须声明为数组,如:
           char template[ ] =”template-XXXXXX”;
           千万不可以使用下列的表达方式
           char *template = “template-XXXXXX”;(mktemp mkstemp)

通常,我们应使用创建与打开函数tmpfile与mkstemp,而不是tmpnam与mktemp。

流错误函数
int feof( FILE *stream );        如果流当前处于文件尾,feof函数返回真
int ferror( FILE *stream );        ferror函数报告流的错误状态,如果出现任何读/写错误的话,函数就返回真。
void clearerr( FILE *stream );        复位错误标志,clearerr的作用是使文件错误标志和文件结束标志置为0. 它没有返回值,也未定义任何错误。

改变缓冲方式
    在流上执行的缓冲方式有时并不合适,下面这两个函数可以用于对缓冲方式进行修改。

void setbuf( FILE *stream, char *buf );
int setvbuf( FILE *stream,char *buf,int mode,size_t size );

setbuf 设置了另一个数组,用于对流进行缓冲。这个数组的字符长度必须为BUFSIZE(它在stdio.h中定义)。为一个流自行指定缓冲区可以防止I/O函数库为他动态分配一个缓冲区。如果用一个NULL参数调用这个函数,setbuf函数将关闭流的所有缓冲方式。

注意:为流缓冲区使用一个数组是很危险的,如果在流关闭前,程序的执行流离开了数组声明所在的代码块,流会继续使用这块内存,但此时它可能已经分配给了其他函数另作他用。

setvbuf函数更为通用。mode参数用于指定缓冲的类型。_IOFBF指定一个完全缓冲的流, _IONBF 指定一个不缓冲的流,_IOLBF指定一个行缓冲的流。所谓行缓冲,就是每当一个换行符写入到缓冲区时,缓冲区便进行刷新。
buf和size参数用于指定需要使用的缓冲区。如果buf为NULL,那么size的值必须是0.一般而言,为了提高一些效率,最好用一个长度为BUFSIZ(我测了一下,这个值在linux64上是8192)或它的整数倍的字符数组作为缓冲区。因为绝大多数操作系统在内部对磁盘的输入/输出进行缓冲操作,它的缓存区的长度应该就是BUFSIZ。如果你自行指定了一个缓冲区,但它的长度却不是操作系统内部使用的缓存区的整数倍,就可能需要一些额外的磁盘操作,用于读取或写入一个内存块的一部分。


定位函数

在正常情况下,数据以线性的方式写入,这意味着后面写入的数据在文件中的位置是在以前所有写入数据的后面。C同时支持随机访问I/O,也就是以任意顺序访问文件的不同位置。随机访问是通过在读取或写入之前先定位到文件中需要的位置来实现的。有两个函数用于执行这项操作,他们的原型如下:
long ftell( FILE *stream );
int fseek( FILE *stream,long offset,int from );
    ftell函数返回流的当前位置,也就是说,下一个读取或写入将要开始的位置距离文件起始位置的偏移量。这个函数允许你保存一个文件的当前位置,这样你可能在将来会返回到这个位置。在二进制流中,这个值就是当前位置距离文件起始位置之间的字节数。在文本流中,这个值表示一个位置,但它并不一定准确地表示当前位置和文件起始位置之间的字符数,因为有些系统将对行末字符进行翻译转换,但是ftell函数的返回值总是可以用于fseek函数中,作为一个距离文件起始位置的偏移量。
    fseek函数允许你在一个流中定位,这个操作将改变下一个读取或写入操作的位置。它的第一个参数是需要改变的流,第2和第3个参数标识文件中需要定位的位置。第3和第2个参数的具体使用方法如下:
如果from是:                    你将定位到:
SEEK_SET                    从流的起始位置起offset个字节,offset必须是一个非负值                    
SEEK_CUR                    从流的当前位置起offset个字节,offset的值可正可负
SEEK_END                    从流的尾部位置起offset个字节,offset的值可正可负,如果是正值他将定位到文件尾的后面

注意:
1.试图定位到一个文件的起始位置之前是一个错误。
2.定位到文件尾之后并进行写入将扩展这个文件,如果进行读取将导致返回一条“到达文件尾”的信息。
3.在二进制流中,从SEEK_END进行定位可能不被支持,所以应该避免。
4.在文本流中,如果from是SEEK_CUR或SEEK_END,offset必须是零。如果from是SEEK_SET,offset必须是一个从同一个流中以前调用ftell所返回的值。

之所以存在这些限制,部分原因是文本流所执行的行末字符映射。由于这种映射的存在,文本文件的字节数可能和程序写入的字节数不同。因此,一个可移植的程序不能根据实际写入字符数的计算结果定位到文本流的一个位置。
注意:用fseek改变一个流的位置会带来三个副作用。首先,行末指示字符被清除。其次,如果在fseek之前使用ungetc把一个字符返回到流中,那么这个被退回的字符会被丢弃,因为在定位操作以后,它不再是“下一个字符”。最后,定位允许你从写入模式切换到读取模式,或者回到打开的流以便更新。
另外,还有三个额外的函数,用一些限制更严的方式执行相同的任务。他们的原型如下:
void rewind(FILE *stream );
int fgetpos( FILE *stream,fpos_t *position );
int fsetpos( FILE *stream,fpos_t const *position );
rewind 函数将读/写指针设置回指定流的起始位置。它同时清除流的错误提示标志。
fgetpos 和 fsetpos 函数分别是ftell 和 fseek 函数的替代方案。他们的主要区别在于这对函数接受一个指向fpos_t的指针作为参数。fgetpos在这个位置存储文件的当前位置,fsetpos把文件位置设置为存储在这个位置的值。
注意:用fpos_t表示一个文件位置的方式并不是由标准定义的。它可能是文件中的一个字节偏移量,也可能不是。因此,使用一个从fgetpos函数返回的fpos_t类型的值唯一安全的用法是把它作为参数传递给后续的fsetpos函数。

注意:fpos_t是一个结构体,原型如下:
typedef _G_fpos_t fpos_t;

typedef struct
{
  __off_t __pos;
   __mbstate_t __state;
} _G_fpos_t;

所以不可以直接对他进行赋值,如果要赋值的话可以采用这种方式:pos.__pos = 10;还是举个例子吧

fsetpos的例子:
 FILE   *fp;
        fpos_t pos;
        char   buffer[50];
        if((fp = fopen( "./text2.txt", "rb" )) == NULL )
                printf( "Trouble opening file/n" );
        else
        {   
                pos.__pos = 10;
                if( fsetpos( fp, &pos ) != 0 )
                                                                                                                                                                                              
                        perror( "fsetpos error" );

                else
                {   
                        fread( buffer, sizeof( char ), 16, fp );
                        printf( "16 bytes at byte %ld: %.16s/n", pos.__pos, buffer );

                }   

        }   

        fclose( fp );   

fgetpos的例子:
          FILE*fp                                                                                                                                                                           
   char string[] = "This is a test";
   fpos_t pos;
   fp = fopen("test.txt", "w+");
   fwrite(string, strlen(string), 1, fp);
   fgetpos(fp, &pos);
   printf("The file pointer is at byte %ld\n", pos);
   fseek(fp,3,0);
   fgetpos(fp, &pos);
   printf("The file pointer is at byte %ld\n", pos);
   fclose(fp);

具体输出什么,就自己看吧,看不明白就执行一下。



终于要讨论unix系统I/O了,unix系统中的大多数文件I/O只需用到5个函数:open read 


write lseek以及close。


注意:这里所说的函数被称为不带缓冲的I/O,不带缓冲指的是每个read和write都调用内核


中的一个系统调用。这些不带缓冲的I/O函数不是ISO C的组成部分,但是,他们是POSIX.1


和Single UNIX Specification 的组成部分。


要讨论系统I/O,就需要先讨论文件描述符:
文件描述符是一个非负整数,对于内核而言,所有打开的文件都通过文件描述符引用,当打


开一个现有文件创建一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时


,使用open或返回的文件描述符标志该文件,将其作为参数传送给read或write。
注:按照惯例,unix系统shell使用文件描述符0与进程的标准输入与关联,文件描述符1与


标准输出相关联,文件描述符2与标准出错输出相关联。这是各种shell以及很多应用程序使


用的惯例,而unix内核无关。在依从posix的应用程序中,为了可读性,幻数0 1 2应当替


换成符号常量STDIN_FILENO  STDOUT_FILENO和STDERR_FILENO。这些常量都定义在头文件


<unistd.h>中。


注意:文件描述符的变化范围是0-OPEN_MAX(每个进程的最大打开文件数)。早期的unix


系统这个值为19,现在很多系统将这个值增至63.对于FreeBSD5.2.1  Mac OS X10.3以及


Solaris 9,文件描述符的变化范围实际上是无限的,它只受到系统配置的存储器总量 整型


的字长以及系统的管理员所配置的软限制硬限制的约束。Linux2.4.22对于每个进程的文件


描述符的硬限是1048 576。


接下来一个一个介绍用到的函数.先说open函数
调用open函数可以打开或创建一个文件,原型如下:
#include <fcntl.h>
int open (const char *pathname,int oflag,.../* mode_t mode */ );


pathname是要打开或创文件的名字。oflag参数可用来说明此函数的多个选项。由下列一个


或多个常量进行“或”运算构成oflag参数(这些常量定义在<fcntl.h>头文件中)。
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读 写打开


注:大多数实现将O_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2,以与早期的程序


兼容。


这三个常量中必须指定一个且只能指定一个。下列常则是可选择的:
O_APPEND 每次写时都追加到文件的尾端。
O_CREAT 若此文件不存在,则创建它。使用此选项时,需要第三个参数mode,用


其指定该新文件的访问权限位。
O_EXCL 用此可以测试一个文件是否存在,如果同时指定了O_CREAT,而文件如果


存在,则会报错,如果不存在则创建此文件。这使测试和创建两者成为


一个原子操作。
O_TRUNC 如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0.
O_NOCTTY 如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终


端。
O_NONBLOCK 如果pathname指的是一个FIFO 一个块特殊文件或一个字符特殊文件,则


此选项为文件的本次打开操作和后续的I/O操作设置为非阻塞模式。


下面三个标志也是可选的,他们是Single UNIX Specification以及POSIX.1中同步输入和


输出选项的一部分

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值