Linux系统编程入门

开发环境

1、阿里云服务器+xshell+vscode。
2、vscode安装remote插件远程连接服务器。

在cmd输入ssh-keygen -t rsa创建密钥对,在服务器中同样的操作创建密钥对,把本地.ssh文件夹的公钥内容复制到服务器中.ssh文件夹的authorized_keys中去即可完成无需密码访问服务器。

3、服务器端安装g++。yum install -y gcc-c++

静态库

1、准备多个.o目标代码文件。
2、使用ar(archive)进行制作。
ar rcs libxxx.a 1.o 2.o 3.o

xxx为库名
参数:
r:将.o插入到.a库文件中。
c:创建库文件。
s:建立索引。

3、使用时-L指定库路径,-l指定库名称

动态库

1、使用g++ -c -fpic/fPIC 1.cpp 2.cpp得到和位置无关的代码。

-fPIC与-fpic都是在编译时加入的选项,用于生成位置无关的代码(Position-Independent-Code)。这两个选项都是可以使代码在加载到内存时使用相对地址,所有对固定地址的访问都通过全局偏移表(GOT)来实现。-fPIC和-fpic最大的区别在于是否对GOT的大小有限制。-fPIC对GOT表大小无限制,所以如果在不确定的情况下,使用-fPIC是更好的选择。
-fPIE与-fpie是等价的:用来生成位置无关的可执行代码。

2、使用g++ -shared 1.o 2.o -o libxxx.so得到动态库。
3、ldd x命令可查询x可执行程序所依赖的动态库。
4、由动态载入器ld-linux.so去找绝对路径。

顺序为:

  • 编译目标代码时指定的动态库搜索路径;
  • 环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
  • 配置文件/etc/ld.so.conf中指定的动态库搜索路径;配置后要运行 ldconfig命令才能生效
  • 默认的动态库搜索路径/lib;
  • 默认的动态库搜索路径/usr/lib;

第一个是最优先的, 使用gcc -Wl,–rpath -Wl,${LIB_DIR1} ./app 指定了搜索路径。

5、用export x=$x:dir配置环境变量(临时配置)
x为环境变量名称,dir为要配置的目录,配置好后用env查看就好。
6、把5中的代码添加到家目录下的.bashrc最后一行(永久用户级配置)
7、把5中的代码添加到家目录下的/etc/profile最后一行(永久系统级配置)
8、把目录添加到/etc/ld.so.conf中

Makefile

1、概念
一种完成自动化编译的文件,用make指令执行,非常方便。
2、格式

目标文件... : 依赖文件...
	命令...

命令前用制表符tab。
通过命令用依赖文件编译生成目标文件。

例:

main:add.cpp sub.cpp div.cpp mult.cpp main.cpp
	g++ add.cpp sub.cpp div.cpp mult.cpp main.cpp -o main

3、工作原理

执行命令前,如果依赖文件不存在,会向下找是否有生成依赖文件的规则,执行该规则生成依赖文件。
因此,Makefile的规则一般都是为第一条规则服务的。

Makefile具有更新功能:
在执行规则中的命令时,会比较目标和依赖文件的时间,如果依赖时间比目标时间晚,需要重新生成目标。否则不执行命令。

4、变量:

AR:归档维护程序的名称,默认值为ar。
CC:c编译器的名称,默认值为cc。
CXX:c++编译器的名称,默认值为g++。
$@:目标的完整名称
$<:第一个依赖文件的名称。
$^:所有的依赖文件
&(变量名) 获取变量的值

例:

main:main.c a.c b.c
	g++ -c main.c a.c b.c -o main

可以换成

src=main.c a.c b.c
target=main
$(target):$(src)
	$(CXX) -c $^ -o $@

5、模式匹配
Makefile支持模式匹配
%匹配字符串,两个%都匹配相同的字符串。
例:
可以生成.o依赖的规则

%.o:%.c
	$(CXX) -c $< -o $@

6、函数
使用格式:
$(函数名 参数…)

