7.GDB与文件IO

1.GDB

什么是 GDB 调试

image-20211209081725297

1.1 GDB 准备工作

gdb 是一个 shell 指令,必须带有 -g 的参数,程序才将调试信息添加到文件中

g++ -g a.cpp -o a.out // 先为文件添加调试信息

打开所有的 warning 选项

g++ -g -Wall main.cpp -o main

如果文件与文件之间有引用如何编译:

g++ -g 1.cpp 2.cpp -o main 

开始调试

gdb a.out // gdb + 可执行程序

-g 的作用是在可执行文件中加入源码信息,比如可执行文件中第几条机器指令对应源码的第几行,但并不是把整个源文件都嵌入到可执行文件中,所以在调试时必须保证 gdb 可以找到源文件

给 main 函数设置参数值

set args 10000

image-20211101215845662

(1)查看相关指令

help 命令简写 

(2)继续执行上一条指令

<CR> // 回车操作

(3)查看代码信息

l +number// 默认从 number 行开始显示代码

1.2 GDB 设置断点

image-20211101222332949

常用指令:

b 行号 // 在某一行打断点
i b //查看自己打的断点å

(1)设置断点

b +number 

image-20211209093915141

address 显示的是打断点的地址

(2) 查看断点:

br l
image-20211209094553089

(3)删除断点:

br del +number // number 是断点编号,不是行号
image-20211209094928751

pending 状态的 breakpoint :

pending 的状态的指令都是我用 gdb 指令在 lldb 中运行报的错。pending 状态的信息是被挂起来的,没有被创建的 breakpoint ,这个指令并没有添加到 streamed 中

lldb 中的 pending 状态

(4)设置条件断点:

b +number if i=3 

1.3 GDB 运行程序

进入到 gdb 调试窗口后输入以下指令

image-20211101222917923

常用指令:

(1)从断点处运行一个程序

r or run 

(2)下一步

执行下一行代码,不跳入方法

n or next 

执行下一行diamante,跳入方法

s or step

一直执行,直到下一个断点停下来:

c or continue

(3)跳出

finish

2.文件 IO

2.1 相关定义

2.2.1 Linu

image-20211102101951357

2.2 虚拟地址空间

虚拟地址空间会被 CPU 中的 MMU 内存管理单元映射到真实地址内存中

2.2.1 为什么会有虚拟地址空间

1.内存空间剩余大小无法满足程序大小

2.内存空间不连续导致程序无法按序存放

2.2.2 虚拟地址空间

虚拟内存空间是系统为进程开辟的一段专门用来存地址的空间 ,大小为 0~4G

虚拟地址空间

image-20211102105509384

0~3G 用户区:用户可以自己操作 ,存放用户自己写的程序

受保护地址:用于保存空指针地址 Null ,nullptr

代码段:代码数据,二进制数据

堆:new 出来的对象都是在堆中存储。由低地址向高地址存储

共享库:库文件

栈:用于保存中间变量,由高地址向低地址存储

3G~4G 内核区

不允许用户自己调用,既没有读权限也没有写权限

如果想要调用就要使用“系统调用”的方法,调用系统 API

2.3 文件描述符

定义:文件描述符用于定位文件位置

2.3.1 程序和进程的区别

程序:不占用内存空间,只占用磁盘空间

进程:正在运行的一个程序,真正被内存分配资源的东西,一个进程启动之后会占用虚拟地址空间

2.3.2 如何找到文件

假设现在有一个程序 test.c 他的功能是向 a.txt 文件写东西,那么 test.c 是如何找到 a.txt 的

调用 fopen 函数会返回一个 file 文件指针,这个指针就是指向该进程的文件描述符表。文件描述符表是保存在 PCB 中的,文件描述符是一个数组,这个数组中包含当前进程所控制的所有文件,这个数组的大小为 1024

文件描述符表中前三个值是,标准输入,标准输出,标准错误文件的描述符

image-20211103102230978

