Makefile的理论和实践的学习记录

在这里插入图片描述

1:Makefile 的变量的四种基本赋值方式:

简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。

2:自动化变量说明:

在这里插入图片描述

3:语法:include 文件名

作用:将其它makefile文件包含进来,组成一个更大的makefile文件,这样有利于makefile模块化编程。通常我们将一些配置选项分开成一个独立的makefile文件,这样有利于makefile文件的管理,或将模块代码的依赖关系和需要编译的文件信息独自写到一个 makefile文件中,最终通过include命令形成一个顶层makefile文件来完成整个工程代码的编译和链接。

·

4:变量定义:

语法:变量名 := 变量值

在makefile中,经常先定义一个变量,然后往该变量中追加新的值(通过+=符号),比如先定义一个C_SRCS变量(该值可以为空),然后将代码文件test1.c和test2.c添加到C_SRCS中,其代码如下所示:

C_SRCS :=

C_SRCS += test1.c test2.c

5:内置命令:

Makefile中内置了一些常用的命令,有字符串处理函数subst、patsubst、strip、findstring、filter、filter-out、sort、word、wordlist、words、firstword、lastword;文件名处理函数dir、notdir、suffix、basename、addsuffix、addprefix、join、wildcard、realpath、abspath;条件处理函数if;循环处理函数foreach等。下面介绍一些常用的函数:

wildcard 函数:其语法为 ( w i l d c a r d p a t t e r n ) , p a t t e r n 为 匹 配 的 模 式 , 比 如 (wildcard pattern),pattern为匹配的模式,比如 (wildcardpattern)pattern(wildcard %.c) 为查找当前路径下面文件名以.c结尾的文件。

foreach 函数:其语法为$(foreach var,list,text),每循环一次var从list中按顺序取值一个,然后执行一次text代码并记录结果,最终返回所用text代码运行的结果。比如

dirs := C_DIR S_DIR