$(wildcard 参数…):获取指定目录下指定类型的文件列表。
例子:
$(wildcard *.c /sub/*.c)
以字符串形式返回当前目录和sub目录下的.c文件。

$(patsubst a,b,c):在c中找到能匹配a的,然后用b替换。
例子:
$(patsubst %.c,%.o, x.c b.c)
返回值:x.o b.o

7、小技巧

.PHONY:
clean:
	rm -rf ...

可以清楚文件,PHONY指出clean是伪目标,不会生成文件。因此就没有“最新的定义”,不适用更新规则,因此每次make claen就会执行该语句。

GDB

由GNU软件系统社区提供的调试工具,同GCC配套组成了一套完整的开发环境。
通常为调试编译时,我们关掉-O编译器优化,打开调试选项-g,并且-Wall打开所有警告。

-g会在可执行文件中加入源代码信息,因此调试的程序一般比中正常可执行程序大,比如可执行文件中的第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件。

例:
g++ -g -Wall a.c -o a
gdb test:调试test可执行文件

1、常用调试命令

基本操作:
quit/q:退出
set args ... :给程序设置参数
show args:显示传入的参数
help:帮助

显示代码:
list/l:显示main程序的源代码
list/l 行号:显示指定行周围代码
list/l 函数名:显示指定函数名周围代码
list/l 文件名:行号:查看指定文件的指定行号周围代码
list/l 文件名:函数名:查看指定文件的函数周围代码
set list/listsize 行数:设置显示代码的行数
show list/listsize:显示显示代码的行数

设置断点:
b/break 行号
b/break 函数名
b/break 文件名:行号
b/break 文件名:函数名

i/info b/break:查看断点
d/del/delete 断点编号 :删除断点
dis/disable 断点编号:设置断点无效
ena/enable 断点编号:使断点生效
b/break 行号 if i==5:设置条件断点(一般在循环的位置)

运行程序:
start:停在第一行
run:遇到断点停止
c/continue:继续运行,停在下一个断点
n/next:向下执行一行代码(不会进入函数体,也就是直接执行完这个函数)
p/print 变量名:打印变量值
ptype 变量名:打印变量类型
s/step:向下单步调试(会进入函数体)
finish:跳出函数体(运行完这个函数)
display num:自动打印指定变量的值
set var 变量名=变量值:设置变量值
until:跳出循环(运行完循环)
display a:把a设置成自动变量,每次向下执行都会打印自动变量
i/info display:查看设置了哪些自动变量
undisplay 编号:删除自动变量

文件IO

1、原理
用户程序调用标准C标准I/O库,C标准I/O库调用内核的write()和read()系统函数。
文件IO
缓冲区只有在装满或者刷新或者关闭文件的时候把数据写入磁盘。

FILE的数据结构为:
文件描述符、文件读写指针、I/O缓冲区(内存地址)。

FILE源码:

typedef struct _IO_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
};

2、虚拟地址空间
虚拟地址空间实现了对物理内存的扩增以及物理内存的管理,类似于物理内存的逻辑地址空间,通过CPU中的MMU单元完成虚拟内存地址向物理内存地址的映射。
3、文件描述符
在系统内核中维护了两个列表一个是系统打开文件列表,一个是进程打开文件列表,列表中的条目在Linux中叫做文件描述符,在windows中叫做文件句柄,进程的文件描述符表也就是一个数组,存放在进程的PCB中,进程的文件描述符指向系统的打开文件列表中的文件描述符,每次进程打开文件时查看系统的打开文件列表,如果有,直接创建文件描述符指向就行,如果没有,系统将文件的FCB调入到内存中的系统开放文件表中,然后在系统打开文件列表中创建条目,最后进程创建文件描述符指向系统的打开列表条目即可。

Linux系统函数

int open(const char  *pathname,int flags);
int open(const char *pathname,int flags,mode_t mode);
int close(int fd);
ssize_t read(int fd,void *buf,size_t count);
ssize_t write(int fd,const void *buf,size_t count);
off_t lseek(int fd,off_t offset,int whence);
int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
int chmod(const char *pathname, mode_t mode);
int truncate(const char *path, off_t length);
int access(const char *pathname, int mode);
int chown(const char *path, uid_t owner, gid_t group);
int rmdir(const char *pathname);
int chdir(const char *path);
char *getcwd(char *buf, size_t size);
int mkdir(const char *pathname, mode_t mode);
int rename(const char *oldpath, const char *newpath);
DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
int dup(int oldfd);
int dup2(int oldfd, int newfd);
int fcntl(int fd, int cmd, ...);
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
参数:
- pathname:路径
- flags:文件权限 值 O_RDONLY(只读), O_WRONLY(只写), or O_RDWR(读写)只能选其中一个
返回值:
- 返回一个文件描述符
- 失败返回-1并设置errno
errno:属于Linux系统函数库,库里面的全局变量,记录了最近调用的错误号。


#include <stdio.h>
void perror(const char *s); 打印errno对应的错误描述
参数s:用户描述,比如说s为hello,最终打印的内容是hello:xxx(实际的错误描述)


#include <unistd.h>
int close(int fd);


int open(const char *pathname, int flags, mode_t mode);
参数:
- pathname:要创建的文件路径
- flags:对文件的操作权限和其他设置,用|分开,因为每一位代表一个标记,按位或获得所有设置。
       必选项:O_RDONLY(只读), O_WRONLY(只写), or O_RDWR(读写)只能选其中一个
       可选项:可选项很多,其中O_CREAT 文件不存在创建新文件
-mode:八进制的数,表示创建出的新的文件的操作权限。最终的权限是mode & ~umask,作用是抹去一些不安全的权限。


#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd:要读取内容到内存中的文件。
- buf:需要读取数据存放的地方,数组的地址。(传出参数)
- count:指出数组的大小
返回值:
- 成功:返回实际读取的字节数
- 失败:返回-1,并设置errno。

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd:文件描述符
- buf:要往磁盘写入的数据
- count:要写的数据的实际大小
返回值:
- 成功:实际写入的字节数
- 失败:返回-1,并设置errno

标准C库的函数
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);


Linux系统函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
- fd:文件描述符,通过open得到的,通过这个fd操作某个文件
- offset:偏移量
- whence:
SEEK_SET
	设置文件指针的偏移量
SEEK_CUR
	设置偏移量:当前位置 + 第二个参数offset的值
SEEK_END
	设置偏移量:文件大小 + 第二个参数offset的值
返回值:返回文件指针的位置
作用:
1.移动文件指针到文件头
lseek(fd, 0, SEEK_SET);
2.获取当前文件指针的位置
lseek(fd, 0, SEEK_CUR);
3.获取文件长度
lseek(fd, 0, SEEK_END);
4.拓展文件的长度,当前文件10b, 110b, 增加了100个字节
lseek(fd, 100, SEEK_END)
注意:需要写一次数据
        
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
作用:获取一个文件相关的一些信息
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
成功:返回0
失败:返回-1 设置errno


int lstat(const char *pathname, struct stat *statbuf);
作用:获取软连接文件的一些信息
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
成功:返回0
失败:返回-1 设置errno
其中的struct statstruct stat
{
    dev_t st_dev;         /* ID of device containing file */
    ino_t st_ino;         /* inode number */
    mode_t st_mode;       /* protection */
    nlink_t st_nlink;     /* number of hard links */
    uid_t st_uid;         /* user ID of owner */
    gid_t st_gid;         /* group ID of owner */
    dev_t st_rdev;        /* device ID (if special file) */
    off_t st_size;        /* total size, in bytes */
    blksize_t st_blksize; /* blocksize for file system I/O */
    blkcnt_t st_blocks;   /* number of 512B blocks allocated */
    time_t st_atime;      /* time of last access */
    time_t st_mtime;      /* time of last modification */
    time_t st_ctime;      /* time of last status change */
};