上面对应的三个绿色 item 分别是标准输入,标准输出,标准错误。代表我们文件所进行的所有输入输出操作都是基于终端的

2.4 文件操作函数

下面是 Linux 系统中的相关函数

image-20211103115723626

2.4.1 如何查看系统手册

man 2 open // 找到 linux 系统 open 函数说明

image-20211103105344690

image-20211103104129812

2.4.2 如何使用 open 函数

文件保存在 lesson 09 中

1.将用的包进行调用

从上图可以看到,要想使用 open 函数必须先要调用对应的库函数

#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>

2.使用 perror 打印错误信息

在 open 函数当中会有一个 RETURN VALUE 的值,也就是这个函数的返回值。可以看到如果返回 -1 就说明没有找到文件描述符。这里可以使用 perror 打印错误,也就是出现 -1 的时候将具体的错误进行打印

image-20211103120458366

下图是 perror 的使用说明

image-20211103112204860

3.使用 gcc 编译程序

最后可以看到输出 open: 找不到文件的错误

image-20211103111956680

4.相关函数的解释说明

image-20211103112519619

5.完整代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include<unistd.h>
#include <stdio.h>

int main(){
        int fd = open("a.text",O_RDONLY);
        if(fd==-1){
                perror("open"); // 打印错误信息
        }
        close(fd);

        return 0;
}

6.可以传入多个占位符的参数

flags 参数是一个 int 类型的数据,它是占用 4 个字节,是 32 位 ,这个参数是可以传入多个占位符的。第一个是打开模式,第二个是文件的状态。占位符与占位符之间使用 | 隔开

image-20211103114537321

为什么使用 | 分开:

1.一个占位符代表一个 32 位的值,代表文件的读写模式,假设不同的模式由下面的二进制位表示

0_RDWR :

image-20211103114940554

0_CREAT:

image-20211103115051819

2.使用 | 可以将不同的模式进行累加

最后执行的是累加这个结果的指令

累加结果为:

image-20211103115124913
int fd = open("b.text",O_RDWR | O_CREAT,0777);

2.4.3 read 和 write 函数

文件保存在 lesson 10 中

1.函数描述

image-20211103120955841 image-20211103121019847 image-20211103121147529 image-20211103121243807

2.具体书写参数

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <unistd.h>

int main(){
    // 1. 打开一个文件
    int srcfb = open("source.text",O_RDONLY);
    if(srcfb==-1){
        perror("open");
        return -1;
    }
    int tarfb = open("target.text",O_WRONLY|O_CREAT,0664);
    if(tarfb==-1){
        perror("open");
        return -1;
    }
    // 2. 频繁的读写操作
    char buf[1024] = {0}; // 定义一个缓冲区
    int len = 0; // 用于保存读取的字符
    while((len=read(srcfb,buf,sizeof(buf)))>0){
        write(tarfb,buf,len); // 写函数
    }
    // 3. 关闭文件
    close(srcfb);
    close(tarfb);

    return 0;
}

2.4.4 lseek 函数

1.lseek API 解析

这个函数是移动文件开始指针的

image-20211104123707518 image-20211104124032994 image-20211104124054127

**2.Demo **

在当前 .text 文件基础上,将文件下一此开始的指针向后移动 100 个字符

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <unistd.h>
int main(){
  int fd = open("hello.text",O_RDWR);
  if(fd==-1){
    perror("open");
    return -1;
  }
  // 扩展文件长度
  int res = lseek(fd,100,SEEK_END);
  // 写入一个空字节,指针开始在后面的第 100 个指针之后
  write(fd," ",1);
  // 关闭文件
  close(fd);
  if(res==-1){
    perror("lseek") ;
    return -1;
  }
  close(fd);
  return 0;
}

在没有执行 lseek 这个执行文件之前 hello.text 是 65 个字节

image-20211104123435707

在执行这个文件之后变成了 166 个字节

image-20211104123552925

最后文件后面加了很多乱七八糟的东西

