linux学习:Makefile学习

目录

makefile调用方式

gcc编译多文件例子

变量详解

特点

例子   自定义变量

例子 系统预定义变量

例子 自动化变量

例子 定义变量的不同形式

例子 变量操作方式

例子 导出变量

例子 特殊变量

VPATH

MAKE

MAKEFLAGS

规则

隐式规则

弊端

静态规则

多目标规则

双冒号规则

作用

例子

条件判断

函数

Makefile 中常用到的内嵌函数的详细信息


自动进行编译的 软件,GNU make(工程管理器 make 在不同环境有很多版本分支,比如 Qt 下的 qmake, Windows 下的 nmake 等,下面提到的 make 指的是 Linux 下的 GNU make)就是这样 的一款软件

而 Makefile,是 make 的配置文件,用来配置运行 make 的时候的一些相关细节,

比如指定编译选项,指定编译环境等等。一般而言,一个工程项目不管是简单还是复杂,每一个源代码子目录都会有一个 Makefile 来管理,然后一般有个所谓的顶层 Makefile 来统一管理所有的子目录 Makefile。

makefile调用方式

make

gcc编译多文件例子

假设我们有一个工程,这个工程总共有4个源文件,姑且叫做 a.c、b.c 以及 x.c 和 y.c 吧,他们最终将会链接生成可执行文件 image:

用来帮我们“自动”执 行编译的工作,此时目标是 image,而其依赖则是四个.o 文件,而 且,这四个.o 文件本身也是目标,他们依赖于其对应的.c 文件

image:a.o b.o x.o y.o
    gcc a.o b.o x.o y.o -o image
a.o:a.c
    gcc a.c -o a.o -c
b.o:b.c
    gcc b.c -o b.o -c
x.o:x.c
    gcc x.c -o x.o -c
y.o:y.c
    gcc y.c -o y.o -c

这个简单的 Makefile 文件总共有 11 行,5 套规则,其中第 1 行中的 image 是第 1 个目标,冒号后面是这个目标的依赖列表(四个.o 可重定位文件)。第 2 行行首是一个制 表符,后面紧跟着一句 Shell 命令。 下面从第 4 行到第 11 行,也都是这样的目标-依赖对,及其相关的 Shell 命令。但是 这里必须注意一点:虽然这个 Makefile 总共出现了 5 个目标,但是第一个规则的目标(即 image)被称之为终极目标,终极目标指的是当你执行 make 的时候,默认生成的那个文 件。注意:如果第一个规则有多个目标,则只有第一个才是终极目标。另外,以圆点.开头 的目标不在此讨论范围内。

变量详解

跟Shell脚本非常类似,在Makefile中也会使用“弱类型”变量(相对于C语言这种强 类型语言而言),在Makefile中变量就是一个名字(像是C语言中的宏),代表一个文本字 符串(变量的值)。在Makefile的目标、依赖、命令中引用一个变量的地方,变量会被它 的值所取代(与C语言中宏引用的方式相同,因此其他版本的make也把变量称之为“宏”)。

特点

  1. 变量和函数的展开(除规则的命令行以外),是在make读取Makefile文件时进行 的,这里的变量包括了使用“=”定义和使用指示符“define”定义的变量。
  2. 变量可以用来代表一个文件名列表、编译选项列表、程序运行的选项参数列表、搜 索源文件的目录列表、编译输出的目录列表和所有我们能够想到的事物。
  3. 变量名不能包括“:”、“#”、“=”、前置空白和尾空白的任何字符串。需要注意的是, 尽管在GNU make中没有对变量的命名有其它的限制,但定义一个包含除字母、数字和下 划线以外的变量的做法也是不可取的,因为除字母、数字和下划线以外的其它字符可能会在 以后的make版本中被赋予特殊含义,并且这样命名的变量对于一些Shell来说不能作为环 境变量使用。
  4. 变量名是大小写敏感的。变量“foo”、“Foo”和“FOO”指的是三个不同的变量。 Makefile传统做法是变量名是全采用大写的方式。推荐的做法是在对于内部定义的一般变 量(例如:目标文件列表objects)使用小写方式,而对于一些参数列表(例如:编译选项 CFLAGS)采用大写方式,这并不是要求的。但需要强调一点:对于一个工程,所有Makefile 中的变量命名应保持一种风格,否则会显得你是一个蹩脚的开发者(就像代码的变量命名风 格一样),随时有被鄙视的危险。
  5. 另外有一些变量名只包含了一个或者很少的几个特殊的字符(符号)。称它们为自 动化变量。像“<”、“@”、“?”、“*”、“@D”、“%F”、“^D”等等,后面会详述之。
  6. 变量的引用跟Shell脚本类似,使用美元符号和圆括号,比如有个变量叫A,那么对 他的引用则是$(A),有个自动化变量叫@,则对他的引用是$(@),有个系统变量是CC则 对其引用的格式是$(CC)。对于前面两个变量而言,他们都是单字符变量,因此对他们引用 的括号可以省略,写成$A和$@。