#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
修改文件的权限
参数:
- pathname: 需要修改的文件的路径
- mode:需要修改的权限值,八进制的数
返回值:成功返回0,失败返回-1


#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
作用:缩减或者扩展文件的尺寸至指定的大小
参数:
- path: 需要修改的文件的路径
- length: 需要最终文件变成的大小
返回值:
成功返回0, 失败返回-1


#include <unistd.h>
int access(const char *pathname, int mode);
作用:判断某个文件是否有某个权限,或者判断文件是否存在
参数:
- pathname: 判断的文件路径
- mode:
    R_OK: 判断是否有读权限
    W_OK: 判断是否有写权限
    X_OK: 判断是否有执行权限
    F_OK: 判断文件是否存在
返回值:成功返回0, 失败返回-1


#include <unistd.h>
int chown(const char *path, uid_t owner, gid_t group);
更改文件的所有者和所在组


#include <unistd.h>
int rmdir(const char *pathname);
移除空目录

#include <unistd.h>
int chdir(const char *path);
作用:修改进程的工作目录
比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder
参数:
- path : 需要修改的工作目录


#include <unistd.h>
char *getcwd(char *buf, size_t size);
作用:获取当前工作目录
参数:
- buf : 存储的路径,指向的是一个数组(传出参数)
- size: 数组的大小
返回值:
返回的指向的一块内存,这个数据就是第一个参数