image-20211104124413224

2.4.5 stat 函数

1.API

image-20211105104906868

2.使用 linux 命令行调用

image-20211105094152594

3.stat 中传入的结构体类型

在 stat 函数 API 中有说明这个 struct stat 是做什么的:

需要创建一个空的结构体变量,stat 函数会自动对这个结构体变量赋值

image-20211105095703075

4.linux 中文件权限如何定义

文件权限是一个文件的属性,在上图中使用变量 st_mode 进行保存,这个是一个 16 位的存储单元。其中三位是 Other ,Group ,User 对文件的使用权限,特殊权限位不知道是什么,高地址存放文件类型

image-20211105100513972

① Linux 中如何定义文件权限

r—4

w—2

x—1

chmod 文件权限 文件名

Linux 文件权限如何书写

②Linux 如何表示文件类型与文件权限

上图所示,Linux 有 7 中文件类型(最后一个是掩码)

假设是一个套接字文件,他的十进制是 14 ,对应的2进制就是 1100 ,正好放在那四维上

image-20211105101820187

如何输出文件类型,将 st_mode 与 S_IFMT 进行 & 操作,其中 S_IFMT 是一个宏值,进行 & 操作后会得到一个值

image-20211105102452171

然后再对这个宏进行 swith 的判断,然后打印相关文件类型

image-20211105102536768

d,- ,l 都是什么文件类型

d:目录

-: 普通文件

l:连接文件

**5.Demo **

使用 C 语言代码输出查看某个文件的属性

#include<sys/stat.h>
#include<unistd.h>
#include<stdio.h>

int main(){
        struct stat statbuf; // 定义一个机构体变量
        int res  = stat("hello.text",&statbuf);
        if(res==-1){
                perror("stat");
                return -1;
        }
        printf("size:%ld\n",statbuf.st_size);// 调用结构体变量的属性
        return 0;

}

2.4.6 lstat 函数

1.API

image-20211105104944859

2.创建一个软连接

ln -s 软连接文件名 原文件名

可以看到软连接的大小是 9 但是 hello 源文件的大小是 166

image-20211105104448731

2.4.7 access 文件属性–是否有某个权限,是否存在

下面三个函数的文件在 lesson14 中

下图是改文件夹下的文件树,主要是上面文件进行改进

image-20211105141730424

1.API

image-20211105133501590

2.C 代码实现

判断一个文件是否存在

#include<unistd.h>>
#include<stdio.h>
int main(){
    int ret = access("a.txt",F_OK);
    if(ret==-1){
        perror("access");
    }
    printf("文件存在~~");
    return 0;
}

3.效果演示

image-20211105141859896

2.4.8 chmod 文件属性–修改用户权限

1.API

image-20211105134136943

2.C 代码实现

更改文件访问权限

#include<sys/stat.h>
#include<stdio.h>
int main(){
    int ret = chmod("a.txt",0777);
    if(ret==-1){
        perror("chmod");
        return -1;
    }
    return 0;
}

3.代码效果

image-20211105142118916

2.4.9 chmod 文件属性-- 更改文件所属用户和所属组

**1.查看当前系统的用户和组 **

vim /etc/passed

当前用户在后面的位置

image-20211105135056716

用同样的方法可以看到组的信息

vim /etc/group

当前我的用户是在组 id 1000 当中的

image-20211105135407700

创建一个 user 判断这个 user 所在组和组 id

sudo useradd gaogao // 创建高高这个用户
id gaogao // 将 gaogao 这个用户的相关 id 信息进行打印

在下图中分别是用户的id 信息,组 id ,组名称

image-20211105135704569

2.4.10 truncate文件属性-- 缩减或扩展文件尺寸

1.API

最终文件的尺寸会变成参数中所定义的尺寸大小

image-20211105140419472

2.C 代码实现

#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
int main(){
    int ret = truncate("b.txt",20);
    if(ret==-1){
        perror("truncate");
        return -1;
    }
    return 0;
}

