Makefile

一个简单的例子:

复制代码
ab.out : a.o b.o
    g++ -o ab.out o/a.o o/b.o

a.o : a.cpp
    g++ -c a.cpp -o o/a.o

b.o : b.cpp
    g++ -c b.cpp -o o/b.o

clean:
    rm -f o/a.o o/b.o
复制代码

 

再看下面一个:

复制代码
simple_server_objects = socket/ServerSocket.o socket/Socket.o server.o
simple_client_objects = socket/ClientSocket.o socket/Socket.o client.o

all : server client

server: $(simple_server_objects)
    g++ -o server $(simple_server_objects) -lmysqlpp -lmemcached -lmysqlclient -L/usr/local/lib

client: $(simple_client_objects)
    g++ -o client $(simple_client_objects)

Socket.o: socket/Socket.cpp
    g++ -c socket/Socket.cpp -o socket/Socket.o
ServerSocket.o: socket/ServerSocket.cpp
    g++ -c socket/ServerSocket.cpp -o socket/ServerSocket.o
ClientSocket.o: socket/ClientSocket.cpp
    g++ -c socket/ClientSocket.cpp -o socket/ClientSocket.o
server.o: server.cpp
    g++ -c server.cpp -o server.o -I/usr/local/mysql/include
simple_client_main.o: simple_client_main.cpp
    g++ -c client.cpp  -o client.o

