C语言:文件操作

1. 为什么使用文件

到目前为止,我们都是从标准输入读取数据,并从标准输出输出数据.标准输入和标准输出是操作系统自动提供给程序访问的.

就比如写通讯录管理系统的时候,虽然能在内存中暂时保存相关信息,但是程序运行完毕后,相关信息也就不复存在了.

要将数据持久化,就需要将数据存放在硬盘文件,或者放到数据库里.

C提供了强大的文件通信方法,可以在程序中打开文件,然后使用特殊的I/O函数读取文件中的信息或者把信息写入文件.

2. 什么是文件

文件(file)通常是在磁盘或固态硬盘上一段已命名的存储区.

在这里插入图片描述

例如这些文件夹里都存放了各种各样的文件,即使电脑重启,这些文件依然存放在磁盘中,并没有被删除.

对我们而言,stdio.h就是一个文件的名称,该文件中包含了一些有用的信息.

对操作系统来言,文件更复杂一些.例如:大型文件会被分开存储,或者包含一些额外的数据,方便操作系统进行文件管理.

C把文件看作是一系列连续的字节,每个字节都能被单独读取.这与UNIX环境中的文件结构相对应.

在程序设计中,我们一般谈的文件有两种:程序文件,数据文件(从文件功能的角度来分类的)

2.1 程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe).

例如:
在这里插入图片描述

这些全是程序文件

2.2 数据文件

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

本章讨论的都是数据文件.

之前的很多数据输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上.

其实更多时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的数据文件.

2.3 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用

文件名包含3部分: 文件路径 + 文件名主干 + 文件后缀

例如: 在我的桌面上存放了一个关于ASCII码表的文件
在这里插入图片描述

那么它的文件名就是: F:\Desktop\ASCII码表.pdf

这是绝对路径,若我已经在桌面上了,..\ASCII码表.pdf,也是可以访问到文件的.

为了方便起见,文件标识通常被称为文件名

3. 文件访问

在读取一个文件之前,必须通过库函数fopen打开该文件,fopen用文件名与操作系统进行某些必要的连接和通信(我们不需要关心这些细节),并返回一个随后可以用于文件读写操作的指针.

3.1 文件指针

fopen返回的指针就叫文件指针

它指向一个包含文件信息的结构,这些信息包括: 缓冲区的位置,缓冲区中当前字符的位置,文件的读或写状态,是否出错或是否已经到达文件结尾等等.

用户不必关心这些细节,因为在<stdio.h>中已经定义了一个包含这些信息的结构FILE

例如,VS2013编译环境提供的<stdio.h>头文件中有以下的文件类型声明:

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 *pf; //文件指针变量

定义pf是一个指向结构FILE的指针.可以使pf指向某个文件的文件信息区(是一个结构体变量).通过该文件信息区中的信息就能访问该文件.也就是说,通过文件指针能够找到与它关联的文件

在这里插入图片描述

3.2 文件的打开和关闭

文件在读写之前应该先打开文件,在操作完毕后应该关闭文件

ANSIC规定使用fopen函数打开文件,使用fclose来关闭文件

//打开文件
FILE *fopen(const char* filename, const char* mode);

//关闭文件
int fclose(FILE* stream);

filename参数指的是文件名,mode参数指的是打开方式
打开方式如下:

文件使用方式含义如指定文件不存在
" r "(只读为了输入数据,打开一个已经存在的文本文件出错
" w "(只写)为了输出数据,打开一个文本文件建立一个新文件
" a "(追加)向文本文件尾添加数据建立一个新文件
" rb "(只读)为了输入数据,打开一个二进制文件出错
" wb "(只写)为了输出数据,打开一个二进制文件建立一个新文件
" ab "(追加)向一个二进制文件尾添加数据出错
" r+ "(读写)为了读和写,打开一个文本文件出错
" w+ "(读写)为了读和写,建立一个新的文件建立一个新的文件
" a+ "(读写)打开一个文件,在文件尾进行读写建立一个新的文件
" rb+ "(读写)为了读和写,打开一个二进制文件出错
" wb+ "(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
" ab+ "(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

注意: 带有+的更新方式允许对同一文件进行读和写.在读和写的交叉过程中,必须调用fflush函数或文件定位函数.对缓冲区进行刷新,保证读写操作作用到文件中.


使用实例:

  • 如果本地没有该文件,要读会出错:
#include <stdio.h>

int main(void)
{
    FILE* pFile;

    //打开文件,以文本格式打开,读模式
    pFile = fopen("myfile.txt", "r");

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

会报如下错误:
在这里插入图片描述


  • 但是写模式,如果没有文件会自动创建一个新文件:
#include <stdio.h>

int main(void)
{
    FILE* pFile;

    //打开文件,以文本格式打开,写模式
    pFile = fopen("myfile.txt", "w");

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

运行程序结果如下:
在这里插入图片描述

打开文件,确实是程序中写入的信息
在这里插入图片描述


  • 如果想使用绝对路径,要注意路径中的斜杠,/本身是转义字符,//才能得到/
#include <stdio.h>

int main(void)
{
    FILE* pFile;

    //打开文件,以文本格式打开,写模式
    pFile = fopen("//root//myfile.txt", "w");

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

程序运行后发现myfile.txt文件出现在了root路径下:

在这里插入图片描述


注意: 不关闭文件的后果:如果只打开不释放,文件会被占用;文件的内容可能会损坏.

4. 文件的读写

文件被打开后,就需要考虑采用哪种方法对文件进行读写

其中最为简单的就是getcputc

  • getc从文件中返回下一字符,它需要知道文件指针.如果到达文件尾或出现错误,该函数将返回EOF.
int getc(FILE* fp);
  • putc将字符c写入fp指向的文件中,并返回写入的字符.如果发生错误,则返回EOF.

类似于getchar()putchar(),getcputc是宏而不是函数.


启动一个C语言程序时,操作系统环境负责打开3个文件,并将这3个文件的指针提供给程序.这3个文件分别是标准输入,标准输出和标准错误,相应的文件指针分别为stdin,stdout,stderr,它们在<stdio.h>声明.

在大多数环境中,stdin指向键盘,而stdout和stderr指向显示器.

getchar()putchar()可以通过getc,putc,stdin和stdout定义如下:

#define getchar()   getc(stdin)
#define putchar(c)  putc((c), stdout)

一个记录文件中字符个数,并在屏幕输出结果的程序:

#include <stdio.h>
#include <stdlib.h>     //提供exit()的原型

int main(int argc, char* argv[])
{
    int ch;             //读取文件时,存储每个字符的地方
    FILE* fp;           //文件指针
    unsigned long count;   

    //如果命令行输入格式不对
    if (argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    //如果文件不存在
    if ((fp = fopen(argv[1], "r")) == NULL) 
    {
        printf("Can't open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    //读取字符,并打印到屏幕上
    while((ch = getc(fp)) != EOF)
    {
        putc(ch, stdout);   //与 putchar(ch); 相同
        count++;
    }

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

    //打印结果
    printf("File %s has %lu characters\n", argv[1], count);

    return 0;
}

一开始没有创建myfile.txt文件,程序直接提示没有文件,并退出
在这里插入图片描述

随后我创建了文件,并输入如下数据:
在这里插入图片描述

再运行程序得到如下结果:
在这里插入图片描述


C语言库提供了很多文件I/O相关的函数如下

4.1 顺序读写函数

顺序读写,就是从头按照顺序对文件进行读写

功能函数名适用于
字符输入函数fgetc 所有输入流
字符输出函数fputc 所有输出流
文本行输入函数fgets所有输入流
文本行输出函数 fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread 文件
二进制输出fwrite文件

4.1.1 fgetc()fputc()函数 (字符输入输出)

使用操作与 getcputc是一样的,这里不过多做演示

需要注意的是当文件读取中出现异常或者文件读到结尾,会返回EOF.
写入出现错误,也会返回EOF

#include <stdio.h>

int main(void)
{
    FILE* fp = fopen("myfile.txt", "r");
    int ret = 0;

    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    ret = fgetc(fp);
    printf("%d\n", ret);
    ret = fgetc(fp);
    printf("%d\n", ret);
    ret = fgetc(fp);
    printf("%d\n", ret);
    ret = fgetc(fp);
    printf("%d\n", ret);

    fclose(fp);
    fp = NULL;

    return 0;
}

这里的myfile.txt中我输入了以下数据(注意文件中自带换行符):
在这里插入图片描述

程序运行结果如下:
在这里插入图片描述

对应ASCII码表确实是我存入的数据
在这里插入图片描述

-1EOF,代表文件读到结尾

4.1.2 fgets()fputs函数 (文本行输入输出)

char * fgets ( char * str, int num, FILE * stream );
int fputs ( const char * str, FILE * stream );
  • fgets通过第二个参数限制读入的字符数来解决gets.如果该参数为n,则fgets读取入n-1个字符,或者读到第一个换行符.最后一个位置用来存放\0;
  • fgets读到一个换行符,也会存储到字符串中,而gets会丢弃换行符
  • fputs仅仅写入字符串,不会像puts把换行符也放入字符串中.

#include <stdio.h>
#define STLEN 14

int main(void)
{
    char words[STLEN];

    puts("Enter a string, please.");
    fgets(words, STLEN, stdin);
    printf("puts(), then fputs()\n");
    puts(words);
    fputs(words, stdout);

    puts("Enter a string, please.");
    fgets(words, STLEN, stdin);
    printf("puts(), then fputs()\n");
    puts(words);
    fputs(words, stdout);

    puts("Done.");

    return 0;
}

程序运行结果如下:
在这里插入图片描述

  • 第一行输入, apple pie, 比fgets()读入的整行输入短,因此, apple pie\n\0被存到数组中.
  • 第二行输入, strawberry shortcake, 比fgets()读入的整行输入要长,fgets()只读入了 13 个字符, 并把strawberry sh\0存储到数组中.

  • fgets()返回指向char的指针.如果一切进行顺利,该函数返回的地址与传入的第 1 个参数相同.
  • 如果fgets()读到文件末尾,则会返回一个空指针.
#include <stdio.h>
#define STLEN 10

int main(void)
{
    char words[STLEN];
    FILE* fp = fopen("myfile.txt", "r");

    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    while(fgets(words, STLEN, fp) != NULL && words[0] != '\n')
        fputs(words, stdout);
    puts("Done.");

    return 0;
}

我在文件myfile.txt中存放了以下数据:
在这里插入图片描述

运行程序后打印出一样的结果:
在这里插入图片描述

  • 虽然fgets()每次只读STLEN - 1 = 9个字符.但是这样看来好像并没有影响.
  • 一开始读了 By the wa, 存储By the wa\0,但此时并未换行,继续读
  • 第二次读了 y, the ge, 存储y, the ge\0,fputs继续打印
  • 直到文件结束,读取完毕.

4.1.3 fprintf()fscanf()函数 (按格式输入输出)

int fprintf ( FILE * stream, const char * format, ... );
int fscanf ( FILE * stream, const char * format, ... );

文件I/O函数fprintf()fscanf()函数的工作方式与printf()scanf()函数类似,区别在于前者需要用第1个参数指定待处理的文件.


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 41

int main(void)
{
    FILE* fp;
    char words[MAX];

    //如果访问文件失败
    if ((fp = fopen("wordy", "a+")) == NULL)
    {
        fprintf(stdout, "Can't open \"wordy\" file.\n");
        exit(EXIT_FAILURE);
    }

    puts("Enter words to add to the file; press the #");
    puts("key at the beginning of a line to terminate.");

    //按格式从键盘输入 存储到字符数组中
    while((fscanf(stdin, "%40s", words) == 1) && (words[0] != '#'))
    {
        fprintf(fp, "%s\n", words); //按格式输出到文件中
    }
        
    puts("File contents:");
    rewind(fp);     //返回到文件开始处

    //按格式将文件内容读取到字符数组中
    while(fscanf(fp, "%s", words) == 1)
    {
        puts(words);
    }

    puts("Done!");

    if (fclose(fp) != 0)
        fprintf(stderr, "Error closing file\n");
    
    return 0;
}
  • 该程序可以在文件中添加单词.
  • 使用"a+"模式,程序可以对文件进行读写;写是在末尾追加内容,而读可以读整个文件
  • 每当有空白字符,代表一个字符串读写结束.

程序运行结果如下:
在这里插入图片描述

4.1.4 fread()fwrite()函数 (二进制I/O)

在介绍fread()fwrite()函数之前,先要了解一些背景知识.

  • 之前用的标准I/O函数都是面向文本的,用于处理字符和字符串的.
  • 在文本中保留数值数据,则要用到fprintf()函数.
    • 如果要保留浮点数例如.1 / 3.表达式为fprintf(fp, "%f", .1 / 3).
    • 将该数存储为8个字符0.333333保存到文本文件中,与真正的32位浮点数存储的值,还是有不小的误差的.

为保证数值在存储前后一致, 最精确的做法是使用与计算机相同的位组合来存储.因此,double类型的值应该存储在一个double大小的单元中

如果以程序所用的表示法把数据存储到文件中,则称以二进制形式存储数据.fread()fwrite()用以二进制形式处理数据.

在这里插入图片描述


size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

例如将结构体按二进制模式写入文件,并读出:

#include <stdio.h>

struct S
{
    int a;
    double b;
};

int main(void)
{
    FILE* fp = fopen("myfile", "w+");

    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    struct S s1 = {20, 3.14};
    struct S s2 = {0,};

    fwrite(&s1, sizeof(struct S), 1, fp);
    fflush(fp);
    rewind(fp);
    fread(&s2, sizeof(struct S), 1, fp);

    fprintf(stdout, "%d %f\n", s2.a, s2.b);

    return 0;
}

程序运行结果如下:
在这里插入图片描述


验证数据的二进制存储:

#include <stdio.h>

int main(void)
{
    int num = 10000;
    FILE* fp = fopen("myfile", "wb");
    
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    fwrite(&num, sizeof(int), 1, fp);

    fclose(fp);
    fp = NULL;

    return 0;
}

程序运行后,myfile存放了如下数据:
在这里插入图片描述

使用hexdump指令查看二进制文件,得到以下结果:
在这里插入图片描述

也就是说小端模式存储下,原数字十六进制应该是0x2710,使用计算器计算恰好是10000
在这里插入图片描述

4.2 随机访问函数

4.2.1 fseek()ftell()

有了fseek()函数,便可把文件看作是数组,在fopen()打开的文件中直接移动到任意字节处.

  • fseek()函数有 3 个参数,返回int类型的值.
  • ftell()函数返回一个long类型的值,表示文件中的当前位置.

下面的程序倒序打印了文件中的内容

#include<stdio.h>

#define CNTL_Z '\032'           //DOS 文本文件中的文件结尾标记

int main(void)
{
    char ch;
    FILE* fp;
    long count, last;

    //如果文件打开失败
    if ((fp = fopen("myfile", "rb")) == NULL)
    {
        perror("fopen");
        return 1;
    }

    fseek(fp, 0L, SEEK_END);                //定位到文件末尾
    last = ftell(fp);

    for (count = 1L; count <= last; count++)
    {
        fseek(fp, -count, SEEK_END);        //回退
        ch = getc(fp);

        if (ch != CNTL_Z && ch != 'r')
            putchar(ch);
    }

    putchar('\n');
    fclose(fp);

    return 0;
}

事先我已经在文件中存放了如下信息:
在这里插入图片描述

程序运行结果如下:

在这里插入图片描述


int fseek ( FILE * stream, long int offset, int origin );
  • fseek()的第一个参数是FILE指针,指向待查找的文件,fopen()应该已经打开该文件了
  • fseek()的第二个参数是偏移量(offset).该参数表示从起始点开始要移动的距离.
    该参数是一个long类型的值,可以为正(前移),为负(后移),为0(保持不动).
  • fseek()的第三个参数是模式,该参数确定起始点.
    根据 ANSI 标准,在stdio.h头文件规定了几个表示模式的明示常量
模式偏移量的起始点
SEEK_SET文件开始处
SEEK_CUR当前位置
SEEK_END文件末尾

下面是调用fseek()的一些实例,fp是一个文件指针:

fseek(fp, 0L, SEEK_SET);    //定位至文件开始处
fseek(fp, 10L, SEEK_SET);   //定位至文件开始处的第10个字节
fseek(fp, 2L, SEEK_CUR);    //定位至当前位置前移2个字节
fseek(fp, 0L, SEEK_END);    //定位至文件末尾处
fseek(fp, -10L, SEEK_END);  //定位至文件结尾处回退10个字节

如果一切正常, fseek()的返回值是 0;
如果出现错误,(例如试图移动的距离超出文件的范围),返回值为 -1

下面还有一个简单实例:

#include <stdio.h>

int main(void)
{
    FILE* fp;

    if ((fp = fopen("myfile", "wb")) == NULL)
    {
        perror("fopen");
        return 1;
    }

    fputs("This is an apple.", fp); //将这行句子写入文件
    fseek(fp, 9L, SEEK_SET);        //定位至文件开始处的第9个字节
    fputs(" sam", fp);              //重新写入,覆盖

    fclose(fp);
    fp = NULL;

    return 0;
}
  • 第一次fputs将句子写入文件中
  • 随后使用fseek()定位至文件开始处的第 9 个字节, 即 n
  • 第二次fputs将从定位出开始写入文件
  • 最后程序运行打开文件得到如下结果:
    在这里插入图片描述

long int ftell ( FILE * stream );
  • ftell()函数的返回类型是long,它返回的是参数指向文件的当前位置据文件开始出的字节数.
  • 文件的第一个字节到文件的开始处的距离是 0 .

下面的代码通过使用fseek()ftell()得到文件的字节数:

#include <stdio.h>

int main(void)
{
    FILE* fp;
    long size;

    if ((fp = fopen("myfile", "rb")) == NULL)
    {
        perror("fopen");
        return 1;
    }

    fseek(fp, 0L, SEEK_END);    //定位到文件结尾
    size = ftell(fp);           
    printf("Size of myfile: %ld bytes.\n", size);

    fclose(fp);
    fp == NULL;

    return 0;
}

还是之前的文件:
在这里插入图片描述

程序运行结果如下:
在这里插入图片描述

4.2.2 rewind()函数 (文件指针回到起始位置)

void rewind ( FILE * stream);
  • rewind()让文件指针指向位置相当于文件起始位置的偏移量为 0 .

利用fseek()ftell()函数验证

#include <stdio.h>

int main(void)
{
    FILE* fp;
    long pos = 0;

    if ((fp = fopen("myfile", "rb")) == NULL)
    {
        perror("fopen");
        return 1;
    }

    fseek(fp, 0L, SEEK_END);
    pos = ftell(fp);
    printf("当前位置相对文件起始位置偏移量为: %ld\n", pos);

    rewind(fp);
    pos = ftell(fp);
    printf("使用rewind后,当前位置相对文件起始位置偏移量为: %ld\n", pos);

    return 0;
}

程序运行结果如下:
在这里插入图片描述


4.3 其他函数

4.3.1 feof()函数

在文件结束时,判断文件因为何种原因导致文件结束的函数,判断是因为读取失败而结束,还是因为遇到文件尾而结束.
如果文件结束,则返回非0值,否则返回0.

注意: 不能用与在文件读取过程中,用来判断是否结束.

feof()函数的作用是: 当文件读取结束的时候,判断读取结束的原因是否是:遇到文件末尾结束


正确判断文件读取是否结束应该如下:

  • 文件文本读取是否结束:判断返回值是否为EOF(fgetc),或者NULL(fgets)
  • 二进制文件的结束判断:判断返回值是否小于实际要读的个数,fread判断返回值个数

下面才是正确使用feof()函数的例子:

  • 文本文件的例子:
#include<stdio.h>

int main(void)
{
    int c;          //用来处理EOF
    FILE* fp;
    if ((fp = fopen("myfile", "r")) == NULL)
    {
        perror("fopen");
        return 1;
    }

    //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF)
    {
        putchar(c);
    }

    //判断是什么原因结束的
    if (ferror(fp))
    {
        puts("I/O error when reading");
    }
    else if (feof(fp))
    {
        puts("End of file reached successfully");
    }

    fclose(fp);
    fp = NULL;

    return 0;
}

程序运行结果如下:
在这里插入图片描述

  • 二进制文件的例子:
#include<stdio.h>

enum{SIZE = 5};

int main(void)
{
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE* fp;

    if ((fp = fopen("myfile", "wb")) == NULL)
    {
        perror("fopen");
        return 1;
    }

    fwrite(a, sizeof*a, SIZE, fp);      //写double的数组
    fclose(fp);
    fp = NULL;

    double b[SIZE];
    if ((fp = fopen("myfile", "rb")) == NULL)
    {
        perror("fopen");
        return 1;
    }

    size_t ret_code = fread(b, sizeof *b, SIZE, fp);
    if (ret_code == SIZE)
    {
        puts("Array read successfully, contents: ");
        int i = 0;
        for (i = 0; i < SIZE; i++)
            printf("%f ", b[i]);
        putchar('\n');
    }
    else
    {
        if (feof(fp))
            puts("Error reading file: unexpected end of file");
        else if(ferror(fp))
            perror("Error reading file");
    }

    fclose(fp);
    fp = NULL;

    return 0;
}

程序运行结果如下:

在这里插入图片描述


5. 文件缓冲区

文件是指存储在外部存储介质上的、由文件名标识的一组相关信息的集合.由于CPU 与 I/O 设备间速度不匹配.为了缓和 CPU 与 I/O 设备之间速度不匹配矛盾.文件缓冲区是用以暂时存放读写期间的文件数据而在内存区预留的一定空间.使用文件缓冲区可减少读取硬盘的次数.

在这里插入图片描述

所以缓冲区不会实时将数据传送到指定目标,可以使用fflush()函数立即刷新缓冲区,在同一程序进行读和写操作过程之间,可以使用fflush()函数,防止缓冲区数据未传输造成程序出错.

本章完.

评论 13
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值