例子   自定义变量

A = apple
B = I love China
C = $(A) tree

继续上方编译多个.c

OBJ = a.o b.o x.o y.o
image:$(OBJ)
    gcc $(OBJ) -o image
a.o:a.c
    gcc a.c -o a.o -c
b.o:b.c
    gcc b.c -o b.o -c
x.o:x.c
    gcc x.c -o x.o -c
y.o:y.c
    gcc y.c -o y.o -c

例子 系统预定义变量

CFLAGS、CC、MAKE、Shell 等等,这些变量已经有了系统预定义好的值,当然我们 可以根据需要重新给他们赋值,例如 CC 的默认值是 gcc,当我们需要使用 c 编译器的时候 可以直接使用他

OBJ = a.o b.o x.o y.o
image:$(OBJ)
    $(CC) $(OBJ) -o image
a.o:a.c
    $(CC) a.c -o a.o -c
b.o:b.c
    $(CC) b.c -o b.o -c
x.o:x.c
    $(CC) x.c -o x.o -c
y.o:y.c
    $(CC) y.c -o y.o -c

如果是要编译arm,只需修改CC

CC = arm-Linux-gnu-gcc

例子 自动化变量

<、@、?、#等等,这些特殊的变量之所以称为自动化变量,是因为他们的值会“自 动地”发生变化

上述列出的自动量变量中。其中有四个在规则中代表一个文件名(@、<、%和*)。 而其它三个的在规则中代表一个文件名的列表

通过这七个自动化变量来获取一个完整文件名中的目录部分或 者具体文件名,需要在这些变量中加入“D”或者“F”字符。这样就形成了一系列变种的自动 化变量

将上面的多文件编译修改成自动化

OBJ = a.o b.o x.o y.o
image:$(OBJ)
    $(CC) $^ -o $@
a.o:a.c
    $(CC) $^ -o $@ -c
b.o:b.c
    $(CC) $^ -o $@ -c
x.o:x.c
    $(CC) $^ -o $@ -c
y.o:y.c
    $(CC) $^ -o $@ -c

