自动化构建-make/Makefile 【Linux基础开发工具】

一、背景

• 会不会写makefile,从一个侧面说明了一个⼈是否具备完成大型工程的能力 • 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作 • makefile带来的好处就是⸺“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。 • make是一个命令#%#$#%@%@%$#%$#%#%#$%@_20dc++e2c6fa909a5cd62526615fe2788a,是一个解释makefile中指令的命令工具,一般来说,大多数的ide都有这个命令,比如:delphi的make,visual c++的nmake,linux下gnu的make。可见,makefile都成为了一种在工程方面的编译方法。 • make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。

二、Makefile编译过程

自动化构建-make/Makefile 【Linux基础开发工具】

Makefile文件中的命令有一定规范,一旦该文件编写好以后在Linux命令行中执行一条make命令即可自动编译整个工程。不同厂家的make可能会稍有不同,并且语法上也有区别,不过基本思想都差不多,主要还是落在目标依赖上,最广泛使用的是GNUmake。

2、语法规则

代码语言:javascript代码运行次数:0运行复制

目标 ... : 依赖 ...命令1命令2. . .

“Makefile的核心规则,类似于一位厨师做菜,目标就是做好一道菜,那么所谓的依赖就是各种食材,各种厨具等等,然后需要厨师好的技术方法类似于命令,才能作出一道好菜。   同时这些依赖也有可能此时并不存在,需要现场制作,或者是由其他厨师做好,那么这个依赖就成为了其他规则的目标,该目标也会有他自己的依赖和命令。这样就形成了一层一层递归依赖组成了Makefile文件。 Makefile并不会关心命令是如何执行的,仅仅只是会去执行所有定义的命令,和我们平时直接输入命令行是一样的效果。”

目标即要生成的文件。如果目标文件的更新时间晚于依赖文件更新时间,则说明依赖文件没有改动,目标文件不需要重新编译。否则会进行重新编译并更新目标文件。默认情况下Makefile的第一个目标为终极目标。依赖:即目标文件由哪些文件生成。命令:即通过执行命令由依赖文件生成目标文件。注意每条命令之前必须有一个tab(此文档编辑器默认是空格,复制下来的代码需要把命令代码的缩进改为tab制表符)保持缩进,这是语法要求。all:Makefile文件默认只生成第一个目标文件即完成编译,但是我们可以通过all 指定所需要生成的目标文件。 例如下面的例子:代码语言:javascript代码运行次数:0运行复制

all: target1 target2 target3target1:# 编译规则1target2:# 编译规则2target3:# 编译规则3

all被设置为第一个目标,并且target1、target2和target3被列为all的依赖。当你在命令行中运行make时,make命令会寻找并执行all目标规则,这将依次执行target1、target2和target3的编译规则。 因此,通过在Makefile中设置all作为默认目标规则,你可以简化构建过程,只需运行make命令即可执行整个编译过程,无需显式指定目标三、变量

符号表示取变量的值,当变量名多于一个字符时,使用”( )”

^ 表示所有的依赖文件 @ 表示生成的目标文件

代码语言:javascript代码运行次数:0运行复制

SRC = $(wildcard *.c)OBJ = $(patsubst %.c, %.o, $(SRC)) ALL: hello.out hello.out: $(OBJ)        gcc $< -o $@ %.o: %.c        gcc -c $< -o $@

四、变量赋值1、”=”是最普通的等号代码语言:javascript代码运行次数:0运行复制

在Makefile中容易搞错赋值等号,

使用 “=”进行赋值,变量的值是整个Makefile中最后被指定的值。

代码语言:javascript代码运行次数:0运行复制

VIR_A = AVIR_B = $(VIR_A) BVIR_A = AA

经过上面的赋值后,最后VIR_B的值是AA B,而不是A B,在make时,会把整个Makefile展开,来决定变量的值(类似于宏定义)

2、“:=” 表示直接赋值代码语言:javascript代码运行次数:0运行复制

赋予当前位置的值。

代码语言:javascript代码运行次数:0运行复制

VIR_A := AVIR_B := $(VIR_A) BVIR_A := AA

最后BIR_B的值是A B,即根据当前位置进行赋值。因此相当于“=”,“:=”才是真正意义上的直接赋值

3、“?=” 表示如果该变量没有被赋值,代码语言:javascript代码运行次数:0运行复制

赋值予等号后面的值。

代码语言:javascript代码运行次数:0运行复制

VIR ?= new_value

如果VIR在之前没有被赋值,那么VIR的值就为new_value。

代码语言:javascript代码运行次数:0运行复制

VIR := old_valueVIR ?= new_value