clean:
    rm -f *.o socket/*.o server client
复制代码

 

通用步骤:
编译时,可以不指定头文件,如果指定头文件,其作用是当此头文件变化时,重新生成该规则。(但如果不指定头文件,则当头文件改动时,只能先 clean 再 make了,下面的通用思路也是如此)
编译时,目标文件如果为 dir/%.o ,表示递归查找目录 dir 下的所有 .o 文件,同样的,依赖文件中如果也相应的为 %.cpp ,则表示查找匹配目标文件中的文件名的CPP文件。
编译命令:$(CC) $(CFLAGS) -c $< -o $@  中,$< 表示依赖集,$@ 表示目标集,编译命令一般情况下可以省略。
总结下最简单的通用MAKEFILE思路:
1、查找项目目录下的所有 .cpp 文件
2、将上面找到的变量集后缀名由 .cpp 替换成 .o
3、将第2步中的变量集,逐一加上 debug/ 或 release/ 前缀,可设为 $(OUTPUT_DIR),该步的目标是把所有 .o 文件单独放到一个文件夹中集中管理,避免 .o 文件凌乱分布。如变量为 OUTPUT_OBJS
4、链接:
target : $(OUTPUT_OBJS)
  $(LINK) $(OUTPUT_OBJS) $(CFLAGS) -o target $(LIB_PATH) $(LIBS)
5、编译:
$(OUTPUT_DIR)/%.o : %.cpp
  $(CC) $(CFLAGS) $(INCLUDE_PATH) -c $< -o $@
 
附实例:
 
复制代码
 1 #定义编译选项
 2 CC = g++
 3 LINK = g++
 4 CFLAGS = -Wall
 5 
 6 #定义头文件目录,链接库目录,链接文件
 7 INCLUDE_PATH = -Iinclude
 8 LIB_PATH = -Llib -L/usr/local/lib -Lsrc/lib
 9 LIBS = -lboost_thread -lboost_system -Llib #-ljsoncpp
10 
11 #定义项目代码根目录及所有文件夹目录
12 SRC_DIR = src
13 VPATH =  $(SRC_DIR)
14 VPATH += $(SRC_DIR)/base
15 VPATH += $(SRC_DIR)/data
16 VPATH += $(SRC_DIR)/include
17 VPATH += $(SRC_DIR)/operate
18 VPATH += $(SRC_DIR)/operate/detail
19 
20 #找出所有 .cpp 文件和相应的 .o 文件(带目录)
21 SRC_FILES = $(foreach n, $(VPATH),$(wildcard $(n)/*.cpp))
22 OBJ_FILES = $(SRC_FILES:.cpp=.o)
23 
24 #把所有的 .o 文件放到定义好的输出文件夹中统一管理
25 OUTPUT_DIR := debug
26 OUTPUT_OBJS = $(addprefix $(OUTPUT_DIR)/,$(subst $(SRC_DIR)/, ,$(OBJ_FILES)))
27 
28 #创建存放 .o 文件的目录结构
29 $(shell mkdir -p "$(OUTPUT_DIR)")
30 $(shell mkdir -p "$(OUTPUT_DIR)/base")
31 $(shell mkdir -p "$(OUTPUT_DIR)/data")
32 $(shell mkdir -p "$(OUTPUT_DIR)/operate")
33 $(shell mkdir -p "$(OUTPUT_DIR)/operate/detail")
34 
35 #更新 ctags
36 #$(shell ctags -R --c++-kinds=+p --fields+iaS --extra=+q .)
37 
38 #定义输出目标名
39 TARGET = server
40 
41 #链接
42 $(TARGET) : $(OUTPUT_OBJS)
43     $(LINK) $(OUTPUT_OBJS) -o $@ $(LIB_PATH) $(LIBS)
44 
45 #编译
46 $(OUTPUT_DIR)/%.o : %.cpp
47     $(CC) -c $< -o $@
48 
49 #清除
50 .PHONY:clean
51 clean:
52     -rm -rf $(OUTPUT_DIR)/*
53     -rm -rf $(TARGET)     
复制代码

 

备注:大体的步骤如上,不过需要提前生成存放 .o 文件的目录结构,可以通过 $(shell mkdir -p "$(OUTPUT_DIR)")  等一系列命令来生成,另外需要事先定义好一些变量,如头文件目录,链接库目录,链接库名称等。该通用步骤可以使MAKEFILE写起来很简单,不过感觉效率不高,太多依赖自动查找,另外头文件改动后,也要先 clean 再 make,这一点才是最致命的。待续。

 

GCC头文件的环境变量是 export CPLUS_INCLUDE_PATH=/root/new_s3/src/include

加入到GCC环境变量相比使用 -I 来包含头文件的区别是,在使用 -MM 生成依赖关系时,前者会忽略生成指定路径的头文件的依赖关系。

 

二、自动依赖

复制代码
#定义编译选项
CC = g++
LINK = g++
CFLAGS = -g -Wall -O0

#定义头文件目录,链接库目录,链接文件
#INCLUDE_PATH = -Isrc/include -Isrc/include/mysql 
LIB_PATH = -Lsrc/lib 
LIBS = -lboost_thread -lboost_system -ljsoncpp -lmysqlpp -lmysqlclient

#定义项目代码根目录及所有文件夹目录
SRC_DIR = src/Server
VPATH =  $(SRC_DIR)
VPATH += $(SRC_DIR)/base
VPATH += $(SRC_DIR)/data
VPATH += $(SRC_DIR)/include
VPATH += $(SRC_DIR)/operate

#设置GCC编译时的查找的头文件目录。如果在 -MM 时用-I来指定,那么会生成依赖关系,如BOOST库等,但一般情况下BOOST库是不会修改的。
export CPLUS_INCLUDE_PATH=src/include:src/include/mysql

#找出所有 .cpp 文件和相应的 .o 文件(带目录)
SRC_FILES = $(foreach n, $(VPATH),$(wildcard $(n)/*.cpp))
ALL_FILES = $(foreach n, $(VPATH),$(wildcard $(n)/*.h $(n)/*.cpp))
OBJ_FILES = $(notdir $(SRC_FILES:.cpp=.o))

.PHONY:all
all : depend server

#生成依赖关系文件,这里不用伪目标,这样只有当代码被改动时,依赖关系文件才会被重新生成
#.PHONY:depend
depend:$(ALL_FILES)
    #@export CPLUS_INCLUDE_PATH=src/include:src/include/mysql && 
    $(CC) $(CFLAGS) -MM $(SRC_FILES) > depend 
-include depend

#链接
server : $(OBJ_FILES)
    $(LINK) $(OBJ_FILES) -o $@ $(LIB_PATH) $(LIBS)

#清除
.PHONY:clean
clean:
    -rm -rf $(TARGET)
    -rm -rf *.o 
复制代码

 

该方法缺点:每次都需要生成所有源文件的依赖关系文件,即使是改动一个源文件的情况下。

time make -j4   使用4线程来编译程序

 

通配符可以使用在变量中,如 objects = *.o  ,不过 *.o 并不会展开,Makefile 中的变量,就是C/C++中的宏,如果需要展开的话,即 objects 的值是所有 .o 文件的集合,可以:

objects = $(wildcard *.o)

Makefile 默认只在当前目录下寻找依赖文件和目标文件,如果定义了VPATH变量,则会到变量中所指示的目录中去寻找,VPATH变量的值,应该用空格或冒号分开,(在WINDOWS下是用空格或分号),与之类似的还有一个vpath关键字,它灵活,可以指定在不同的目录里搜索不同类型的文件,使用方法有三种:

vpath <pattern> <dir>    #为模式指定目录

vpath <pattern>       #清除某种模式的搜索目录

vpath           #清除所有模式的搜索目录

<pattern>中使用 % ,来匹配0或若干个字符。如:vpath %.h src/include

 

伪目标也可以做为依赖:

复制代码
.PHONY : cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
  rm program
cleanobj :
  rm *.o
cleandiff :
  rm *.diff
复制代码

 

files = a.o b.o c.i

$(filter %.o,$(files)) : %.o : %.cpp

感觉 $(filter %.o,$(files)) : %.cpp 与 $(file) : %.o : %.cpp 效果一样。

$(files: .o = .d)  #把变量$(files) 中的所有以 .o 结尾(后面是空格或结束符)字符串换成 .d

 

gcc 的 -M 和 -MM 选项,可以生成依赖关系,前者会包括标准库的头文件,后者不带。

 

命令行前如果使用 @ 符号,则这个命令不会被 make 显示出来,如 @echo 正在编译……,会输出“正在编译……”

可以通过 make -n 或 make --just-print 来查看而不执行命令。 make -s 或 make --slient 是全面禁止显示命令。
如果有多条命令,后面的命令是在前面的命令的基础上执行的,比如 cd src;pwd ,则应该把这两条命令写在同一行上,使用分号隔开,而不能写成两行。
在命令前可以加上 - 号,表示命令出错也仍然继续,或者使用 make -i 或 make --ignore-errors,表示忽略所有的错误。
 
《跟我一起写Makefile》书写命令一章中“嵌套执行make” 和 "定义命令包" 两小节在使用到时再看看吧。
顺便提一下,可以使用 info make 来查看更多信息,关于 man 和 info,man,即 manunal,是 UNIX 系统手册的电子版本; Linux 中的大多数软件开发工具都是来自自由软件基金会的 GNU 项目,这些工具软件件的在线文档都以 info 文件的形式存在。info 程序是 GNU 的超文本帮助系统。info 文档一般保存在 /usr/info 目录下,使用 info 命令查看 info 文档。
 
 
Makefile变量的值有四种方式可以定义,一种是使用=,一种是使用 := ,前者可以使用后面定义的值,后者不可以。
描述一个空格的方法:
nullstring :=
space := $(nullstring) #end of the line
注意,如果一个变量指明一个路径: dir = src/include   #end of the line      
但这样其实后面还带有空格,如果使用这个变量去拼接路径的话,会出问题。
第三种方式是使用 ?= ,表示如果该变量之前定义过,则无效;如果没有定义过,则定义。
DIR ?= src/include
等同于:
ifeq($(origin DIR), undefined)
  DIR = src/include
endif
第四种是 += ,用来追加变量值。如果该变量之前没有定义过,会自动变成=
 
关于 override 指示符和使用define关键字产生多行变量,忽略。
 
目标变量:根据不同的目标,使用不同的变量值,相当于局部变量。
如:
server : CXXFLAGS=-g
类似的还有模式变量,如: %.o : CXXFLAGS=-g
 
Makefile 里的条件判断:ifeq(param1,param2)  ... else ... endif
除了 ifeq 外还有 ifneq\ifdef\ifndef
如 ifdef(param) ,param值为非空,则成立。
 
字符串函数:
复制代码
$(subst <from>,<to>,<text>)    #字符串替换,返回处理后的字符串
$(patsubst <from>,<to><text>)      #模式字符串替换,相当于 $(var:%.c=%.o),返回处理后的字符串
$(strip <string>)    #去首尾空格,返回处理后的字符串
$(findstring <find>,<text>)    #查找字符串,找到则返回<find>,否则返回空字符串
$(filter <pattern>,<text>)    #过滤出text中符合模式的字符串,如:$(filter %.c %.cpp,$(source))
$(filter-out <pattern>,<text>)   #和上面相反,返回不符合模式的字串,同样可以有多个模式
$(sort <text>)    #给字符串中的单词排序,注意它会去掉重复的单词
$(word <n>,<text>)     #取出第n个单词
$(wordlist <s>,<e>,<text>)   #取出第s到e个单词,包括第s和第e的单词
$(words <text>)   #统计单词个数
$(firstword <text>)   #返回第一个单词,相当于 $(word 1,<text>)
复制代码

 

文件函数:

复制代码
$(dir <names>)   #返回文件名序列的目录部分
$(nodir <names>)   #返回文件名序列的文件部分
$(suffix <names>)   #返回文件名序列的后缀部分
$(basename <names>)   #返回文件名序列的前缀部分(返回最后一个小数点前的字串)
$(addsuffix <suffix>,<names>)   #为文件名序列中每个单词加后缀
$(addprefix <prefix>,<names>)   #为文件名序列中每个单词加前缀
$(join <list1>,<list2>)   #连接单词,如 $(join aaa bbb,111 222 333) 返回 aaa111 bbb222 333
复制代码

 

$(foreach <var>,<list>,<cmd>)   把<list>中的单词逐一取出来,放到<var>中,然后执行<cmd>,如:

names := a b c dfiles := $(foreach n,$(names),$(n).o)

 
$(if <condition>,<then>)   或 $(if <condition>,<then>,<else>)
 
$(call <expression>,<param1>,<param2>,...)  为表达式传递参数,如:
files = $(2) $(1)
$(call files,a,b)   则返回 b a
 
$(origin  <var>)   判断一个变量的出处,如是否定义过,是否是环境变量等
 
$(shell <shell cmd>)   执行shell命令
 
$(wildcard <pattern-text>)   展开符合模式的字符串,如:SRC_FILES = $(foreach n, $(VPATH),$(wildcard $(n)/*.cpp))

 
自动化变量:
复制代码
$@  目标文件集  
$<   第一个依赖项
$?   所有比目标新的依赖项
$^    所有依赖集合
$+   同上,只是不去重复
另外,加上D或F,分别表示目录部分或文件部分,如 $(@F)
当一个模式匹配包含有斜杠(实际也不经常包含)的文件时,那么在进行模式匹配时,目
录部分会首先被移开,然后进行匹配,成功后,再把目录加回去。在进行""的传递时,我
们需要知道这个步骤。例如有一个模式"e%t",文件"src/eat"匹配于该模式
复制代码

 

生成动态链接库与静态链接库的方法(先生成 *.o 文件,直接通过源文件生成亦可):

静态链接库其实就可以看成是 *.o 文件的简单打包:

ar -crv libtest.a test.o      或: ar -r libtest.a test.cpp     -r选项是必须的,表示插入到备份文件,并且有则替换  -c表示创建备份文件  -v表示显示详情

动态链接库优于静态链接库,无论是在内存使用和磁盘使用上(多进程时),既可以使用源码文件直接生成,也可以使用 *.o 文件生成(*.o 文件的生成必须使用 -fPIC 参数,而从*.o 文件生成动态链接库则不用再重复此参数):

gcc -c -fPIC test.cpp && gcc test.o -o libtest.so -shared

或: gcc test.cpp -o libtest.so -shared -fPIC

-shared 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件
-fPIC 表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。

 

 
预编译头:
1、预编译这个头文件:
g++ -x c++-header -c stdc++.h   -o stdc++.h.gch
使用-x c++-header来说明这个文件作为C++的预编译头文件。注意:C和C++的处理方式不一样,C要使用-x c-header选项。
2、在源文件中使用 #include <bits/stdc++.h>
3、编译器在引入 stdc++.h 的时候,会检查 stdc++.h 的同一目录下是否有 stdc++.h.gch,如果有,再检查GCC的编译选项(及其编译命令行中定义的宏)与预编译头文件时候的参数是否一致。仅仅只在编译参数完全一致(顺序无关)的情况下,预编译的gch文件才会生效。
4、可以在 GCC 的命令行中加入 -Winvalid-pch 参数,当 .gch 文件无效的时候可以给出警告。通过 -H 参数,也可以查看使用了哪个预编译头。

一、命令行参数

-n/--just-print/--dry-run/--recon  只显示命令,但不会执行命令,用于调试 makefile。

-s/--slient/--quiet  禁止命令的显示

-e/--environment-overrides  系统环境变量将覆盖 makefile 中定义的变量

-f/--file  指定 makefile 文件

-t/--touch  只更新目标文件的时间而不编译。

-B/--always-make  重编译所有目标

-C/-directory=<dir>  指定读取 makefile 的目录

--debug=[=<options>]  输出 make 调试信息,如果没有参数则为最简单的调试信息。可以为:

a  all      所有的调试信息,最高

b  basic    基本调试信息,最低

v  verbose  包括哪个 makefile 被解析,不需要被重编译的依赖文件(或是依赖目标)等

i  implicit  输出所有的隐含规则

j  jobs  输出执行规则中命令的详细信息,如命令的PID,返回码等。

m  makefile  输出 make 读取 makefile ,更新 makefile ,执行 makefile 的信息

 

-d  相当于 --debug=a

-i/--ignore-errors  忽略所有错误

-I/--include-dir=<dir>  指定 makefile 的搜索路径

-j/--jobs[=<jobsnum>]  并行执行

-k/--keep-going  出错也不停止运行

-S/--no-keep-going  取消 -k 的作用

-o/--old-file=<file>/--assume-old=<file>  不重新生成指定的<file>

-p/--print-data-base  输出 makefile 中的所有数据,包括所有的规则和变量。如果只想输出信息而不想执行,可以使用 make -qp,想要查看执行 makefile 前的预设变量和规则,使用 make -p -f /dev/null 。此参数对于学习 makefile 极为重要。

-r/--no-builtin-rules  禁止 make 的所有隐含规则

-R/--no-builtin-variabes  禁止 make 使用任何用于变量上的隐含规则

-w/--print-directory  输出运行 makefile 之前和之后的信息,对于跟踪嵌套调用 make 有用。

--no-print-directory  禁止 -w

--warn-undefined-variables  只要make 发现有未定义的变量,就输出警告。

 

常用命令前缀:

@ 不显示命令到屏幕上

;  让下一条命令建立在上一条命令执行结果上,如 cd /opt;pwd

-  忽略命令的错误提示

 

条件判断语句和定义命令序列:

if/ifeq/ifneq/ifdef/ifndef ... else ... endif

define 命令包名  命令列表  endef

 

常用的约定俗成的伪目标名:

all  所有目标的目标,其功能一般是编译所有的目标

clean  删除所有被 make 创建的文件

install  安装已编译好的程序,其实就是把目标执行文件复制到指定的目录中去

print  列举改变过的源文件

tar  把源程序打包备份

dist  创建一个压缩文件,一般是把 tar 文件压成 Z 文件或 gz 文件

TAGS  更新所有的目标,以备完整的重新编译使用

check 和 test  测试 makefile 的流程

 

引入其它的 makefile 文件: include <filename>

1、首先寻找 make 执行时有 -I/--include-dir 参数指定的目录

2、寻找 <prefix>/include (一般是 /usr/local/bin 或 /usr/include ) 目录

如果文件没有找到,则 make 会生成一条警告信息,但不会马上出现致使错误,它会继续载入其它的文件,一旦完成 makefile 的读取,make 会再重试这些没有找到或是不能读取的文件,如果还不行,才会出现一条致命错误,如果想让 make 忽略无法读取的文件,可以在 include 前面加上 - 的命令前缀。

 

依赖文件和目标文件搜索路径:

1、VPATH 环境变量

2、vpath 关键字,它比 VPATH 更灵活,可以指定不同格式的文件在不同的目录下搜索。它的使用有三种形式:

◆  vpath <pattern> <directories>

     为符合模式的文件指定搜索目录

◆  vpath <pattern>

     清除符合模式文件的搜索目录

◆  vpath  

     清除所有已被设置好了的文件搜索目录

 

变量:

makefile 中的变量,更确切的说,类似于是宏,而不是通常意义上的变量。

=/:=/?=/+=   注意这几个变量操作符的区别

override  关键字修饰的变量,可以覆盖运行 makefile 时由命令行设置的变量

设置变量的值时,往往因为空格造成很多问题,这时可以利用 # 完成一些明确声明,如  nullstring :=#结束        space :=$(nullstring) #值为一个空格 

 

自动化变量:

$@  目标文件集

$%  仅当目标是函数库文件时,表示规则中的目标成员名

$<  依赖文件中的第一个目标名字,如果依赖文件是以模式定义的,则表示符合模式的一系列文件集

$?   所有比目标新的依赖文件集,以空格分隔

$^  所有依赖文件集,以空格分隔,如果依赖文件中有重复,则去重复

$+  同上,不去重复

$*  表示目标模式文件的主名部分

$(@D)  表示 $@ 的目录部分(不以斜杠作为结尾)

$(@F)  表示 $@ 的文件部分

其它类似的还有 $(*D)/$(*F)/$(<D)/$(<F)/$(^D)/$(^F)/$(?D)/$(?F)

 

 

 

常用的命令参数变量(默认为空):

CC  C编译器,默认为 gcc

CXX  C++母夜叉器,默认为 g++

CFLAGS  C编译参数

CXXFLAGS  C++编译参数

CPPFLAGS  C预处理器参数

LDFLAGS  链接器参数(如 ld)

 

函数:

一、字符串处理函数

1、$(subst <from>,<to>,<text>)  返回 <text> 中的 <from> 替换成 <to> 后的字符串

2、$(patsubst <pattern>,<replacement>,<text>)  同上,模式替换

3、$(strip <string>)  返回去掉 <string> 字符串开头或结尾的空字符

4、$(findstring <find>,<int>)  在 <in>字串中查找<find>,若找到返回 <find>,找不到返回空字符串

5、$(filter <pattern...>,<text>)  返回符合模式 <pattern> 的字串

6、$(filter-out <pattern...>,<text>)  和上面相反,返回不符合模式 <pattern> 的字串

7、$(sort <list>)  对 <list> 中的字串进行升序去重复排序,返回排序后的字串

8、$(word <n>,<text>  返回 <text> 中的第 <n> 个单词,如果 <n> 大于 <text> 的单词数,则返回空字串

9、$(wordlist <s>,<e>,<text>)  同上,取出多个单词,<s> 和 <e> 是数字

10、$(words <text>)  返回 <text> 中单词个数

11、$(firstword <text>)  返回 <text> 中第一个单词

12、$(join <list1>,<list2>)  将 <list2> 中的子串一一对应的加到 <list1> 的子串后面,如果 <list2> 子串数比 <list1> 多,多出来的 <list2> 子串就直接复制过去,函数返回连接过后的字符串

 

二、文件操作函数

1、$(dir <names...>)  返回文件名序列 <names> 的目录部分

2、$(notdir <names...>)  返回文件名序列 <names> 的非目录部分

3、$(suffix <names...>)  返回文件名序列 <names> 的文件名后缀部分

4、$(basename <names...>)  返回文件名序列 <names> 的文件名前缀部分

5、$(addsuffix <suffix>,<names...>)  返回文件名序列 <names> 加过后缀 <suffix> 的文件名序列

6、$(addprefix <prefix>,<names...>)  返回文件名序列 <names> 加过前缀 <suffix> 的文件名序列

 

三、条件与循环函数

$(if <condition>,<then-part>) 或 $(if <condition>,<then-part>,<else-part>)

$(foreach <var>,<list>,<text>)  将<list>中的单词逐一取出放到<var>所指定的变量中,然后再执行<text>所包含的表达式,每次循环返回的内容由空格隔开,就是该函数的最终返回值。如:

names:=a b c d

files:=$(foreach n,$(names),$(n).o)

则 $(files) 的值就是 "a.o b.o c.o d.o"

 

四、其它函数

$(call <expression>,<parm1>,<parm2>,<parm3>,...)

$(origin <variable>)

$(shell <expression>)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值