例子 定义变量的不同形式

  1. 递归定义方式
    A = I love $(B)
    B = China

    此处,在变量 B 出现之前,变量 A 的定义包含了对变量 B 的引用,由于 A 的定义方式 是所谓的“递归”定义方式,因此当出现$(B)时会对全文件进行搜索,找到 B 的值并代进 A 中,结果变量 A 的值是“I love China“  ,缺点如下

    1. 使用此风格的变量定义,可能会由于出现变量的递 归定义而导致make陷入到无限的变量展开过程中,最终使make执行失败。例如A=$(A), 这将导致无限嵌套迭代。

    2. 这种风格变量的定义中如果使引用了某一个函数,那么函数 总会在其被引用的地方被执行。是因为这种风格变量的定义中,对函数引用的替换展开发生在展开它自身的时候,而不是在定义它的时候。这样所带来的问题是,可能可能会使make 的执行效率降低,同时对某些变量和函数的引用出现问题。特别是当变量定义中引用了 “Shell”和“wildcard”函数的情况,可能出现不可控制或者难以预料的错误,因为我们无法 确定它在何时会被展开

  2. 直接定义方式
    B = China
    A := I love $(B

    定义 A 时用的是所谓的“直接”定义方式,说白了就是如果其定义里出现有对 其他变量的引用的话,只会其前面的语句进行搜寻(不包含自己所在的那一行),而不是搜 寻整个文件

  3. 条件定义方式
    A = apple
    A ?= I love China

    有时我们需要先判断一个变量是否已经定义了,如果已经定义了则不作操作,如果没有 定义再来定义它的值,此处对 A 进行了两次定义,其中第二次是条件定义,其含义是:如果 A 在此之前没有 定义,则定义为“I love China”,否则维持原有的值

  4. 多行命令定义方式
    define commands
        echo “thank you!” 
        echo “you are welcome.” 
    endef

    此处定义了一个包含多行命令的变量commands,我们利用它的这个特点实现一个完 整命令包的定义。注意其语法格式:以define开头,以endef结束,所要定义的变量名必须 在指示符“define”的同一行之后,指示符define所在行的下一行开始一直到“end”所在行的 上一行之间的若干行,是变量的值。这种方式定义的所谓命令包,可以理解为编程语言中的 函数

例子 变量操作方式

  1. 追加变量的值
    A = apple
    A += tree
    这样,变量A的值就是apple tree
  2. 修改变量的值
    A = srt.c string.c tcl.c
    B = $(A:%.c=%.o
    等同于
    A = srt.c string.c tcl.c
    B = $(patsubst %.c, %.o, $(A))

    变量B的值就变成了 srt.o string.o tcl.o。例子中$(A:%.c=%.o)的意思是: 将变量A中所有以.c作为后缀的单词,替换为以.o作为后缀

  3. override一个变量
    override CFLAGS += -Wal

    在执行make时,通常可以在命令行中携带一个变量的定义,如果这个变量跟Makefile 中出现的某一变量重名,那么命令行变量的定义将会覆盖Makefile中的变量。就是说,对 于一个在Makefile中使用常规方式(使用“=”、“:=”或者“define”)定义的变量,我们可以 在执行make时通过命令行方式重新指定这个变量的值,命令行指定的值将替代出现在 Makefile中此变量的值

    A = an apple tree
    all:
        @echo $(A)
    
    运行
    make A="an elephant" 
    会出现结果
    an elephant
    
    如果不想被覆盖就要在A前面加override
    override A = an apple tree

    指示符“override”并不是用来防止Makefile的内部变量被命令行参数覆 盖的,其存在的目的是为了使用户可以改变或者追加那些使用make的命令行指定的变量的 定义。从另外一个角度来说,就是实现了在Makefile中增加或者修改命令行参数的一种机 制。想象一下,我们可能会有这样的需求:对一些通用的参数或者必需的编译参数我们可以 在Makefile中指定,而在命令行中可以指定一些特殊的参数。对待这种需求,我们可以使 用指示符“override”来实现

例子 导出变量

在Makefile中导出一个变量的作用是:使得该变量可以传递给子Makefile。在缺省的 情况下,除了变量”Shell”、”MAKEFLAGS”、不为空的”MAKEFILES”以及在执行make之 前就已经存在的环境变量之外,其他变量不会被传递给子Makefile

  • /----dir/
  • /      |------Makefile
  • /----Makefile

第一层Makefile

export A = apple
B = banana
all:
    echo "rank 1: $(A)"
    echo "rank 1: $(B)"
    $(MAKE) -C dir/

第二层Makefile

all:
    echo "rank 2: $(A)"
    echo "rank 2: $(B)"

运行make -sw   显示目录,会出现结果如下

rank 1: apple
rank 1: banana
rank 2: apple
rank 2: 

也可以防止传递

unexport MAKEFLAGS

这样,上一级Makefile的命令行参数就不会传递给子Makefile了。

例子 特殊变量

VPATH

这个特殊的变量用以指定Makefile中文件的备用搜寻路径:当Makefile中的目标文件 或者依赖文件不在当前路径时,make会在此变量所指定的目录中搜寻,如果VPATH包含多 个备用路径,他们使用空格或者冒号隔开

VPATH = dir1/:dir2/

可以使用小写的指示符vpath来更灵活地为各种不同的文件指定不同的路 径

vpath %.c = dir1/:dir2/
vpath %.h = include/

VPATH是一个变量,而vpath是一个指示符

MAKE

当需要在一个Makefile中调用子Makefile时,用到的变量就是MAKE,实际上该变量 代表了当前系统中make软件的全路径

MAKEFLAGS

在执行make时的命令行参数,这个变量是缺省会被传递给子Makefile 的特殊变量之一

如果运行 make -s  ,则MAKEFLAGS就等于s

规则

隐式规则

上面用来管理四个源程序文件(a.c b.c x.c 和 y.c)的那个 Makefile 还是显得比较笨 拙,需要对每一个文件编写一个规则,但其实 Makefile 是有一定的智能的,我们可以将编 译语句省略掉,也可以将依赖文件都省略掉,甚至连目标都省略掉!

OBJ = a.o b.o x.o y.o
image:$(OBJ)
    $(CC) $(OBJ) -o image

运行make就会运行

cc -c -o a.o a.c
cc -c -o b.o b.c
cc -c -o x.o x.c
cc -c -o y.o y.c
gcc a.o b.o x.o y.o -o image

虽然后四个规则的目标、依赖文件和编译语句都没写,但是执行 make 也 照样可以运行,可见 make 会自动帮我们找到.o 文件所需要的源程序文件,也能自动帮我 们生成对应的编译语句,这个情况称之为 Makefile 的隐式规则

虽然我们可以省略后四个规则的依赖文件和编译语句,但是第一个规则的 依赖文件和编译语句不能省略,因为隐式规则是有限制的,他只能自动找到跟目标同名的依 赖文件,比如目标叫 a.o,那么他会自动查找到 a.c,换了个名字就找不到了,生成的编译 语句也是缺省的单文件形式

弊端

  • 第一,有时一个目标可能并不是一个文 件,而仅仅是一个动作,这时就不应该在其身上运用隐式规则。
  • 第二,使用隐式规则不能让 我们更好地控制编译语句,比如我在编译的时候想要链接某个指定的库文件,或者添加某些 指定的编译选项,此时隐式规则就显得笨拙

针对第一点,有时我们需要明确地告诉 Makefile 不要对某个目标运用隐式规则,比如 我们每次想要清理工程项目中所有的目标文件,可以将清理工作交给 Makefile 来完成:

OBJ = a.o b.o x.o y.o

image:$(OBJ)
    $(CC) $(OBJ) -o image

clean:
    $(RM) $(OBJ) image

.PHONY: clean

第 6、7 行声明了一个清理目标文件和 image 的规则,执行这条 Shell 命令时需要指 定 make 的参数 clean,clean 是一个动作的代号,而不是一个我们要生成的文件,但是根 据隐式规则,假如当前目录恰巧有个文件叫做 clean.c,就可能会导致 Makefile 自动生成 其对应的编译语句,从而引起混淆。在第 9 行中用指示符.PHONY 来明确地告诉 Makefile 不要对 clean 运用任何隐式规则,事实上,不能运用隐式规则的目标被称为伪目标

静态规则

针对上述第二点(使用隐式规则不能让我们更好地控制编译语句),我们也许在编译.o 文件的时候需要一些特殊的编译选项,不能完全将他们弃之不管,但是又不想对每一个.o文件写一个规则,那就可以使用静态规则

OBJ = a.o b.o x.o y.o

image:$(OBJ)
$(CC) $(OBJ) -o image

$(OBJ):%.o:%.c
$(CC) $^ -o $@ -Wall -c

clean:
$(RM) $(OBJ) image

.PHONY: clea

第 6、7 行运用了所谓的静态规则,其工作原理是:$(OBJ)被称为原始列表,即(a.o b.o x.o y.o),紧跟在其后的%.o 被称为匹配模式,含义是在原始列表中按照这种指定的 模式挑选出能匹配得上的单词(在本例中要找出原始列表里所有以.o 为后缀的文件)作为 规则的目标

多目标规则

针对我们要生成的三个.o 文件,也可以使用 所谓的多目标规则,具体(其中函数$(subst)的用法和功能请参阅下面有关“函数”的小节

SRC = $(wildcard *.c)
OBJ = $(SRC:%.c=%.o)

image:$(OBJ)
$(CC) $(OBJ) -o image -lgcc

$(OBJ):$(SRC)
$(CC) $(subst .o,.c,$@) -o $@ -c

clean:
$(RM) $(OBJ) image

.PHONY:clean

着重看第 7、8 行,展开后是:

a.o b.o x.o y.o:a.c b.c x.c y.c
    $(CC) $(subst .o,.c,$@) -o $@ -c

当中的四个.o文件都是这个规则的目标,规则所定义的命令对所有的目标有效。这个具 有多目标的规则相当于多个规则,规则中命令对不同的目标的执行效果不同,因为在规则的 命令中可能使用自动环变量”$@”,多目标规则意味着所有的目标具有相同的依赖文件。

比如,当目标是 a.o 时,多目标规则将自动构建如下针对 a.o 的规则:

a.o:a.c b.c x.c y.c

        $(CC) $(subst .o,.c,$@) -o $@ -c

即:

a.o:a.c b.c x.c y.c

        $(CC) a.c -o a.o -c

可以看到,在这个范例中使用多目标规则是比较笨拙的,因为他把所有的源文件都当成 是 a.o 的依赖文件了,因为多目标规则不能根据目标来自动改变依赖文件,要做到这一点 可以使用上面的静态规则。

多目标规则应用在以下两种场合

  • 只需描述依赖关系,而不需要指定相关 Shell 命令。比如当前的所有的目标文件都 依赖于一个叫 head.h 的头文件,可以用多目标规则来表达,这样只要 head.h 有改动,四个目标文件都将会被重新编译
  • 有多个具有类似构建命令的目标,就是一个.o有好多个.c编译

双冒号规则

双冒号规则就是使用“::”代替普通规则的“:”得到的规则

当同一个文件作为多个规则 的目标时,双冒号规则的处理和普通规则的处理过程完全不同(双冒号规则允许在多个规则 中为同一个目标指定不同的重建目标的命令)

首先需要明确的是:Makefile中,一个目标可以出现在多个规则中。但是这些规则必 须是同一种规则,要么都是普通规则,要么都是双冒号规则。而不允许一个目标同时出现在 两种不同的规则中。

作用

  • 当依赖列表为空时,即使目标文件已经存在,双冒号规则能确保规则中的Shell命 令也会被无条件执行
  • 当同一个文件作为多个双冒号规则的目标时。这些不同的规则会被独立的处理, 而不是像普通规则那样合并所有的依赖到一个目标文件。这就意味着对这些规则的处理就像 多个不同的普通规则一样。就是说多个双冒号规则中的每一个的依赖文件被改变之后, make只执行此规则定义的命令,而其它的以这个文件作为目标的双冒号规则将不会被执 行

例子

现在有a.c b.c libx.so liby.so Makefile这几个文件

image::b.c
$(CC) a.c -o $@ -L. -lx

image::b.c
$(CC) b.c -o $@ -L. -l
运行make后
cc a.c -o image
cc b.c -o image

不管a.c或者b.c都生成image文件,如果“a.c”文件被修改,执行make以后将根 据“a.c”文件重建目标“image”。而如果“b.c”被修改那么“image”将根据“b.c”被重建。

条件判断

之前提到,我们的工程文件可能在 PC 端编译,也可能在 ARM 平台运行,不同的编译 环境需要使用不同的工具链,我们可以通过手工改动 Makefile 的方式来达到更改编译器的 目的,也可以使用条件判断机制让 Makefile 自动处理

OBJ = a.o b.o x.o y.o

ifdef TOOLCHAIN # ifdef 语句用来判断变量 TOOLCHAIN 是否有定义
CC = $(TOOLCHAIN)
else
CC = gcc
endif

image:$(OBJ)
$(CC) $(OBJ) -o image

$(OBJ):%.o:%.c
$(CC) $^ -o $@ -c

clean:
$(RM) $(OBJ) image

.PHONY:clea
运行make TOOLCHAIN=arm-Linux-gnu-gcc
arm-Linux-gnu-gcc a.c -o a.o -c
arm-Linux-gnu-gcc b.c -o b.o -c
arm-Linux-gnu-gcc x.c -o x.o -c
arm-Linux-gnu-gcc y.c -o y.o -c
arm-Linux-gnu-gcc a.o b.o x.o y.o -o image

执行 make 时指定了参数 TOOLCHAIN=arm-Linux-gcc, 那第 3 行的 ifdef 语句将成立,因此编译器 CC 被调整为用户指定的 TOOLCHAIN。在这个 例子中我们同时也看到了如何在命令行中给 make 传递参数。

假如在用户使用 gcc 编译时需要链接库文件 libgcc.so,而在使用交叉工具 链 arm-Linux-gnu-gcc 时不需要,那么我们的 Makefile 需要再改成

OBJ = a.o b.o x.o y.o

ifdef TOOLCHAIN
CC = $(TOOLCHAIN)
else
CC = gcc
endif

image:$(OBJ)
ifeq ($(CC), gcc) # ifeq ( )用来判断变量 CC 的值是否等于 gcc
$(CC) $(OBJ) -o image -lgcc
else
$(CC) $(OBJ) -o image
endif

$(OBJ):%.o:%.c
$(CC) $^ -o $@ -c

clean:
$(RM) $(OBJ) image

.PHONY:clea

在第 10 行中,使用 ifeq ( )来对 CC 进行了判断,注意:ifeq 跟后面的圆括号之间有 一个空格!ifeq ( )也可以用来判断一个变量是否为空

ifeq ($(A),)
    echo “$(A) is empty” 
endif

函数

怎样让 Makefile 知道我们的工程来了一个新的文件 c.c 呢?这需要一个叫 wildcard 的函数帮忙

SRC = $(wildcard *.c)

注意到在 Makefile 中书写一个函数的格式:$(function arg1,arg2,arg3, ... ...) 其 中 function 是函数的名字,后面跟一个空格,然后是参数列表,如果有多个参数则用逗号 隔开(注意逗号后面最好不要有空格),整个函数用$( )包裹起来(跟变量一样)

由于 wildcard 函数的作用就是找到参数匹配的文件名,因此该语句的作用就相当于

SRC = a.c b.c c.c x.c y.c

有了源程序文件名字列表,通过变量的替换操作,很容易就可以得到.o 文件列表

OBJ = $(SRC: %.c=%.o)

于是又可以更简便了

SRC = $(wildcard *.c)
OBJ = $(SRC:%.c=%.o)

ifdef TOOLCHAIN
CC = $(TOOLCHAIN)
else
CC = gcc
endif

image:$(OBJ)
ifeq ($(CC),gcc)
$(CC) $(OBJ) -o image -lgcc
else
$(CC) $(OBJ) -o image
endif

$(OBJ):%.o:%.c
$(CC) $^ -o $@ -c

clean:
$(RM) $(OBJ) image

.PHONY:clea

Makefile 中常用到的内嵌函数的详细信息

第一类  文本处理函数。此类函数专门用于处理文本(字符串)

1. 将字符串 TEXT 中的字符 FROM 替换为 TO

$(subst FROM,TO,TEXT)

A = $(subst pp,PP,apple tree)
替换之后变量 A 的值是”aPPle tree”

返回: 替换之后的新字符串

2. 按 照 PATTERN 搜 索 TEXT 中 所 有 以 空 格 隔 开 的 单 词 , 并 将 它 们 替 换 为 REPLACEMENT

$(patsubst PATTERN,REPLACEMENT,TEXT)

范例:
A = $(patsubst %.c,%.o,a.c b.c)
替换之后变量 A 的值是”a.o b.o”

返回: 替换之后的新字符串。

注意:参数 PATTERN 可以使用模式通配符%来代表一个单词中的 若干字符,如果此时 REPLACEMENT 中也出现%,那么 REPLACEMENT 中的%跟 PATTERN 中的%是一样的。

3. 去掉字符串中开头和结尾的多余的空白符(掐头去尾),并将其中连续的多个空白 符合并为一个。

$(strip STRING)

范例:
A = $(strip apple tree )
处理之后,变量 A的值是”apple tree"

返回: 去掉多余空白符之后的新字符串。

注意:所谓的空白符指的是空格、制表符。

4. 在给定的字符串 STRING 中查找 FIND 子串

$(findstring FIND, STRING)

范例:
A = $(findstring pp, apple tree)
B = $(findstring xx, apple tree)
变量 A 的值是”pp”,变量 B 的值是空。

返回: 找到则返回 FIND,否额返回空

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值