这种情况下,VIR的值就是old_value

4、”+=”和写代码是一样的,代码语言:javascript代码运行次数:0运行复制

表示将符号后面的值添加到前面的变量上

五、预定义变量

CC:c编译器的名称,默认值为cc。 cpp c预编译器的名称默认值为$(CC) -E

代码语言:javascript代码运行次数:0运行复制

CC = gcc

回显问题,Makefile中的命令都会被打印出来。如果不想打印命令部分 可以使用@去除回显

代码语言:javascript代码运行次数:0运行复制

@echo "clean done!"

六、函数通配符代码语言:javascript代码运行次数:0运行复制

SRC = $(wildcard ./*.c)

匹配目录下所有.c 文件,并将其赋值给SRC变量。

代码语言:javascript代码运行次数:0运行复制

OBJ = $(patsubst %.c, %.o, $(SRC))

这个函数有三个参数,意思是取出SRC中的所有值,然后将.c 替换为.o 最后赋值给OBJ变量。

示例:如果目录下有很多个.c 源文件,就不需要写很多条规则语句了,而是可以像下面这样写

代码语言:javascript代码运行次数:0运行复制

SRC = $(wildcard *.c)OBJ = $(patsubst %.c, %.o, $(SRC)) ALL: test.exe  #生成执行文件 test.exe: $(OBJ)        gcc $(OBJ) -o test %.o: %.c        gcc -c $< -o $@

这里先将所有.c 文件编译为 .o 文件,这样后面更改某个 .c 文件时,其他的 .c 文件将不在编译,而只是编译有更改的 .c 文件,可以大大提高大项目中的编译速度。

智谱AI开放平台 智谱AI开放平台

智谱AI大模型开放平台-新一代国产自主通用AI开放平台

智谱AI开放平台 85 查看详情 智谱AI开放平台 七、伪目标 .PHONY

伪目标只是一个标签,clean是个伪目标没有依赖文件,只有用make来调用时才会执行 当目录下有与make 命令 同名的文件时 执行make 命令就会出现错误。 解决办法就是使用伪目标

代码语言:javascript代码运行次数:0运行复制

SRC = $(wildcard *.c)OBJ = $(patsubst %.c, %.o, $(SRC)) ALL: test.exe test.exe: $(OBJ)        gcc $< -o $@ $(OBJ): $(SRC)        gcc -c $< -o $@ clean:        rm -rf $(OBJ) test.exe .PHONY: clean ALL通常也会把ALL设置成伪目标

八、其他常用功能代码清理clean

我们可以编译一条属于自己的clean语句,来清理make命令所产生的所有文件,列如

代码语言:javascript代码运行次数:0运行复制

SRC = $(wildcard *.c)OBJ = $(patsubst %.c, %.o, $(SRC)) ALL: test.exe test.exe: $(OBJ)        gcc $< -o $@ $(OBJ): $(SRC)        gcc -c $< -o $@ clean:        rm -rf $(OBJ) test.exe

九、嵌套执行Makefile

在一些大工程中,会把不同模块或不同功能的源文件放在不同的目录中,我们可以在每个目录中都写一个该目录的Makefile这有利于让我们的Makefile变的更加简洁,不至于把所有东西全部写在一个Makefile中。   列如在子目录subdir目录下有个Makefile文件,来指明这个目录下文件的编译规则。外部总Makefile可以这样写

代码语言:javascript代码运行次数:0运行复制

subsystem:            cd subdir && $(MAKE)其等价于:subsystem:            $(MAKE) -C subdir

定义$(MAKE)宏变量的意思是,也许我们的make需要一些参数,所以定义成一个变量比较有利于维护。两个例子意思都是先进入”subdir”目录,然后执行make命令   我们把这个Makefile叫做总控Makefile,总控Makefile的变量可以传递到下级的Makefile中,但是不会覆盖下层Makefile中所定义的变量,除非指定了 “-e”参数。   如果传递变量到下级Makefile中,那么可以使用这样的声明export   如果不想让某些变量传递到下级Makefile,可以使用unexport

代码语言:javascript代码运行次数:0运行复制

export variable = value等价于variable = valueexport variable等价于export variable := value等价于variable := valueexport variable如果需要传递所有变量,那么只要一个export就行了。后面什么也不用跟,表示传递所有变量

十、指定头文件路径

一般都是通过”-I”(大写i)来指定,假设头文件在:

代码语言:javascript代码运行次数:0运行复制

/home/develop/include

则可以通过-I指定:

代码语言:javascript代码运行次数:0运行复制

-I/home/develop/include

将该目录添加到头文件搜索路径中 在Makefile中则可以这样写:

代码语言:javascript代码运行次数:0运行复制

CFLAGS=-I/home/develop/include

然后在编译的时候,引用CFLAGS即可,如下

代码语言:javascript代码运行次数:0运行复制

yourapp:*.c    gcc $(CFLAGS) -o yourapp

十一、指定库文件路径

与上面指定头文件类似只不过使用的是”-L”来指定

代码语言:javascript代码运行次数:0运行复制

LDFLAGS=-L/usr/lib -L/path/to/your/lib

告诉链接器要链接哪些库文件,使用”-l”(小写L)如下:

代码语言:javascript代码运行次数:0运行复制

LIBS = -lpthread -liconv

十二、基本使用

实例代码

代码语言:javascript代码运行次数:0运行复制

#include  int main() {     printf("hello Makefile!n");     return 0;} 

Makefile文件

代码语言:javascript代码运行次数:0运行复制

myproc:myproc.cgcc -o myproc myproc.c.PHONY:cleanclean:rm -f myproc 

依赖关系 • 上面的文件myproc,它依赖myproc.c 依赖方法 • gcc -o myproc myproc.c ,就是与之对应的依赖关系 项目清理 • 工程是需要被清理的 • 像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令⸺“make clean”,以此来清除所有的目标文件,以便重编译。 • 但是一般我们这种clean的目标文件,我们将它设置为伪目标,用 .PHONY 修饰,伪目标的特性是,总是被执行的。 • 可以将我们的 hello 目标文件声明成伪目标,测试一下。 什么叫做总是被执行?

代码语言:javascript代码运行次数:0运行复制

$ stat XXXFile: ‘XXX’Size: 987  Blocks: 8  IO Block: 4096  regular fileDevice: fd01h/64769d Inode: 1321125 Links: 1Access: (0664/-rw-rw-r--) Uid: ( 1000/ whb) Gid: ( 1000/ whb)Access: 2024-10-25 17:05:30.430619002 +0800Modify: 2024-10-25 17:05:25.940595116 +0800Change: 2024-10-25 17:05:25.940595116 +0800⽂件 = 内容 + 属性Modify: 内容变更,时间更新Change:属性变更,时间更新Access:常指的是⽂件最近⼀次被访问的时间。在Linux的早期版本中,每当⽂件被访问时,其atime都会更新。但这种机制会导致⼤量的IO操作。具体更新原则,不做过多解释。
自动化构建-make/Makefile 【Linux基础开发工具】

? 结论: .PHONY:让make忽略源文件和可执行目标文件的M时间对比

十三、推导过程代码语言:javascript代码运行次数:0运行复制

myproc:myproc.o gcc myproc.o -o myproc myproc.o:myproc.sgcc -c myproc.s -o myproc.omyproc.s:myproc.i gcc -S myproc.i -o myproc.smyproc.i:myproc.c gcc -E myproc.c -o myproc.i .PHONY:clean clean: rm -f *.i *.s *.o myproc

编译

代码语言:javascript代码运行次数:0运行复制

$ makegcc -E myproc.c -o myproc.igcc -S myproc.i -o myproc.sgcc -c myproc.s -o myproc.ogcc myproc.o -o myproc
自动化构建-make/Makefile 【Linux基础开发工具】

make是如何工作的,在默认的方式下,也就是我们只输入make命令。那么:

make会在当前目录下找名字叫“Makefile”或“makefile”的文件。如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到 myproc 这个文件,并把这个文件作为最终的目标文件。如果 myproc 文件不存在,或是 myproc 所依赖的后面的 myproc.o 文件的文件修改时间要比 myproc 这个文件新(可以用 touch 测试),那么,他就会执行后面所定义的命令来生成myproc 这个文件。如果 myproc 所依赖的 myproc.o 文件不存在,那么 make 会在当前文件中找目标为myproc.o 文件的依赖性,如果找到则再根据那一个规则生成 myproc.o 文件。(这有点像一个堆栈的过程)当然,你的C文件和H文件是存在的啦,于是 make 会生成 myproc.o 文件,然后再用 myproc.o 文件声明 make 的终极任务,也就是执行文件 hello 了。这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。十四、适度扩展语法代码语言:javascript代码运行次数:0运行复制

BIN=proc.exe  # 定义变量CC=gcc#SRC=$(shell ls *.c)   # 采⽤shell命令⾏⽅式,获取当前所有.c⽂件名SRC=$(wildcard *.c)   # 或者使⽤ wildcard 函数,获取当前所有.c⽂件名OBJ=$(SRC:.c=.o)   # 将SRC的所有同名.c 替换 成为.o 形成⽬标⽂件列表LFLAGS=-o   # 链接选项FLAGS=-c   # 编译选项RM=rm -f   # 引⼊命令$(BIN):$(OBJ)@$(CC) $(LFLAGS) $@ $^   # $@:代表⽬标⽂件名。 $^: 代表依赖⽂件列表@echo "linking ... $^ to $@"%.o:%.c # %.c 展开当前⽬录下所有的.c。 %.o: 同时展开同名.o@$(CC) $(FLAGS) $< # %<: 对展开的依赖.c⽂件,⼀个⼀个的交给gcc。@echo "compling ... $< to $@" # @:不回显命令.PHONY:cleanclean:$(RM) $(OBJ) $(BIN)   # $(RM): 替换,⽤变量内容替换它.PHONY:testtest:@echo $(SRC)@echo $(OBJ)

以上就是自动化构建-make/Makefile 【Linux基础开发工具】的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/739312.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
手机淘宝和淘宝特价版有什么区别?详细区别介绍?
上一篇 2025年11月25日 14:20:11
解决反转链表后只打印一个节点的问题
下一篇 2025年11月25日 14:20:15

相关推荐

  • composer require-dev和require有什么不同_Composer Require与Require-Dev区别解析

    require用于声明项目运行必需的依赖,如框架、数据库组件和第三方SDK,这些包会随项目部署到生产环境;2. require-dev用于声明仅在开发和测试阶段需要的工具,如PHPUnit、PHPStan、Faker等,不会默认部署到生产环境;3. 安装时composer install根据环境决定…

    2026年5月10日
    1000
  • Golang JSON序列化:控制敏感字段暴露的最佳实践

    本教程探讨golang中如何高效控制结构体字段在json序列化时的可见性。当需要将包含敏感信息的结构体数组转换为json响应时,通过利用`encoding/json`包提供的结构体标签,特别是`json:”-“`,可以轻松实现对特定字段的忽略,从而避免敏感数据泄露,确保api…

    2026年5月10日
    000
  • 利用海象运算符简化条件赋值:Python教程与最佳实践

    本文旨在探讨Python中海象运算符(:=)在条件赋值场景下的应用。通过对比传统if/else语句与海象运算符,以及条件表达式,分析海象运算符在简化代码、提高可读性方面的优势与局限性。并通过具体示例,展示如何在列表推导式等场景下合理使用海象运算符,同时强调其潜在的复杂性及替代方案,帮助开发者更好地掌…

    2026年5月10日
    100
  • Debian syslog性能优化技巧有哪些

    提升Debian系统syslog (通常基于rsyslog)性能,关键在于精简配置和高效处理日志。以下策略能有效优化日志管理,提升系统整体性能: 精简配置,高效加载: 在rsyslog配置文件中,仅加载必要的输入、输出和解析模块。 使用全局指令设置日志级别和格式,避免不必要的处理。 自定义模板: 创…

    2026年5月10日
    000
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • c++中的SFINAE技术是什么_c++模板编程中的SFINAE原理与应用

    SFINAE 是“替换失败不是错误”的原则,指模板实例化时若参数替换导致错误,只要存在其他合法候选,编译器不报错而是继续重载决议。它用于条件启用模板、类型检测等场景,如通过 decltype 或 enable_if 控制函数重载,实现类型特征判断。尽管 C++20 引入 Concepts 简化了部分…

    2026年5月10日
    000
  • Go语言mgo查询构建:深入理解bson.M与日期范围查询的正确实践

    本文旨在解决go语言mgo库中构建复杂查询时,特别是涉及嵌套`bson.m`和日期范围筛选的常见错误。我们将深入剖析`bson.m`的类型特性,解释为何直接索引`interface{}`会导致“invalid operation”错误,并提供一种推荐的、结构清晰的代码重构方案,以确保查询条件能够正确…

    2026年5月10日
    100
  • Golang goroutine与channel调试技巧

    使用go run -race检测数据竞争,结合runtime.NumGoroutine监控协程数量,通过pprof分析阻塞调用栈,利用select超时避免永久阻塞,有效排查goroutine泄漏、死锁和数据竞争问题。 Go语言的goroutine和channel是并发编程的核心,但它们也带来了调试上…

    2026年5月10日
    000
  • 使用 Jupyter Notebook 进行探索性数据分析

    Jupyter Notebook通过单元格实现代码与Markdown结合,支持数据导入(pandas)、清洗(fillna)、探索(matplotlib/seaborn可视化)、统计分析(describe/corr)和特征工程,便于记录与分享分析过程。 Jupyter Notebook 是进行探索性…

    2026年5月10日
    000
  • 《魔兽世界》将于6月11日开启国服回归技术测试

    《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试

    《%ign%ignore_a_1%re_a_1%》官方宣布,将于6月11日开启国服回归技术测试,时间为7天,并称可以在6月内正式开服,玩家们可以访问官网下载战网客户端并预下载“巫妖王之怒”客户端,技术测试详情见下图。 WordAi WordAI是一个AI驱动的内容重写平台 53 查看详情 以上就是《…

    2026年5月10日 用户投稿
    200
  • php常量怎么用_PHP常量(define/const)定义与使用方法

    PHP中可通过define函数和const关键字定义常量,用于存储不可变值。define适用于全局作用域,支持动态名称和条件定义,如define(‘SITE_NAME’, ‘MyWebsite’);const在编译时生效,语法简洁但限制多,只能在类或全…

    2026年5月10日
    000
  • 如何在HTML中插入表单元素_HTML表单控件与输入类型使用指南

    HTML表单通过标签构建,包含action和method属性定义数据提交目标与方式,常用input类型如text、password、email等适配不同输入需求,配合label、required、placeholder提升可用性,结合textarea、select、button等控件实现完整交互,是…

    2026年5月10日
    100
  • 网站标题关键词更新后,搜索引擎为何仍显示旧标题?

    网站标题更新后,搜索引擎为何显示旧标题? 网站SEO优化中,站长常修改网站标题关键词,期望搜索结果显示自定义标题。然而,即使更新标签、meta keywords、meta description和结构化数据中的name属性后,搜索结果仍显示旧标题,这令人费解。本文将对此进行解释。 问题:站长修改了网…

    2026年5月10日
    100
  • c#文件怎么打开

    打开 C# 文件有三种方法:Visual Studio:启动 Visual Studio,通过“文件”菜单打开 C# 文件。文本编辑器:使用文本编辑器打开 C# 文件,将其视为普通文本。.NET Core 命令行工具:使用 csc.exe 命令行工具编译 C# 文件,生成可执行文件。 如何打开 C#…

    2026年5月10日
    000
  • 创建指定大小并填充特定数据的Golang文件教程

    本文将介绍如何使用Golang创建一个指定大小的文件,并用特定数据填充它。我们将使用 `os` 包提供的函数来创建和截断文件,从而实现快速生成大文件的目的。示例代码展示了如何创建一个10MB的文件,并将其填充为全零数据。掌握这些方法,可以方便地在例如日志系统或磁盘队列等场景中,预先创建测试文件或初始…

    2026年5月10日
    000
  • Python命令怎样使用profile分析脚本性能 Python命令性能分析的基础教程

    使用Python的cProfile模块分析脚本性能最直接的方式是通过命令行执行python -m cProfile your_script.py,它会输出每个函数的调用次数、总耗时、累积耗时等关键指标,帮助定位性能瓶颈;为进一步分析,可将结果保存为文件python -m cProfile -o ou…

    2026年5月10日
    000
  • 如何插入查询结果数据_SQL插入Select查询结果方法

    如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法

    使用INSERT INTO…SELECT语句可高效插入数据,通过NOT EXISTS、LEFT JOIN、MERGE语句或唯一约束避免重复;表结构不一致时可通过别名、类型转换、默认值或计算字段处理;结合存储过程可提升可维护性,支持参数化与动态SQL。 将查询结果数据插入到另一个表中,可以…

    2026年5月10日 用户投稿
    000
  • 使用 WebCodecs VideoDecoder 实现精确逐帧回退

    本文档旨在解决在使用 WebCodecs VideoDecoder 进行视频解码时,实现精确逐帧回退的问题。通过比较帧的时间戳与目标帧的时间戳,可以避免渲染中间帧,从而提高用户体验。本文将提供详细的解决方案和示例代码,帮助开发者实现精确的视频帧控制。 在使用 WebCodecs VideoDecod…

    2026年5月10日
    000
  • Discord.py 交互按钮超时与持久化解决方案

    本教程旨在解决Discord.py中交互按钮在一段时间后出现“This Interaction Failed”错误的问题。我们将深入探讨视图(View)的超时机制,并提供通过正确设置timeout参数以及利用bot.add_view()方法实现按钮持久化的具体方案,确保您的机器人交互功能稳定可靠,即…

    2026年5月10日
    000
  • Debian Copilot的社区活跃度如何

    debian copilot是codeberg社区维护的ai助手,旨在为debian用户提供服务。尽管搜索结果中没有直接提供关于debian copilot社区支持活跃度的具体数据,但我们可以通过debian社区的整体活跃度和特点来推断其活跃性。 Debian社区的一般情况: Debian拥有详尽的…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信