3.实现效果

通过上面对文件打印可以看到原先的 b.txt 中有 16 个字符(15+1 \0),下面将其扩展为 20 字符

image-20211105142420092

image-20211105142456300

2.5 Linux 复现 ls 文件属性操作

改文件保存在 lesson12 的 ls.c 中

2.5.1 main 带有参数的main 函数

调用接收两个参数的 main 函数,第一个参数是参数个数,第二个参数是参数列表的值。这样我们就可以接收到用户在命令行输入的参数了

带有参数的 main 函数使用

这里对参数个数进行判断:

因为 main 方法当中默认的一个参数是当前文件的名称,所以如果自己输入一个参数那么这个时候形参中保存的其实是有两个参数的,下面对参数输入个数进行判断

// 判断输入的参数是否正确
    if(argc < 2) {
        printf("%s filename\n", argv[0]);
        return -1;
    }

2.5.2 通过 stat 得到文件的相关信息

下面就要通过 stat 得到文件的相关信息,将这个信息赋值给一个 stat 类型的 struct

struct stat st; // 初始化用于赋值的结构体
    int ret = stat(argv[1], &st);
    if(ret == -1) {
        perror("stat");
        return -1;
    }

2.5.3 获取文件类型

我们得到 16 位的 st_mode 的值后就要进行 & 操作,将里面有用的信息进行解析和保存,将其保存到一个字符数组中

char perms[11] = {0};   // 用于保存文件类型和文件权限的字符串
    switch(st.st_mode & S_IFMT) { // 根据得到的宏值判断需要
        case S_IFLNK:
            perms[0] = 'l';
            break;
        case S_IFDIR:
            perms[0] = 'd';
            break;
        case S_IFREG:
            perms[0] = '-';
            break; 
        case S_IFBLK:
            perms[0] = 'b';
            break; 
        case S_IFCHR:
            perms[0] = 'c';
            break; 
        case S_IFSOCK:
            perms[0] = 's';
            break;
        case S_IFIFO:
            perms[0] = 'p';
            break;
        default:
            perms[0] = '?';
            break;
    }

2.5.4 获取文件权限

// 文件所有者
perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-'; // 这里进行有或者没有的一个判断
perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-';
perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-';

// 文件所在组
perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-';
perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-';
perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-';

// 其他人
perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-';
perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-';
perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-';

2.5.5 得到其他的文件属性

//5.硬连接数
int linkNum = st.st_nlink;

//6.文件所有者
char * fileUser = getpwuid(st.st_uid)->pw_name;

//6.文件所在组
char * fileGrp = getgrgid(st.st_gid)->gr_name;

//7.文件大小
long int fileSize = st.st_size;

//8.获取修改的时间
char * time = ctime(&st.st_mtime);
char mtime[512] = {0};
strncpy(mtime, time, strlen(time) - 1); // 将得到的指针结果赋值给一个 char 类型的数组

2.5.6 将以上的文件属性进行字符串拼接并打印

// 将输出结果进行字符串拼接
char buf[1024];
sprintf(buf, "%s %d %s %s %ld %s %s", perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]);
printf("%s\n", buf);

2.5.7 在 Linux 中使用该命令

1.将 .c 文件编译成可执行文件

gcc ls.c -o ls

2.执行 ls 指令

这里的 ls 指令因为是自己写的所以前面还是要加 ./ ,与此同时将 main 需要的参数拼接到文件名称之后

./ls hello.txt

3.最后的输出结果

上图是自己的输出结果,下图是系统的输出结果

image-20211105125017895

image-20211105125107047

其实,-l 和 hello.txt 应该都是 main 的两个参数

2.6 目录操作函数

目录和文件操作相同,因为目录就是一个个的文件

2.6.1 mkdir–创建目录

1.API

image-20211106094324340
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>

2.代码实现

int main(){
    int res = mkdir("aaa",0777); // 这里前面必须加 0 不加 0 就会调用一个10进制数
    if(res==-1){
        perror("mkdir");
        return -1;
    }
    return 0;
}