#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
作用:创建一个目录
参数:
- pathname: 创建的目录的路径
- mode: 权限,八进制的数
返回值:
成功返回0, 失败返回-1


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


#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
参数:
- name:需要打开的目录的名称
返回值:
DIR *类型,理解为目录流
错误返回NULL


// 读取目录中的数据
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- 参数:dirp是opendir返回的结果 
- 返回值: struct dirent,代表读取到的文件的信息
读取到了末尾或者失败了,返回NULL
其中目录项结构为:
struct dirent
{
    ino_t d_ino;             /* inode number */
    off_t d_off;             /* not an offset; see NOTES */
    unsigned short d_reclen; /* length of this record */
    unsigned char d_type;    /* type of file; not supported
                                              by all file system types */
    char d_name[256];        /* filename */
};


// 关闭目录
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);


#include <unistd.h>
int dup(int oldfd);
作用:复制一个新的文件描述符
fd=3, int fd1 = dup(fd),
fd指向的是a.txt, fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符


#include <unistd.h>
int dup2(int oldfd, int newfd);
作用:重定向文件描述符
oldfd 指向 a.txt, newfd 指向 b.txt
调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt
oldfd 必须是一个有效的文件描述符
oldfd和newfd值相同,相当于什么都没有做


#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
参数:
fd : 表示需要操作的文件描述符
cmd : 表示对文件描述符进行如何操作 -
- F_DUPFD : 复制文件描述符,
            复制的是第一个参数fd,得到一个新的文件描述符(返回值) int ret = fcntl(fd, F_DUPFD);
- F_GETFL : 获取指定的文件描述符文件状态flag
            获取的flag和我们通过open函数传递的flag是一个东西。
- F_SETFL : 设置文件描述符文件状态flag
            必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
            可选性:O_APPEND, O_NONBLOCK
                   O_APPEND 表示追加数据
                   O_NONBLOK 设置成非阻塞
        阻塞和非阻塞:描述的是函数调用的行为。

其中stat结构体中的权限变量st_mode结构如下:

st_mode
通过(st_mode&S_IFMT)后获得文件类型字段。

例:通过系统调用完成copy功能,从把1.txt内容复制到2.txt

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
    int fd1 = open("1.txt", O_RDONLY);
    if (!(~fd1))
    {
        perror("1.txt");
        return -1;
    }
    int fd2 = open("2.txt", O_WRONLY | O_CREAT, 0777);
    if (!(~fd2))
    {
        perror("2.txt");
        return -1;
    }
    char buf[1024] = {0};
    int len = 0;
    while ((len = read(fd1, buf, sizeof(buf))) > 0)
        write(fd2, buf, len);
    close(fd2);
    close(fd1);
    return 0;
}

例:扩展文件大小101个字节

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd = open("hello.txt", O_RDWR);
    if (fd == -1)
    {
        perror("hello.txt");
        return -1;
    }
    int res = lseek(fd, 100, SEEK_END);
    if (res == -1)
    {
        perror("lseek");
        return -1;
    }
    write(fd, " ", 1);
    close(fd);
    return 0;
}

例:使用stat获取文件信息

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