file := ( f o r e a c h d i r , (foreach dir, (foreachdir,(dirs),$(wildcard $(dir)/*))

将C_DIR和S_DIR文件夹下面的所有文件添加到file变量中。

dir 函数:其语法为$(dir names…),用于获取names中文件夹路径,比如

$(dir src/foo.c hacks)

将获得文件夹路径 src/ ./

notdir 函数:其语法为$(notdir names…),用于获取names中除去路径的信息,比如

$(notdir src/foo.c hacks)

将获得文件信息 foo.c hacks

basename 函数:其语法为$(basename names…),用于获取names中除去后缀信息,比如

$(basename src/foo.c src-1.0/bar hacks)

将获得信息 src/foo src-1.0/bar hacks

addsuffix 函数:其语法为$(addsuffix suffix,names…),用于往names中添加后缀信息suffix,比如

$(addsuffix .c,foo bar)

将获得文件信息 foo.c bar.c

addprefix 函数:其语法为$(addprefix prefix,names…),用于往names中添加前缀信息prefix,比如

$(addprefix src/,foo bar)

将获得信息src/foo src/bar

patsubst 函数:其语法为$(patsubst pattern,replacement,text),根据 pattern信息将text替换成replacement,比如

objects = foo.o bar.o baz.o

files = ( p a t s u b s t (patsubst %.o,%.c, (patsubst( objects))

将获得信息 foo.c bar.c baz.c

其可以简单写成

objects = foo.o bar.o baz.o

files = $(objects:.o=.c)————————————————

Makefile 描述的是文件编译的相关规则,它的规则主要是两个部分组成,分别是依赖的关系和执行的命令,其结构如下所示:

targets : prerequisites
    command

相关说明如下:
targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。
注意:我们的目标和依赖文件之间要使用冒号分隔开,命令的开始一定要使用Tab键。

2、它的具体工作顺序是:当在 shell 提示符下输入 make 命令以后。 make 读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。在我们的例子中,第一个规则就是目标 “main” 所在的规则。
“.o” 文件为目标的规则处理有下列三种情况:
目标 “.o” 文件不存在,使用其描述规则创建它;
目标 “.o” 文件存在,目标 “.o” 文件所依赖的 “.c” 源文件 “.h” 文件中的任何一个比目标 “.o” 文件“更新”(在上一次 make 之后被修改)。则根据规则重新编译生成它;
目标 “.o” 文件存在,目标 “.o” 文件比它的任何一个依赖文件(".c" 源文件、".h" 文件)“更新”(它的依赖文件在上一次 make 之后没有被修改),则什么也不做。

3、如果我们的通配符使用在依赖的规则中的话一定要注意这个问题:不能通过引用变量的方式来使用,如下所示。
纯文本复制

OBJ=*.c
test:$(OBJ)
    gcc -o $@ $^

我们去执行这个命令的时候会出现错误,提示我们没有 “.c" 文件,实例中我们相要表示的是当前目录下所有的 “.c” 文件,但是我们在使用的时候并没有展开,而是直接识别成了一个文件。文件名是 ".c”。

如果我们就是相要通过引用变量的话,我们要使用一个函数 “wildcard”,这个函数在我们引用变量的时候,会帮我们展开。我们把上面的代码修改一下就可以使用了。

OBJ=$(wildcard *.c)
test:$(OBJ)
    gcc -o $@ $^

这样我们再去使用的时候就可以了。调用函数的时候,会帮我们自动展开函数。

4、"%.o" 把我们需要的所有的 “.o” 文件组合成为一个列表,从列表中挨个取出的每一个文件,"%" 表示取出来文件的文件名(不包含后缀),然后找到文件中和 "%"名称相同的 “.c” 文件,然后执行下面的命令,直到列表中的文件全部被取出来为止。

VPATH的使用
在 Makefile 中可以这样写:
VPATH := src

我们可以这样理解,把 src 的值赋值给变量 VPATH,所以在执行 make 的时候会从 src 目录下找我们需要的文件。

当存在多个路径的时候我们可以这样写:
VPATH := src car

或者是
VPATH := src:car
多个路径之间要使用空格或者是冒号隔开,表示在多个路径下搜索文件。

vpath的使用
学习了 VPATH的使用,我们再来了解一下关键字搜索 vpath 的使用,这种搜索方式一般被称作选择性搜索。使用上的区别我们可以这样理解:VPATH 是搜索路径下所有的文件,而 vpath 更像是添加了限制条件,会过滤出一部分再去寻找。

具体用法:

vpath PATTERN DIRECTORIES
vpath PATTERN
vpath
( PATTERN:可以理解为要寻找的条件,DIRECTORIES:寻找的路径 )
首先是用法一,命令格式如下:
vpath test.c src

可以这样理解,在 src 路径下搜索文件 test.c。多路径的书写规则如下:
vpath test.c src car 或者是 vpath test.c src : car

多路径的用法其实和 VPATH 差不多,都是使用空格或者是冒号分隔开,搜索路径的顺序是先 src 目录,然后是 car 目录。

其次是用法二,命令格式如下:
vpath test.c
用法二的意思是清除符合文件 test.c 的搜索目录。

最后是用法三,命令格式如下:
vpath
vpath 单独使的意思是清除所有已被设置的文件搜索路径。

8、Makefile ifeq、ifneq、ifdef和ifndef(条件判断)

关键字 功能
ifeq 判断参数是否不相等,相等为 true,不相等为 false。
ifneq 判断参数是否不相等,不相等为 true,相等为 false。
ifdef 判断是否有值,有值为 true,没有值为 false。
ifndef 判断是否有值,没有值为 true,有值为 false。
feq 和 ifneq
条件判断的使用方式如下:
ifeq (ARG1, ARG2)
ifeq ‘ARG1’ ‘ARG2’
ifeq “ARG1” “ARG2”
ifeq “ARG1” ‘ARG2’
ifeq ‘ARG1’ “ARG2”
实例:

libs_for_gcc= -lgnu
normal_libs=
ifeq( ( C C ) , g c c ) l i b s = (CC),gcc) libs= (CC),gcc)libs=(libs_for_gcc)
else
libs= ( n o r m a l l i b s ) e n d i f f o o : (normal_libs) endif foo: (normallibs)endiffoo:(objects)
$(CC) -o foo $(objects) $(libs)

ifdef 和 ifndef
使用方式如下:
ifdef VARIABLE-NAME

它的主要功能是判断变量的值是不是为空,实例:

实例 1:

bar =
foo = $(bar)
all:
ifdef foo
@echo yes
else
@echo no
endif

实例 2:

foo=
all:
ifdef foo
@echo yes
else
@echo no
endif

通过两个实例对比说明:通过打印 “yes” 或 “no” 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 “yes” ,实例 2打印的结果是 “no” 。其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。

9、在书写伪目标的时候,需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标 “clean” 的完整书写格式如下:

.PHONY:clean
clean:
    rm -rf *.o test

使用伪目标有两点原因:
避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突。
提高执行 make 时的效率,特别是对于一个大型的工程来说,提高编译的效率也是我们所必需的。

10、引用变量的格式为$(变量名),函数调用的格式如下:
$( ) 或者是 ${ }

其中,function 是函数名,arguments 是函数的参数,参数之间要用逗号分隔开。而参数和函数名之间使用空格分开。调用函数的时候要使用字符“$”,后面可以跟小括号也可以使用花括号。这个其实我们并不陌生,我们之前使用过许多的函数,比如说展开通配符的函数 wildcard,以及字符串替换的函数 patsubst ,Makefile 中函数并不是很多。

今天主要讲的是字符串处理函数,这些都是我们经常使用到的函数,下面是对函数详细的介绍。

模式字符串替换函数,函数使用格式如下:
( p a t s u b s t , , ) 函 数 说 明 : 函 数 功 能 是 查 找 t e x t 中 的 单 词 是 否 符 合 模 式 p a t t e r n , 如 果 匹 配 的 话 , 则 用 r e p l a c e m e n t 替 换 。 返 回 值 为 替 换 后 的 新 字 符 串 。 实 例 : O B J = (patsubst ,, ) 函数说明:函数功能是查找 text 中的单词是否符合模式 pattern,如果匹配的话,则用 replacement 替换。返回值为替换后的新字符串。实例: OBJ= (patsubst,,)textpatternreplacementOBJ=(patsubst %.c,%.o,1.c 2.c 3.c)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是 “1.o 2.o 3.o”,这些都是替换后的值。

字符串替换函数,函数使用格式如下:
( s u b s t , , ) 函 数 说 明 : 函 数 的 功 能 是 把 字 符 串 中 的 f o r m 替 换 成 t o , 返 回 值 为 替 换 后 的 新 字 符 串 。 实 例 : O B J = (subst ,, ) 函数说明:函数的功能是把字符串中的 form 替换成 to,返回值为替换后的新字符串。实例: OBJ= (subst,,)formtoOBJ=(subst ee,EE,feet on the street)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“fEEt on the strEEt”。

去空格函数,函数使用格式如下:
( s t r i p ) 函 数 说 明 : 函 数 的 功 能 是 去 掉 字 符 串 的 开 头 和 结 尾 的 字 符 串 , 并 且 将 其 中 的 多 个 连 续 的 空 格 合 并 成 为 一 个 空 格 。 返 回 值 为 去 掉 空 格 后 的 字 符 串 。 实 例 : O B J = (strip ) 函数说明:函数的功能是去掉字符串的开头和结尾的字符串,并且将其中的多个连续的空格合并成为一个空格。返回值为去掉空格后的字符串。实例: OBJ= (strip)OBJ=(strip a b c)
all:
@echo $(OBJ)
执行完 make 之后,结果是“a b c”。这个只是除去开头和结尾的空格字符,并且将字符串中的空格合并成为一个空格。

查找字符串函数,函数使用格式如下:
( f i n d s t r i n g , ) 函 数 说 明 : 函 数 的 功 能 是 查 找 i n 中 的 f i n d , 如 果 我 们 查 找 的 目 标 字 符 串 存 在 。 返 回 值 为 目 标 字 符 串 , 如 果 不 存 在 就 返 回 空 。 实 例 : O B J = (findstring ,) 函数说明:函数的功能是查找 in 中的 find ,如果我们查找的目标字符串存在。返回值为目标字符串,如果不存在就返回空。实例: OBJ= (findstring,)infind,OBJ=(findstring a,a b c)
all:
@echo $(OBJ)
执行 make 命令,得到的返回的结果就是 “a”。

过滤函数,函数使用格式如下:
( f i l t e r , ) 函 数 说 明 : 函 数 的 功 能 是 过 滤 出 t e x t 中 符 合 模 式 p a t t e r n 的 字 符 串 , 可 以 有 多 个 p a t t e r n 。 返 回 值 为 过 滤 后 的 字 符 串 。 实 例 : O B J = (filter , ) 函数说明:函数的功能是过滤出 text 中符合模式 pattern 的字符串,可以有多个 pattern 。返回值为过滤后的字符串。实例: OBJ= (filter,)textpatternpatternOBJ=(filter %.c %.o,1.c 2.o 3.s)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“1.c 2.o”。

反过滤函数,函数使用格式如下:
( f i l t e r − o u t , ) 函 数 说 明 : 函 数 的 功 能 是 功 能 和 f i l t e r 函 数 正 好 相 反 , 但 是 用 法 相 同 。 去 除 符 合 模 式 p a t t e r n 的 字 符 串 , 保 留 符 合 的 字 符 串 。 返 回 值 是 保 留 的 字 符 串 。 实 例 : O B J = (filter-out , ) 函数说明:函数的功能是功能和 filter 函数正好相反,但是用法相同。去除符合模式 pattern 的字符串,保留符合的字符串。返回值是保留的字符串。实例: OBJ= (filterout,)filterpatternOBJ=(filter-out 1.c 2.o ,1.o 2.c 3.s)
all:
@echo $(OBJ)
执行 make 命令,打印的结果是“3.s”。

排序函数,函数使用格式如下:
( s o r t ) 函 数 说 明 : 函 数 的 功 能 是 将 中 的 单 词 排 序 ( 升 序 ) 。 返 回 值 为 排 列 后 的 字 符 串 。 实 例 : O B J = (sort ) 函数说明:函数的功能是将 中的单词排序(升序)。返回值为排列后的字符串。实例: OBJ= (sort)OBJ=(sort foo bar foo lost)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“bar foo lost”。
注意:sort会去除重复的字符串。

取单词函数,函数使用格式如下:
( w o r d , ) 函 数 说 明 : 函 数 的 功 能 是 取 出 函 数 中 的 第 n 个 单 词 。 返 回 值 为 我 们 取 出 的 第 n 个 单 词 。 实 例 : O B J = (word , ) 函数说明:函数的功能是取出函数 中的第n个单词。返回值为我们取出的第 n 个单词。实例: OBJ= (word,)nnOBJ=(word 2,1.c 2.c 3.c)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“2.c”。

11、Makefile常用文件名操作函数

取目录函数,函数使用格式如下:
( d i r ) 函 数 说 明 : 函 数 的 功 能 是 从 文 件 名 序 列 n a m e s 中 取 出 目 录 部 分 , 如 果 没 有 n a m e s 中 没 有 “ / ” , 取 出 的 值 为 “ . / ” 。 返 回 值 为 目 录 部 分 , 指 的 是 最 后 一 个 反 斜 杠 之 前 的 部 分 。 如 果 没 有 反 斜 杠 将 返 回 “ . / ” 。 实 例 : O B J = (dir ) 函数说明:函数的功能是从文件名序列 names 中取出目录部分,如果没有 names 中没有 “/” ,取出的值为 “./” 。返回值为目录部分,指的是最后一个反斜杠之前的部分。如果没有反斜杠将返回“./”。实例: OBJ= (dir)namesnames/././OBJ=(dir src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是“src/ ./”。提取文件 foo.c 的路径是 “/src” 和文件 hacks 的路径 “./”。

取文件函数,函数使用格式如下:
( n o t d i r ) 函 数 说 明 : 函 数 的 功 能 是 从 文 件 名 序 列 n a m e s 中 取 出 非 目 录 的 部 分 。 非 目 录 的 部 分 是 最 后 一 个 反 斜 杠 之 后 的 部 分 。 返 回 值 为 文 件 非 目 录 的 部 分 。 实 例 : O B J = (notdir ) 函数说明:函数的功能是从文件名序列 names 中取出非目录的部分。非目录的部分是最后一个反斜杠之后的部分。返回值为文件非目录的部分。实例: OBJ= (notdir)namesOBJ=(notdir src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是“foo.c hacks”。

取后缀名函数,函数使用格式如下:
( s u f f i x ) 函 数 说 明 : 函 数 的 功 能 是 从 文 件 名 序 列 中 n a m e s 中 取 出 各 个 文 件 的 后 缀 名 。 返 回 值 为 文 件 名 序 列 n a m e s 中 的 后 缀 序 列 , 如 果 文 件 没 有 后 缀 名 , 则 返 回 空 字 符 串 。 实 例 : O B J = (suffix ) 函数说明:函数的功能是从文件名序列中 names 中取出各个文件的后缀名。返回值为文件名序列 names 中的后缀序列,如果文件没有后缀名,则返回空字符串。实例: OBJ= (suffix)namesnamesOBJ=(suffix src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“.c ”。文件 “hacks” 没有后缀名,所以返回的是空值。

取前缀函数,函数使用格式如下:
( b a s e n a m e ) 函 数 说 明 : 函 数 的 功 能 是 从 文 件 名 序 列 n a m e s 中 取 出 各 个 文 件 名 的 前 缀 部 分 。 返 回 值 为 被 取 出 来 的 文 件 的 前 缀 名 , 如 果 文 件 没 有 前 缀 名 则 返 回 空 的 字 符 串 。 实 例 : O B J = (basename ) 函数说明:函数的功能是从文件名序列 names 中取出各个文件名的前缀部分。返回值为被取出来的文件的前缀名,如果文件没有前缀名则返回空的字符串。实例: OBJ= (basename)namesOBJ=(notdir src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到值是“src/foo hacks”。获取的是文件的前缀名,包含文件路径的部分。

添加后缀名函数,函数使用格式如下:
( a d d s u f f i x , ) 函 数 说 明 : 函 数 的 功 能 是 把 后 缀 s u f f i x 加 到 n a m e s 中 的 每 个 单 词 后 面 。 返 回 值 为 添 加 上 后 缀 的 文 件 名 序 列 。 实 例 : O B J = (addsuffix ,) 函数说明:函数的功能是把后缀 suffix 加到 names 中的每个单词后面。返回值为添加上后缀的文件名序列。实例: OBJ= (addsuffix,)suffixnamesOBJ=(addsuffix .c,src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 后我们可以得到“sec/foo.c.c hack.c”。我们可以看到如果文件名存在后缀名,依然会加上。

添加前缀名函数,函数使用格式如下:
( a d d p e r f i x , ) 函 数 说 明 : 函 数 的 功 能 是 把 前 缀 p r e f i x 加 到 n a m e s 中 的 每 个 单 词 的 前 面 。 返 回 值 为 添 加 上 前 缀 的 文 件 名 序 列 。 实 例 : O B J = (addperfix ,) 函数说明:函数的功能是把前缀 prefix 加到 names 中的每个单词的前面。返回值为添加上前缀的文件名序列。实例: OBJ= (addperfix,)prefixnamesOBJ=(addprefix src/, foo.c hacks)
all:
@echo $(OBJ)

执行 make 命令,我们可以得到值是 “src/foo.c src/hacks” 。我们可以使用这个函数给我们的文件添加路径。

链接函数,函数使用格式如下:
( j o i n , ) 函 数 说 明 : 函 数 功 能 是 把 l i s t 2 中 的 单 词 对 应 的 拼 接 到 l i s t 1 的 后 面 。 如 果 l i s t 1 的 单 词 要 比 l i s t 2 的 多 , 那 么 , l i s t 1 中 多 出 来 的 单 词 将 保 持 原 样 , 如 果 l i s t 1 中 的 单 词 要 比 l i s t 2 中 的 单 词 少 , 那 么 l i s t 2 中 多 出 来 的 单 词 将 保 持 原 样 。 返 回 值 为 拼 接 好 的 字 符 串 。 实 例 : 纯 文 本 复 制 O B J = (join ,) 函数说明:函数功能是把 list2 中的单词对应的拼接到 list1 的后面。如果 list1 的单词要比 list2的多,那么,list1 中多出来的单词将保持原样,如果 list1 中的单词要比 list2 中的单词少,那么 list2 中多出来的单词将保持原样。返回值为拼接好的字符串。实例: 纯文本复制 OBJ= (join,)list2list1list1list2list1list1list2list2OBJ=(join src car,abc zxc qwe)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是“srcabc carzxc qwe”。很显然 中的文件名比的少,所以多出来的保持不变。

获取匹配模式文件名函数,命令使用格式如下:
( w i l d c a r d P A T T E R N ) 函 数 说 明 : 函 数 的 功 能 是 列 出 当 前 目 录 下 所 有 符 合 模 式 的 P A T T E R N 格 式 的 文 件 名 。 返 回 值 为 空 格 分 隔 并 且 存 在 当 前 目 录 下 的 所 有 符 合 模 式 P A T T E R N 的 文 件 名 。 实 例 : O B J = (wildcard PATTERN) 函数说明:函数的功能是列出当前目录下所有符合模式的 PATTERN 格式的文件名。返回值为空格分隔并且存在当前目录下的所有符合模式 PATTERN 的文件名。实例: OBJ= (wildcardPATTERN)PATTERNPATTERNOBJ=(wildcard *.c .h)
all:
@echo $(OBJ)
执行 make 命令,可以得到当前函数下所有的 ".c " 和 “.h” 结尾的文件。这个函数通常跟的通配符 “” 连用,使用在依赖规则的描述的时候被展开。

12、Makefile中的其它常用函数
$(foreach , )
函数的功能是:把参数中的单词逐一取出放到参数 所指定的变量中,然后再执行 所包含的表达式。每一次 会返回一个字符串,循环过程中, 的返所返回的每个字符串会以空格分割,最后当整个循环结束的时候, 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。所以 最好是一个变量名, 可以是一个表达式,而 中一般会只用 这个参数来一次枚举中的单词。

实例:
name:=a b c d
files:=( f o r e a c h n , (foreach n,(foreachn,(names),$(n).o)
all:
@echo $(files)
执行 make 命令,我们得到的值是“a.o b.o c.o d.o”。
注意,foreach 中的 参数是一个临时的局部变量,foreach 函数执行完后,参数的变量将不再作用,其作用域只在 foreach 函数当中。

$(if ,)或(if,)
可见,if 函数可以包含else部分,或者是不包含,即if函数的参数可以是两个,也可以是三个。condition参数是 if 表达式,如果其返回的是非空的字符串,那么这个表达式就相当于返回真,于是,then-part就会被计算,否则else-part会被计算。

而if函数的返回值是:如果condition为真(非空字符串),那么then-part会是整个函数的返回值。如果condition为假(空字符串),那么else-part将会是这个函数的返回值。此时如果else-part没有被定义,那么整个函数返回空字串符。所以,then-part和else-part只会有一个被计算。

实例:

OBJ:=foo.c
OBJ:=$(if ( O B J ) , (OBJ),(OBJ),(OBJ),main.c)
all:
@echo $(OBJ)

执行 make 命令我们可以得到函数的值是 foo.c,如果变量 OBJ 的值为空的话,我们得到的 OBJ 的值就是main.c。
$(call ,…)
call 函数是唯一一个可以用来创建新的参数化的函数。我们可以用来写一个非常复杂的表达式,这个表达式中,我们可以定义很多的参数,然后你可以用 call 函数来向这个表达式传递参数。

当 make 执行这个函数的时候,expression参数中的变量( 1 ) 、 (1)、(1)、(2)、$(3)等,会被参数parm1,parm2,parm3依次取代。而expression的返回值就是 call 函数的返回值。

实例 1:

reverse = $(1) $(2)
foo = $(call reverse,a,b)
all:
@echo $(foo)

那么,foo 的值就是“a b”。当然,参数的次序可以是自定义的,不一定是顺序的,

实例 2:

reverse = $(2) $(1)
foo = $(call reverse,a,b)
all:
@echo $(foo)

此时的 foo 的值就是“b a”。
( o r i g i n < v a r i a b l e > ) o r i g i n 函 数 不 像 其 他 的 函 数 , 它 并 不 操 作 变 量 的 值 , 它 只 是 告 诉 你 这 个 变 量 是 哪 里 来 的 。 注 意 : v a r i a b l e 是 变 量 的 名 字 , 不 应 该 是 引 用 , 所 以 最 好 不 要 在 v a r i a b l e 中 使 用 “ (origin ) origin 函数不像其他的函数,它并不操作变量的值,它只是告诉你这个变量是哪里来的。 注意: variable 是变量的名字,不应该是引用,所以最好不要在 variable 中使用“(origin)origin函数不像其他的函数,它并不操作变量的值,它只是告诉你这个变量是哪里来的。注意:variable是变量的名字,不应该是引用,所以最好不要在variable中使用“”字符。origin 函数会员其返回值来告诉你这个变量的“出生情况”。

下面是origin函数返回值:
“undefined”:如果从来没有定义过,函数将返回这个值。
“default”:如果是一个默认的定义,比如说“CC”这个变量。
“environment”:如果是一个环境变量并且当Makefile被执行的时候,“-e”参数没有被打开。
“file”:如果这个变量被定义在Makefile中,将会返回这个值。
“command line”:如果这个变量是被命令执行的,将会被返回。
“override”:如果是被override指示符重新定义的。
“automatic”:如果是一个命令运行中的自动化变量。

这些信息对于我们编写 Makefile 是非常有用的,例如假设我们有一个 Makefile ,其包含了一个定义文件Make.def,在Make.def中定义了一个变量bletch,而我们的环境变量中也有一个环境变量bletch,我们想去判断一下这个变量是不是环境变量,如果是我们就把它重定义了。如果是非环境变量,那么我们就不重新定义它。于是,我们在 Makefile 中,可以这样写:
ifdef bletch
ifeq “$(origin bletch)” “environment”
bletch = barf,gag,etc
endif
endif

13、命令回显
通常 make 在执行命令行之前会把要是执行的命令行输出到标准输出设备。我们称之为 “回显”,就好像我们在 shell 环境下输入命令执行时一样。如果规则的命令行以字符“@”开始,则 make 在执行的时候就不会显示这个将要被执行的命令。典型的用法是在使用echo命令输出一些信息时。

实例 1:
OBJ=test main list
all:
@echo $(OBJ)
执行时将会得到test main list这条输出信息,如果在执行命令之前没有字符“@”,那么make的输出将是 echo test main list。

我们在执行 make 时添加上一些参数,可以控制命令行是否输出。当使用 make 的时候机加上参数-n或者是–just-print ,执行时只显示所要执行的命令,但不会真正的执行这个命令。只有在这种情况下 make 才会打印出所有的 make 需要执行的命令,其中包括了使用的“@”字符开始的命令。这个选项对于我们调试 Makefile 非常的有用,使用这个选项就可以按执行顺序打印出 Makefile 中所需要执行的所有命令。而 make 参数-s或者是–slient则是禁止所有的执行命令的显示。就好像所有的命令行都使用“@”开始一样。

14、命令的执行
当规则中的目标需要被重建的时候,此规则所定义的命令将会被执行,如果是多行的命令,那么每一行命令将是在一个独立的子 shell 进程中被执行。因此,多命令行之间的执行命令时是相互独立的,相互之间不存在以来。

在 Makefile 中书写在同一行中的多个命令属于一个完整的 shell 命令行,书写在独立行的一条命令是一个独立的 shell 命令行。因此:在一个规则的命令中命令行 “cd”改变目录不会对其后面的命令的执行产生影响。就是说之后的命令执行的工作目录不会是之前使用“cd”进入的那个目录。如果达到这个目的,就不能把“cd”和其后面的命令放在两行来书写。而应该把这两个命令放在一行上用分号隔开。这样才是一个完整的 shell 命令行。

实例 2:
foo:bar/lose
cd bar;gobble lose >…/foo
如果想把一个完整的shell命令行书写在多行上,需要使用反斜杠 ()来对处于多行的命令进行连接,表示他们是一个完整的shell命令行。例如上例我们也可以这样书写:
foo:bar.lose
cd bar;
gobble lose > …/foo
make 对所有规则的命令的解析使用环境变量“SHELL”所指定的那个程序。在 GNU make 中,默认的程序时 “/bin/sh”。不像其他的绝大多数变量,他们的只可以直接从同名的系统环境变量那里获得。make 的环境变量 “SHELL”没有使用环境变量的定义。因为系统环境变量“SHELL”指定的那个程序被用来作为用户和系统交互的接口程序,他对于不存在直接交互过程的 make 显然不合适。在 make 环境变量中“SHELL”会被重新赋值;他作为一个变量我们也可以在 Makefile 中明确的给它赋值,变量“SHELL“的默认值时“/bin/sh”。
并发执行命令
GNU make 支持同时执行多条命令。通常情况下,同一时刻只有一个命令在执行,下一个命令只有在当前命令结束之后才能够开始执行。不过可以通过 make 命令行选项 “-j” 或者 “–jobs” 来告诉 make 在同一时刻可以允许多条命令同时执行。

如果选项 “-j” 之后存在一个整数,其含义是告诉 make 在同一时刻可以允许同时执行的命令行的数目。这个数字被称为job slots。当 “-j” 选项中没有出现数字的时候,那么同一时间执行的命令数目没有要求。使用默认的job solts,值为1,表示make将串行的执行规则的命令(同一时刻只能由一条命令被执行)。

15、“include” 使用的具体方式如下:
include

filenames 是 shell 支持的文件名(可以使用通配符表示的文件)。
注意:“include” 关键字所在的行首可以包含一个或者是多个的空格(读取的时候空格会被自动的忽略),但是不能使用 Tab 开始,否则会把 “include” 当作式命令来处理。包含的多个文件之间要使用空格分隔开。使用 “include” 包含进来的 Makefile 文件中,如果存在函数或者是变量的引用,它们会在包含的 Makefile 中展开。

include 通常使用在以下的场合:
在一个工程文件中,每一个模块都有一个独立的 Makefile 来描述它的重建规则。它们需要定义一组通用的变量定义或者是模式规则。通用的做法是将这些共同使用的变量或者模式规则定义在一个文件中,需要的时候用 “include” 包含这个文件。
当根据源文件自动产生依赖文件时,我们可以将自动产生的依赖关系保存在另一个文件中。然后在 Makefile 中包含这个文件。
注意:如果使用 “include” 包含文件的时候,指定的文件不是文件的绝对路径或者是为当前文件下没有这个文件,make 会根据文件名会在以下几个路径中去找,首先我们在执行 make 命令的时候可以加入选项 “-I” 或 “–include-dir” 后面添加上指定的路径,如果文件存在就会被使用,如果文件不存在将会在其他的几个路径中搜索:“usr/gnu/include”、“usr/local/include” 和 “usr/include”。

如果在上面的路径没有找到 “include” 指定的文件,make 将会提示一个文件没有找到的警示提示,但是不会退出,而是继续执行 Makefile 的后续的内容。当完成读取整个 Makefile 后,make 将试图使用规则来创建通过 “include” 指定但不存在的文件。当不能创建的时候,文件将会保存退出。

使用时,通常用 “-include” 来代替 “include” 来忽略文件不存在或者是无法创建的错误提示,使用格式如下:
-include

使用方法和 “include” 的使用方法相同。

这两种方式之间的区别:
使用 "include " ,make 在处理程序的时候,文件列表中的任意一个文件不存在的时候或者是没有规则去创建这个文件的时候,make 程序将会提示错误并保存退出。
使用 "-include ",当包含的文件不存在或者是没有规则去创建它的时候,make 将会继续执行程序,只有真正由于不能完成终极目标重建的时候我们的程序才会提示错误保存退出。

16、嵌套执行make
举例说明如下:
subsystem:
cd subdir && $(MAKE)
这个例子可以这样来理解,在当前目录下有一个目录文件 subdir 和一个 Makefile 文件,子目录 subdir 文件下还有一个 Makefile 文件,这个文件是用来描述这个子目录文件的编译规则。使用时只需要在最外层的目录中执行 make 命令,当命令执行到上述的规则时,程序会进入到子目录中执行 make。这就是嵌套执行 make,我们把最外层的 Makefile 称为是总控 Makefile。

上述的规则也可以换成另外一种写法:
subsystem
$(MAKE) -C subdir
在 make 的嵌套执行中,我们需要了解一个变量 “CURDIR”,此变量代表 make 的工作目录。当使用 make 的选项 “-C” 的时候,命令就会进入指定的目录中,然后此变量就会被重新赋值。总之,如果在 Makefile 中没有对此变量进行显式的赋值操作,那么它就表示 make 的工作目录。我们也可以在 Makefile 中为这个变量赋一个新的值,当然重新赋值后这个变量将不再代表 make 的工作目录。
export的使用
使用 make 嵌套执行的时候,变量是否传递也是我们需要注意的。如果需要变量的传递,那么可以这样来使用:
export

如果不需要那么可以这样来写:
unexport

是变量的名字,不需要使用 “$” 这个字符。如果所有的变量都需要传递,那么只需要使用 “export” 就可以,不需要添加变量的名字。

Makefile 中还有两个变量不管是不是使用关键字 “export” 声明,它们总会传递到下层的 Makefile 中。这两个变量分别是 SHELL 和 MAKEFLAGS,特别是 MAKEFLAGS 变量,包含了 make 的参数信息。如果执行总控 Makefile 时,make 命令带有参数或者在上层的 Makefile 中定义了这个变量,那么 MAKEFLAGS 变量的值将会是 make 命令传递的参数,并且会传递到下层的 Makefile 中,这是一个系统级别的环境变量。

make 命令中有几个参数选项并不传递,它们是:"-C"、"-f"、"-o"、"-h" 和 “-W”。如果我们不想传递 MAKEFLAGS 变量的值,在 Makefile 中可以这样来写:
subsystem:
cd subdir && $(MAKE) MAKEFLAGS=

17、通过一个大的项目工程来详细的分析一下如何嵌套执行 make。

假设有一个 MP3 player 的应用程序,它可以被划分为若干个组件:用户界面(ui)、编解码器(codec)以及数据管理库(db)。它们分别可以用三个程序库来表示:libui.a、libcodec.a 和 libdb.a。将这些组件紧凑的放到一起就可以组成这个应用程序。具体的文件结构展示为(我们展示的只是目录文件,没有展示详细的源文件):
├──Makefile //最外层的Makefile文件,不是目录文件。
├──include //编译的时候需要链接的库文件
│ ├──codec //libui.a 库文件所在的目录
│ ├──db //libdb.a 库文件所在的目录
│ ├──ui //libui.a库文件所在的目录
├──lib //源文件所在的目录,子目录文件中包含Makefile文件
│ ├──codec //编解码器所在的源文件的目录
│ ├──db //数据库源文件所在的目录
│ ├──ui //用户界面源文件所在目录
├──app
│ ├──player
└──doc //这个工程编译说明

我们可以看到最外层有一个 Makefile 文件,这就是我们的 “总控Makefile” 文件,我们使用这个 Makefile 调用项目中各个子目录的 Makefile 文件的运行。假设只有我们的 lib 目录下和 app 目录下的各个子目录含有 Makefile 文件。那我们总控的 Makefile 的文件可以这样来写:

lib_codec := lib/codec
lib_db := lib/db
lib_ui := lib/ui
libraries := $(lib_codec) $(lib_db) $(lib_ui)
player := app/player
.PHONY : all $(player) $(libraries)
all : $(player)
$(player) $(libraries) :
$(MAKE) -C $@

我们可以看到在 “总控 Makefile” 中,一个规则在工作目标上列出了所有的子目录,它对每一个子目录的 Makefile 调用的代码是:
$(player) $(libraries) :
$(MAKE) -C $@

在 Makefile 文件中,MAKE 变量应该总是用来调用 make 程序。make 程序一看到 MAKE 变量就会把它设成 make 的实际路径,所以递归调用中的每次调用都会使用同一个执行文件。此外,当命令 --touch(-t)、–just-print(-n) 和 --question(-q) 被使用时,包含 MAKE 变量的每一行都会受到特别的处理。

由于这些“工作目标目录”被设成 .PHONY 的依赖文件,所以即使工作目标已经更新,此规则仍旧会进行更新动作。使 --directory(-C) 选项的目的是要让 make 在读取 Makefile 之前先切换到相应的 “工作目录” 。

当 make 在建立依存图的时候找不到程序库与 app/player 工作目标之间的依存关系时,这意味着建立任何程序库之前,make 将会先执行 app/player 目录中的 Makefile。显然这将会导致失败的结果,因为应用程序的链接需要程序库。为解决这个问题,我们会提供额外的依存信息:
$(player) : $(libraries)
$(lib_ui) : $(lib_db) $(lib_codec)

我们在此处做了如下的描述:运行 app/player 目录中的 Makefile 之前必须先运行程序库子目录中的 Makefile。此外,编译 lib/ui 目录中的程序代码之前必须先编译 lib/db 和lib/codec 目录中的程序库。这么做可以确保任何自动产生的程序代码,在 lib/ui 目录中的程序代码被编译之前就已经产生出来了。

更新必要条件的时候,会引发微妙的次序问题。如同所有的依存关系,更新的次序取决于依存图的分析结果,但是当工作目标的必要条件(依赖文件)出现在同一行时,GNU make 将会从左至右的次序进行更新。例如:
all : a b c
all : d e f

如果不存在其他的依存关系,这6个必要条件的更新动作可以是任何次序,不过GNU make将会以从左向右的次序来更新出现在同一行的必要条件,这会产生如下的更新次序:“a b c d e f” 或 “d e f a b c”。
注意:不要因为之前这么做更新的次序是对的,就以为每次这么做都是对的,而忘了提供完整的依存信息。

最后,依存分析可能会产生不同的次序而引发一些问题。所以,如果有一组工作目标需要以特定的次序进行更新时,就必须提供适当的必要条件来实现正确的次序。

当我们在最外层执行 make 的时候我们会看到l输出的信息:

make -C lib/db
make[1]: Entering directory ‘/MP3_player/lib/db’
make[1]:Update db library...
make[1]: Leaving directory ‘/MP3_player/lib/db’
make -C lib/codec
make[1]: Entering directory ‘/MP3_player/lib/codec’
make[1]:Update codec library...
make[1]: Leaving directory ‘/MP3_player/lib/codec’
make -C lib/ui
make[1]: Entering directory ‘/MP3_player/lib/ui’
make[1]:Update ui library...
make[1]: Leaving directory ‘/MP3_player/lib/ui’
make -C app/player
make[1]: Entering directory ‘/MP3_player/app/player’
make[1]:Update player library...
make[1]: Leaving directory ‘/MP3_player/app/player’

当 make 发觉它正在递归调用另一个 make 时,他会启 用–print-directory(-w) 选项,这会使得 make 输出 Entering directory(进入目录) 和 Leaving directory(离开目录) 的信息。当 --directory(-C) 选项被使用时,也会启用这个选项。我们还可以看到每一行中,MAKELEVEL 这个 make 变量的值加上方括号之后被一起输出。在这个简单的例子里,每个组件的 Makefile 只会输出组件正在更新的信息,而不会真正的更新组件。

18、变量的替换引用
我们定义变量的目的是为了简化我们的书写格式,代替我们在代码中频繁出现且冗杂的部分。它可以出现在我们规则的目标中,也可以是我们规则的依赖中。我们使用的时候会经常的对它的值(表示的字符串)进行操作。遇到这样的问题我们可能会想到我们的字符串操作函数,比如 “patsubst” 就是我们经常使用的。但是我们使用变量同样可以解决这样的问题,我们通过下面的例子来具体的分析一下。
实例:

foo:=a.c b.c d.c
obj:=$(foo:.c=.o)
All:
@echo $(obj)

这段代码实现的功能是字符串的后缀名的替换,把变量 foo 中所有的以 .c 结尾的字符串全部替换成 .o 结尾的字符串。我们在 Makefile 中这样写,然后再 shell 命令行执行 make 命令,就可以看到打印出来的是 “a.o b.o d.o” ,实现了文件名后缀的替换。
注意:括号中的变量使用的是变量名而不是变量名的引用,变量名的后面要使用冒号和参数选项分开,表达式中间不能使用空格。第二个变量 obj 是对整体的引用。

上面的例子我们可以换一种更加通用的方式来写,代码展示如下:

foo:=a.c b.c d.c
obj:=$(foo:%.c=%.o)
All:
@echo $(obj)

我们在 shell 中执行 make 命令,发现结果是相同的。

对比上面的实例我们可以看到,表达式中使用了 “%” 这个字符,这个字符的含义就是自动匹配一个或多个字符。在开发的过程中,我们通常会使用这种方式来进行变量替换引用的操作。

为什么这种方式比第一种方式更加实用呢?我们在实际使用的过程中,我们对变量值的操作不只是修改其中的一个部分,甚至是改变其中的多个,那么第一种方式就不能实现了。我们来看一下这种情况:

foo:=a123c a1234c a12345c
obj:=$(foo:a%c=x%y)
All:
@echo $(obj)

我们可以看到这个例子中我们操作的是两个不连续的部分,我们执行 make 后打印的值是 “x123y x1234y x12345y”,这种情况下我们使用第一种情况就不能实现,所以第二种的使用更全面。

19、变量的嵌套使用
变量的嵌套引用的具体含义是这样的,我们可以在一个变量的赋值中引用其他的变量,并且引用变量的数量和和次数是不限制的。下面我们通过几个实例来说明一下。
实例 1:

foo:=test
var:=$(foo)
All:
@echo ( v a r ) 

这 种 用 法 是 最 常 见 的 使 用 方 法 , 打 印 出 v a r 的 值 就 是 t e s t 。 我 们 可 以 认 为 是 一 层 的 嵌 套 引 用 。 实 例 2 : 纯 文 本 复 制 f o o = b a r v a r = t e s t v a r : = (var) 这种用法是最常见的使用方法,打印出 var 的值就是 test。我们可以认为是一层的嵌套引用。 实例 2: 纯文本复制 foo=bar var=test var:=(var)这种用法是最常见的使用方法,打印出var的值就是test。我们可以认为是一层的嵌套引用。
实例2:纯文本复制

foo=barvar=testvar:=($(foo))
All:
@echo ( v a r ) 

我 们 再 去 执 行 m a k e 命 令 的 时 候 得 到 的 结 果 也 是 t e s t , 我 们 可 以 来 分 析 一 下 这 段 代 码 执 行 的 过 程 : (var) 我们再去执行 make 命令的时候得到的结果也是 test,我们可以来分析一下这段代码执行的过程:(var)我们再去执行make命令的时候得到的结果也是test,我们可以来分析一下这段代码执行的过程:(foo) 代表的字符串是 bar,我们也定义了变量 bar,所以我们可以对 bar 进行引用,变量 bar 表示的值是 test,所以对 bar 的引用就是 test,所以最终 var 的值就是 test。

20、Makefile 中提供了两个控制 make 运行方式的函数。其作用是当 make 执行过程中检测到某些错误时为用户提供消息,并且可以控制 make 执行过程是否继续。这两个函数是 “error” 和 “warning”,我们来详细的介绍一下这两个函数。
$(error TEXT…)

函数说明如下:
函数功能:产生致命错误,并提示 “TEXT…” 信息给用户,并退出 make 的执行。需要说明的是:“error” 函数是在函数展开时(函数被调用时)才提示信息并结束 make 进程。因此如果函数出现在命令中或者一个递归的变量定义时,读取 Makefile 时不会出现错误。而只有包含 “error” 函数引用的命令被执行,或者定义中引用此函数的递归变量被展开时,才会提示知名信息 “TEXT…” 同时退出 make。
返回值:空
函数说明:“error” 函数一般不出现在直接展开式的变量定义中,否则在 make 读取 Makefile 时将会提示致命错误。

我们通过两个例子来说明一下;
实例 1:
ERROR1=1234
all:
ifdef ERROR1
( e r r o r e r r o r i s ( E R R O R 1 ) ) e n d i f m a k e 读 取 解 析 M a k e f i l e 时 , 如 果 所 起 的 变 量 名 是 已 经 定 义 好 的 " E R R O R 1 " , m a k e 将 会 提 示 致 命 错 误 信 息 " e r r o r i s 1234 " 并 保 存 退 出 。 实 例 2 : E R R = ( E R R O R 1 ) ) e n d i f m a k e 读 取 解 析 M a k e f i l e 时 , 如 果 所 起 的 变 量 名 是 已 经 定 义 好 的 " E R R O R 1 " , m a k e 将 会 提 示 致 命 错 误 信 息 " e r r o r i s 1234 " 并 保 存 退 出 。 实 例 2 : E R R = ( E R R O R 1 ) ) e n d i f m a k e 读 取 解 析 M a k e f i l e 时 , 如 果 所 起 的 变 量 名 是 已 经 定 义 好 的 " E R R O R 1 " , m a k e 将 会 提 示 致 命 错 误 信 息 " e r r o r i s 1234 " 并 保 存 退 出 。 实 例 2 : E R R = ( e r r o r f o u n d a n e r r o r ! ) . P H O N Y : e r r e r r : ; (error error is ( E R R O R 1 ) ) e n d i f m a k e 读 取 解 析 M a k e f i l e 时 , 如 果 所 起 的 变 量 名 是 已 经 定 义 好 的 " E R R O R 1 " , m a k e 将 会 提 示 致 命 错 误 信 息 " e r r o r i s 1234 " 并 保 存 退 出 。 实 例 2 : E R R = (ERROR1)) endif make 读取解析 Makefile 时,如果所起的变量名是已经定义好的"ERROR1",make 将会提示致命错误信息 "error is 1234" 并保存退出。 实例 2: ERR=(ERROR1))endifmake读取解析Makefile时,如果所起的变量名是已经定义好的"ERROR1",make将会提示致命错误信息"erroris1234"并保存退出。实例2:ERR=(error found an error!) .PHONY:err err:; (errorerroris(ERROR1))endifmakeMakefile"ERROR1"make"erroris1234"退2ERR=(ERROR1))endifmakeMakefile"ERROR1"make"erroris1234"退2ERR=(ERROR1))endifmakeMakefile"ERROR1"make"erroris1234"退2ERR=(errorfoundanerror!).PHONY:errerr:;(ERR)
这个例子,在 make 读取 Makefile 时不会出现致命错误。只有目标 “err” 被作为是一个目标被执行时才会出现。
$(warning TEXT…)

函数说明如下:
函数功能:函数 “warning” 类似于函数 “error” ,区别在于它不会导致致命错误(make不退出),而只是提示 “TEXT…”,make 的执行过程继续。
返回值:空
函数说明:用法和 “error” 类似,展开过程相同。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

七 六 伍

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

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

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

打赏作者

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

抵扣说明:

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

余额充值