易错点:

在定义文件权限时需要在前面加一个 0 ,否则系统会认为是十进制数,导致权限操作失败

3.代码效果

image-20211106105344742

2.6.2 *getcwd , chdir --查看和修改进程目录

Demo 在 lesson14

1.API

image-20211106102025520
// 通用包
#include<stdio.h>
#include<unistd.h>
#include<string.h>
// open 函数相关
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>

2.代码实现

// 得到当前的工作目录
    char buf[128];
    (*getcwd)(buf,sizeof(buf)); // 得到当前进程目录,将其传给 buf 
    print("当前的工作目录是 %s",buf);

    // 更改当前的工作目录
    int res = chdir("/home/xu/C++/lesson13");
    if(res==-1){
        perror("chdir");
        return -1;
    }
    // 在当前修改的目录下创建文件
    int fd = open("lesson14.txt",O_CREAT,O_RDWR,0664);
    if(fd==-1){
        perror("open");
        return -1;
    }
    close(fd);

    // 在此打印当前工作目录
    char buf1[128];
    (*getcwd)(buf1,sizeof(buf1)); // 得到当前进程目录,将其传给 buf 
    print("当前的工作目录是 %s",buf);

3.代码演示

image-20211106110056943

image-20211106110110455

2.6.3 其他目录操作

image-20211106110227103

2.6.4 opendir,readdir,closedir–打开,读,关闭目录

image-20211106110403906

1.API

这里使用 man 3 opendir 查看

image-20211106111305481 image-20211106111104013 image-20211106111417414

其中 DIR 是一个 DIR 类型的成员变量,它有以下属性:

image-20211106114104869
#include<dirent.h>

2.代码

查看某个目录下的所有文件

#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int getFileNum(const char * path);
// 读取某个目录下所有的普通文件的个数
int main(int argc, char * argv[]) {
    if(argc < 2) {
        printf("%s path\n", argv[0]);
        return -1;
    }

    int num = getFileNum(argv[1]);

    printf("普通文件的个数为:%d\n", num);

    return 0;
}
// 用于获取目录下所有普通文件的个数
int getFileNum(const char * path) {

    // 1.打开目录
    DIR * dir = opendir(path);

    if(dir == NULL) {
        perror("opendir");
        exit(0);
    }

    struct dirent *ptr;
    // 记录普通文件的个数
    int total = 0;
    while((ptr = readdir(dir)) != NULL) {

        // 获取名称
        char * dname = ptr->d_name;

        // 忽略掉. 和..
        if(strcmp(dname, ".") == 0 || strcmp(dname, "..") == 0) {
            continue;
        }

        // 判断是否是普通文件还是目录
        if(ptr->d_type == DT_DIR) {
            // 目录,需要继续读取这个目录
            char newpath[256];
            sprintf(newpath, "%s/%s", path, dname);
            total += getFileNum(newpath);
        }

        if(ptr->d_type == DT_REG) {
            // 普通文件
            total++;
        }
    }

    // 关闭目录
    closedir(dir);

    return total;
}

3.代码演示

执行代码,并在代码后面拼接文件目录

image-20211106122852027

4.知识扩展

字符串的拼接操作:

int sprintf (char* buffer, const char* fmt, ...);
char newpath[256];
sprintf(newpath,"%s/%s",path,ptr->d_name);

字符串的比较操作

int strcmp ( const char * str1, const char * str2 );
strcmp(dname, ".");

2.6.5 dup 文件描述符的复制

1.API

image-20211106132253358

2.代码