int main()
{
    struct stat buf;
    int res = stat("hello.txt", &buf);
    if (res == -1)
    {
        perror("stat");
        return -1;
    }
    printf("%d\n", buf.st_size);
    return 0;
}

例:利用stat函数实现一个ls -l命令

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cstring>
#include <bitset>
#include <pwd.h>
#include <grp.h>
#include <time.h>
using namespace std;
int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        cout << argv[0] << " filename" << endl;
        return -1;
    }

    struct stat res;
    int flag = 0;
    flag = stat(argv[1], &res);
    if (flag == -1)
    {
        perror("stat");
        return -1;
    }
    string s;
    char c;
    switch (res.st_mode & S_IFMT)
    {
    case (S_IFREG):
        c = '-';
        break;
    case (S_IFDIR):
        c = 'd';
        break;
    case (S_IFLNK):
        c = 'l';
        break;
    case (S_IFSOCK):
        c = 's';
        break;
    case (S_IFBLK):
        c = 'b';
        break;
    case (S_IFCHR):
        c = 'c';
        break;
    case (S_IFIFO):
        c = 'p';
        break;
    default:
        c = '?';
        break;
    }
    s += c;
    s += res.st_mode & S_IRUSR ? 'r' : '-';
    s += res.st_mode & S_IWUSR ? 'w' : '-';
    s += res.st_mode & S_IXUSR ? 'x' : '-';

    s += res.st_mode & S_IRGRP ? 'r' : '-';
    s += res.st_mode & S_IWGRP ? 'w' : '-';
    s += res.st_mode & S_IXGRP ? 'x' : '-';

    s += res.st_mode & S_IROTH ? 'r' : '-';
    s += res.st_mode & S_IWOTH ? 'w' : '-';
    s += res.st_mode & S_IXOTH ? 'x' : '-';
    s += ' ';
    s += to_string(res.st_nlink);
    s += ' ';
    s += getpwuid(res.st_uid)->pw_name;
    s += ' ';
    s += getgrgid(res.st_uid)->gr_name;
    s += ' ';
    s += ctime(&res.st_mtime);

    s.erase(s.end() - 1);
    s += ' ';
    s += argv[1];
    cout << s << endl;
    return 0;
}
//-rwxr-xr-x 1 root root 8968 Aug 13 16:43 test

例:递归求得目标目录下的普通文件个数

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <iostream>
#include <dirent.h>
#include <string.h>
#include <stdio.h>
#include <string>
using namespace std;
int caldirnum(const char *path)
{
    DIR *dir = opendir(path);
    if (dir == NULL)
    {
        perror("open");
        return -1;
    }
    int sum = 0;

    struct dirent *ptr;

    while ((ptr = readdir(dir)) != NULL)
    {
        char *s = ptr->d_name;
        if (!strcmp(s, ".") || !strcmp(s, ".."))
            continue;
        if (ptr->d_type == DT_DIR)
        {
            char newpath[256];
            sprintf(newpath, "%s/%s", path, s);
            sum += caldirnum(newpath);
        }
        else if (ptr->d_type == DT_REG)
            sum++;
    }

    closedir(dir);
    return sum;
}
int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        cout << argv[0] << " dirname" << endl;
        return -1;
    }

    int res = caldirnum(argv[1]);
    cout << res << endl;
    return 0;
}

例:使用fcntl更改文件打开标志位

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main()
{

    // 1.复制文件描述符
    // int fd = open("1.txt", O_RDONLY);
    // int ret = fcntl(fd, F_DUPFD);

    // 2.修改或者获取文件状态flag
    int fd = open("1.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        return -1;
    }

    // 获取文件描述符状态flag
    int flag = fcntl(fd, F_GETFL);
    if (flag == -1)
    {
        perror("fcntl");
        return -1;
    }
    flag |= O_APPEND; // flag = flag | O_APPEND

    // 修改文件描述符状态的flag,给flag加入O_APPEND这个标记
    int ret = fcntl(fd, F_SETFL, flag);
    if (ret == -1)
    {
        perror("fcntl");
        return -1;
    }

    char *str = "nihao";
    write(fd, str, strlen(str));

    close(fd);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值