使用 open 得到文件描述符 fd ,再使用 dup 得到文件描述符 fd1 。对 fd 关闭,然后再对 fd1 文件描述符写入

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(){

    // 使用 open 的方法打开通配符
    int fd = open("a.txt",O_RDWR|O_CREAT,0664);
    // 复制得到的文件描述符
    int fd1 = dup(fd);

    if(fd1==-1){
        perror("dup");
        return -1;
    }
    // 判断 fd 和 fd1 之间是否相等
    printf("fd:%d,fd1:%d\n",fd,fd1);
    close(fd); // 关闭 fd 文件

    char* str = "Hello World";
    int ret = write(fd1,str,strlen(str)); // 判断仅适用赋值的描述符是否可以操作文件
    if(ret==-1){
        perror("write");
        return -1;
    }
    close(fd1);
    return 0;
}

3.代码演示

为什么进行 dup 复制的值和 fd 是不一样的。文件描述符类似于进程的 PCB ,进程 PCB 每打开一个文件就会创建一个文件 PCB ,这里暂且较为句柄,我们通过句柄去控制一个个的文件。每次去控制文件都会创建一个句柄。句柄的位置是从 3 开始的,并且使用 dup 的时候会每次从 pcb 的文件分配表中找到最小的可以存储句柄位置的地方保存这个句柄

所以 fd 和 fd1 的值是不一样的,但是他们都可以指向相同的文件

image-20211106162804020

2.6.6 dup2 文件描述符重定向

1.API

image-20211106140606860

2…代码

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main(){
    // 使用 open 的方法打开通配符
    int fd = open("1.txt",O_RDWR|O_CREAT,0664);
    // 复制得到的文件描述符
    if(fd==-1){
        perror("fd");
        return -1;
    }
    int fd1 = open("2.txt",O_RDWR|O_CREAT,0664);
    // 复制得到的文件描述符
    if(fd1==-1){
        perror("fd1");
        return -1;
    }
    printf("fd:%d,fd1:%d\n",fd,fd1);
    int fd2 = dup2(fd,fd1);
    // 判断 fd 和 fd1 之间是否相等
    printf("fd:%d,fd1:%d,fd2:%d\n",fd,fd1,fd2);

    char* str = "Hello World\n";
    int ret = write(fd1,str,strlen(str)); // 本来 fd1 指向 2.txt ,最后是向 1.txt 写入文件
    if(ret==-1){
        perror("write");
        return -1;
    }
    close(fd);
    close(fd1);
    return 0;
}

3.代码演示

打印每个文件描述符的指向

image-20211108095426278

将文件描述符进行重定位后 fd1 的文件指针指向了 1.txt,所以向 1.txt 写入

image-20211108095253729

2.txt 中则没有数据

image-20211108095501690

2.6.7 fcntl–文件控制函数

有点类似于前面所有函数的统一指令,对应文件 lesson17

**1.API **

在传参的地方有很多 … ,代表是这个函数中可以传入多个参数

image-20211108102006966
#include <unistd.h>
#include <fcntl.h>

阻塞与非阻塞:

描述的是函数调用的行为

阻塞:在进行耗时操作时进程或者现成被挂起,只有在得到结果之后才会返回

函数非阻塞:调用一个函数会立刻得到返回值,有时候会得到想要的结果,有时候不会。但不会阻塞当前进程

就像 Linux 的终端就是阻塞的,因为它在等待我们输入一个指令,但是在执行时又是不阻塞的

2.代码

#include <unistd.h>
#include <fcntl.h>
#include<stdio.h>
#include<string.h>

int main(){
    // 得到一个文件描述符后面对其操作
    int fd = open("1.txt",O_RDWR);
    // 得到文件描述符的状态的操作
    int flag = fcntl(fd,F_GETFL);
    // 在当前状态下添加追加操作,这里使用异或的方式代表追加
    flag|=O_APPEND;
    // 在上面状态基础上再写入一个操作
    int res = fcntl(fd,F_SETFL,flag);

    // 写入
    char* str = "\nnihao\n";
    write(fd,str,strlen(str));

    close(fd);

    return 0;
}

3.代码演示

可以看到原先 1.txt 的内容是 hello world

image-20211108104952177

在对文件进行相应的操作之后内容进行了追加

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值