当前位置: 首页 > news >正文

从0到1:Makefile自动化编译实战全解析

目录

  • 一、Makefile 初相识
    • 1.1 什么是 Makefile
    • 1.2 Makefile 的基本结构
  • 二、语法基础夯实
    • 2.1 变量定义与使用
    • 2.2 模式规则
    • 2.3 伪目标
  • 三、实战项目搭建
    • 3.1 项目背景与需求
    • 3.2 编写基础 Makefile
    • 3.3 优化 Makefile
  • 四、进阶技巧探索
    • 4.1 多平台支持
    • 4.2 第三方库集成
    • 4.3 版本控制与清理
  • 五、总结与展望


一、Makefile 初相识

1.1 什么是 Makefile

在软件开发的庞大工程中,编译环节犹如一座桥梁,连接着代码编写与可执行程序的生成。当项目规模较小,仅有寥寥几个源文件时,手动执行编译命令或许还能应付自如。比如,使用gcc -o main main.c这样简单的命令,就能轻松将main.c源文件编译成可执行的main程序。

然而,一旦项目逐渐壮大,源文件数量从几个增长到几十个、几百个,甚至更多,手动编译就会变得异常繁琐且容易出错。此时,Makefile 就如同一位智能管家,肩负起自动化编译的重任。它通过定义一系列清晰明确的规则,详细指定哪些文件需要先编译,哪些文件需要后编译,哪些文件因为依赖关系的改变需要重新编译。有了 Makefile,开发者只需轻敲一个make命令,整个项目就能按照预设的规则自动完成编译,大大节省了时间和精力,显著提高了软件开发的效率。它是现代软件开发中不可或缺的工具,是保障项目高效构建和持续迭代的关键所在。

1.2 Makefile 的基本结构

Makefile 的基本结构主要由目标(Target)、依赖(Dependency)和命令(Command)这三个关键部分构成。

目标是 Makefile 中期望生成的文件或者想要执行的操作名称,它可以是最终的可执行文件,也可能是中间生成的目标文件,甚至可以是一个抽象的操作指令。例如,在一个 C 语言项目中,最终生成的可执行程序my_program就是一个典型的目标。依赖则明确了生成目标所不可或缺的文件或者其他目标。例如,my_program这个可执行文件可能依赖于多个目标文件,如main.o和utils.o,因为在链接阶段需要将这些目标文件合并在一起才能生成最终的可执行程序。而命令则是具体用于生成目标的一系列 shell 指令,这些命令必须以制表符(Tab)开头,以表明它们是构建目标的具体操作。

下面通过一个简单的 Makefile 示例来深入理解:

# 定义目标和依赖
my_program: main.o utils.ogcc -o my_program main.o utils.o  # 命令,注意前面是制表符main.o: main.c main.hgcc -c main.cutils.o: utils.c utils.hgcc -c utils.cclean:rm -f my_program main.o utils.o

在这个示例中,my_program是最终的目标,它依赖于main.o和utils.o这两个目标文件。当执行make my_program命令时,Makefile 会首先检查main.o和utils.o是否存在,如果不存在或者它们的源文件(main.c、utils.c等)有更新,就会执行相应的编译命令来生成它们。然后,再执行链接命令gcc -o my_program main.o utils.o,将这两个目标文件链接成可执行文件my_program。

main.o和utils.o也分别是目标,它们各自依赖于对应的源文件和头文件,并且都有自己的编译命令。最后,clean是一个特殊的目标,用于清理编译过程中生成的文件,执行make clean时,会执行rm -f my_program main.o utils.o命令,删除这些文件,保持工作目录的整洁。通过这个简单的示例,可以清晰地看到 Makefile 中目标、依赖和命令之间的紧密关系,以及它们是如何协同工作实现自动化编译的。

二、语法基础夯实

2.1 变量定义与使用

在 Makefile 中,变量是一种强大的工具,它能够存储各种值,比如文件名、编译选项、命令等,通过使用变量,可以让 Makefile 更加简洁、易维护,并且具有更高的灵活性。变量的定义十分简单,基本语法为变量名=值。例如:

CC = gcc
CFLAGS = -Wall -g
SRC = main.c utils.c

在上述例子中,定义了三个变量:CC表示编译器,这里指定为gcc;CFLAGS用于存储编译选项,-Wall表示开启所有警告信息,-g表示生成调试信息,方便后续调试程序;SRC则列出了源文件,包含main.c和utils.c。

变量的引用方式也很直观,使用$(变量名)或者${变量名}的形式。例如,在定义目标的规则中,可以这样引用变量:

$(BIN): $(OBJ)$(CC) $(CFLAGS) -o $(BIN) $(OBJ)

这里,$(BIN)表示最终生成的可执行文件的名称,$(OBJ)代表目标文件列表,$(CC)和$(CFLAGS)分别是编译器和编译选项。通过这种方式,当需要修改编译器、编译选项或者目标文件名时,只需要在变量定义处修改一次,整个 Makefile 中所有引用该变量的地方都会随之改变,大大提高了维护效率。

除了上述简单的变量定义和引用,Makefile 还支持多种变量赋值方式,以满足不同的需求。比如,使用:=进行立即赋值,这种方式在定义变量时就会立即解析其值,后续对变量的修改不会影响之前已经解析的值。例如:

A := hello
B := $(A) world
A := hi

此时,B的值为hello world,因为在定义B时,A的值是hello,并且已经被立即解析并赋值给了B,后续A的值变为hi,但不会影响B的值。

还有?=用于默认赋值,只有当变量未被定义时才会生效。例如:

A ?= default_value

如果之前没有定义过A,那么A就会被赋值为default_value;如果已经定义过A,则这条赋值语句不会起作用,A保持原来的值。

+=用于追加赋值,在变量原有的值基础上添加新的内容。例如:

CFLAGS = -Wall
CFLAGS += -O2

最终CFLAGS的值为-Wall -O2,这种方式在逐步添加编译选项或者文件列表时非常实用。

2.2 模式规则

模式规则是 Makefile 中一个非常强大的特性,它允许通过通配符%来匹配一类文件,从而为这类文件定义通用的规则,实现 “一条规则处理多个文件” 的效果,大大简化了 Makefile 的编写。模式规则的语法形式为%.o: %.c,其中%是通配符,代表任意长度的字符串。例如:

%.o: %.c$(CC) $(CFLAGS) -c $< -o $@

这条规则表示,所有的.o文件都依赖于同名的.c文件,并且通过$(CC) $(CFLAGS) -c $< -o $@这条命令来生成。这里,$<是自动变量,表示规则中的第一个依赖文件,在这个例子中就是对应的.c文件;$@也是自动变量,表示当前规则的目标文件,即.o文件。

假设有多个源文件main.c、utils.c、math.c等,按照传统的写法,需要为每个源文件单独编写生成目标文件的规则,如:

main.o: main.c$(CC) $(CFLAGS) -c main.c -o main.o
utils.o: utils.c$(CC) $(CFLAGS) -c utils.c -o utils.o
math.o: math.c$(CC) $(CFLAGS) -c math.c -o math.o

而使用模式规则后,只需要上面那一条规则就可以处理所有的.c文件生成对应的.o文件,无论项目中新增或删除了多少个.c文件,Makefile 都无需修改,极大地提高了可维护性和通用性。

模式规则通常与自动变量和变量结合使用,进一步增强其功能。比如,可以结合变量来指定不同的编译选项或者编译器。例如:

DEBUG = 1
ifeq ($(DEBUG), 1)CFLAGS = -Wall -g
elseCFLAGS = -O3
endif%.o: %.c$(CC) $(CFLAGS) -c $< -o $@

这里,通过条件判断变量DEBUG的值来设置不同的编译选项。当DEBUG为 1 时,使用-Wall -g选项,以便在开发过程中开启警告和生成调试信息;当DEBUG不为 1 时,使用-O3选项进行优化编译。

2.3 伪目标

伪目标是 Makefile 中一种特殊的目标,它并不代表实际存在的文件,而是用于执行特定的命令或操作,通常用于组织常见的构建任务,如清理编译生成的文件、执行测试、安装程序等。伪目标的定义需要使用特殊的标记.PHONY。以常见的clean目标为例,它用于清理编译过程中生成的文件,保持工作目录的整洁。例如:

.PHONY: clean
clean:rm -f $(BIN) $(OBJ)

在这个例子中,首先通过.PHONY: clean声明clean是一个伪目标。如果不声明clean为伪目标,当当前目录下恰好存在一个名为clean的文件时,执行make clean命令,Make 会认为目标clean已经存在且是最新的,不会执行后面的rm -f $(BIN) $(OBJ)命令,从而无法达到清理文件的目的。而声明为伪目标后,无论是否存在名为clean的文件,执行make clean时,都会执行后面的删除命令,确保编译生成的文件被正确删除。

伪目标还可以依赖其他目标或伪目标,形成复杂的操作流程。例如,定义一个all伪目标,用于一次性构建所有的目标:

.PHONY: all clean
all: $(BIN)@echo "All targets built successfully."$(BIN): $(OBJ)$(CC) $(CFLAGS) -o $(BIN) $(OBJ)%.o: %.c$(CC) $(CFLAGS) -c $< -o $@clean:rm -f $(BIN) $(OBJ)

在这个 Makefile 中,all伪目标依赖于$(BIN),当执行make all时,会先检查并构建$(BIN),如果$(BIN)的依赖文件$(OBJ)不存在或者有更新,会先通过模式规则生成$(OBJ),然后再链接生成$(BIN),最后输出提示信息 “All targets built successfully.”。通过这种方式,all伪目标可以方便地组织整个项目的构建流程,让开发者通过一个简单的命令就能完成复杂的构建任务。

此外,伪目标还可以用于执行一些自定义的操作,比如运行测试脚本、部署程序到远程服务器等。例如,定义一个test伪目标来运行测试用例:

.PHONY: test clean
test: $(BIN)./test.sh
clean:rm -f $(BIN) $(OBJ)

这里,test伪目标依赖于$(BIN),表示在运行测试脚本test.sh之前,需要先确保可执行文件$(BIN)已经构建完成。当执行make test时,会先检查并构建$(BIN),然后执行./test.sh脚本,运行测试用例。伪目标的存在使得 Makefile 不仅能够实现自动化编译,还能灵活地管理各种项目相关的操作,是 Makefile 强大功能的重要组成部分。

三、实战项目搭建

3.1 项目背景与需求

假设我们正在开发一个简单的 C++ 数学运算库项目,项目名称为MathLibrary,旨在提供一些常用的数学运算函数,如加法、减法、乘法和除法。项目结构如下:

MathLibrary/
├── include/
│   └── mathlib.h
├── src/
│   ├── add.cpp
│   ├── sub.cpp
│   ├── mul.cpp
│   └── div.cpp
├── test/
│   └── test_mathlib.cpp
└── main.cpp
  • include/mathlib.h:头文件,声明数学运算函数的原型。
  • src/*.cpp:源文件,实现具体的数学运算函数。
  • test/test_mathlib.cpp:测试文件,用于测试数学运算库的功能。
  • main.cpp:主程序文件,调用数学运算库的函数进行实际运算。

编译需求如下:

  1. 将src目录下的所有源文件编译成目标文件。
  2. 将main.cpp编译成目标文件。
  3. 将所有目标文件链接成可执行文件math_app。
  4. 提供一个清理目标,用于删除编译生成的目标文件和可执行文件。

3.2 编写基础 Makefile

根据上述项目需求,我们开始编写基础的 Makefile。

CC = g++
CFLAGS = -Wall -gSRC_DIR = src
INC_DIR = include
TEST_DIR = testSRC = $(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(main.cpp))
OBJ = $(patsubst %.cpp, %.o, $(SRC))math_app: $(OBJ)$(CC) $(CFLAGS) -I$(INC_DIR) -o math_app $(OBJ)$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@main.o: main.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c main.cpp -o main.oclean:rm -f $(OBJ) math_app

在这个 Makefile 中:

  • 首先定义了编译器CC为g++,编译选项CFLAGS为-Wall -g,用于开启所有警告信息并生成调试信息。
  • 定义了源文件目录SRC_DIR、头文件目录INC_DIR和测试文件目录TEST_DIR。
  • 使用wildcard函数获取src目录下所有的.cpp源文件以及main.cpp,并将结果存储在变量SRC中。
  • 通过patsubst函数将SRC中的.cpp文件替换为对应的.o目标文件,存储在变量OBJ中。
  • math_app目标依赖于所有的目标文件$(OBJ),通过链接这些目标文件生成可执行文件math_app。
  • 模式规则$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp用于将src目录下的每个.cpp源文件编译成对应的.o目标文件。
  • 单独为main.cpp定义了生成main.o的规则。
  • clean目标用于删除所有的目标文件和可执行文件,保持工作目录的整洁。

3.3 优化 Makefile

虽然基础的 Makefile 已经能够满足项目的编译需求,但还可以从以下几个方面进行优化,使其更加灵活和高效。

  1. 使用变量提高可维护性:进一步将一些重复出现的部分提取为变量,比如链接选项等。
CC = g++
CFLAGS = -Wall -g
LDFLAGS = SRC_DIR = src
INC_DIR = include
TEST_DIR = testSRC = $(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(main.cpp))
OBJ = $(patsubst %.cpp, %.o, $(SRC))BIN = math_app$(BIN): $(OBJ)$(CC) $(CFLAGS) $(LDFLAGS) -I$(INC_DIR) -o $(BIN) $(OBJ)$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@main.o: main.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c main.cpp -o main.oclean:rm -f $(OBJ) $(BIN)

这里定义了变量LDFLAGS用于存储链接选项(当前为空,可根据实际需求添加),以及变量BIN用于指定最终生成的可执行文件名称,这样在修改可执行文件名或链接选项时,只需要在变量定义处修改一次即可。

  1. 运用自动变量简化规则:在规则中,充分利用自动变量来简化命令的书写。自动变量$@表示目标文件,$<表示第一个依赖文件,$^表示所有依赖文件(去除重复)。
CC = g++
CFLAGS = -Wall -g
LDFLAGS = SRC_DIR = src
INC_DIR = include
TEST_DIR = testSRC = $(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(main.cpp))
OBJ = $(patsubst %.cpp, %.o, $(SRC))BIN = math_app$(BIN): $(OBJ)$(CC) $(CFLAGS) $(LDFLAGS) -I$(INC_DIR) -o $@ $^$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@main.o: main.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@clean:rm -f $(OBJ) $(BIN)

在链接目标$(BIN)的规则中,使用$@表示目标文件math_app,$^表示所有的目标文件$(OBJ),使命令更加简洁直观。

  1. 添加条件判断增强灵活性:根据不同的编译选项或环境变量,动态调整编译过程。例如,添加一个DEBUG变量,用于控制是否开启调试信息。
DEBUG = 1
CC = g++
CFLAGS = -Wall 
LDFLAGS = ifeq ($(DEBUG), 1)CFLAGS += -g
elseCFLAGS += -O3
endifSRC_DIR = src
INC_DIR = include
TEST_DIR = testSRC = $(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(main.cpp))
OBJ = $(patsubst %.cpp, %.o, $(SRC))BIN = math_app$(BIN): $(OBJ)$(CC) $(CFLAGS) $(LDFLAGS) -I$(INC_DIR) -o $@ $^$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@main.o: main.cpp$(CC) $(CFLAGS) -I$(INC_DIR) -c $< -o $@clean:rm -f $(OBJ) $(BIN)

这里通过ifeq条件判断语句,根据DEBUG变量的值来调整CFLAGS编译选项。当DEBUG为 1 时,添加-g选项开启调试信息;当DEBUG不为 1 时,添加-O3选项进行优化编译。这样可以方便地在调试和发布阶段切换不同的编译配置。

四、进阶技巧探索

4.1 多平台支持

在实际开发中,我们的项目可能需要在不同的平台上进行编译和运行,如 Linux、macOS 和 Windows 等。通过 Makefile 的条件编译功能,可以轻松实现多平台的支持。

首先,我们可以利用$(shell uname -s)命令获取当前系统的名称。在 Makefile 中,可以这样使用:

OS := $(shell uname -s)

然后,通过ifeq条件判断语句,根据不同的操作系统设置相应的编译选项。例如:

ifeq ($(OS), Linux)CFLAGS += -D LINUXLDFLAGS += -lrt  # Linux 实时库
else ifeq ($(OS), Darwin)CFLAGS += -D MACOSLDFLAGS += -framework CoreFoundation  # macOS 框架示例
else ifeq ($(OS), Windows_NT)CFLAGS += -D WINDOWSCC = gcc  # 假设使用 MinGW 等 GCC 编译器LDFLAGS += -L/path/to/winlibs -lmingw32 -lSDL2main -lSDL2  # Windows 库路径和链接库示例
endif

在上述代码中,根据$(OS)的值,分别为不同的操作系统设置了特定的编译选项和链接选项。当$(OS)为Linux时,添加-D LINUX编译宏,用于在代码中区分不同平台的代码逻辑,同时添加-lrt链接选项,链接 Linux 的实时库;当$(OS)为Darwin(即 macOS)时,添加-D MACOS编译宏和-framework CoreFoundation链接选项,链接 macOS 的 CoreFoundation 框架;当$(OS)为Windows_NT时,添加-D WINDOWS编译宏,指定编译器为gcc(假设使用 MinGW 等 GCC 编译器),并设置相应的库路径和链接库。

这样,在不同的平台上执行make命令时,Makefile 会根据当前系统自动选择合适的编译和链接选项,从而实现多平台的支持。

4.2 第三方库集成

在项目开发中,常常会用到第三方库来提高开发效率和实现特定功能。在 Makefile 中集成第三方库主要涉及静态链接和动态链接两种方式。

静态链接
静态链接是将第三方库的代码直接链接到可执行文件中,生成的可执行文件较大,但运行时不需要依赖外部库文件。以使用 Boost 库为例,假设 Boost 库的安装路径为/usr/local/boost。首先,需要在 Makefile 中设置库路径和库名:

LIBS += -L/usr/local/boost/lib -lboost_system

这里,-L/usr/local/boost/lib指定了 Boost 库的路径,-lboost_system表示链接boost_system库。然后,在链接目标的规则中使用这些设置:

$(BIN): $(OBJ)$(CC) $(CFLAGS) $(LDFLAGS) -I/usr/local/boost/include -o $@ $^ $(LIBS)

在这个规则中,-I/usr/local/boost/include指定了 Boost 库的头文件路径,确保在编译时能够找到库的头文件。

动态链接
动态链接是在运行时才加载第三方库,生成的可执行文件较小,但运行时需要依赖外部库文件。动态链接除了设置库路径和库名外,还需要设置运行时库路径。例如,假设第三方库位于项目的third_party/lib目录下:

LDLIBS += -Wl,-rpath=$(PWD)/third_party/lib -L$(PWD)/third_party/lib -lmythirdlib

这里,-Wl,-rpath=$(PWD)/third_party/lib设置了运行时库路径,$(PWD)表示当前工作目录;-L$(PWD)/third_party/lib指定了库路径;-lmythirdlib表示链接mythirdlib库。同样,在链接目标的规则中使用这些设置:

$(BIN): $(OBJ)$(CC) $(CFLAGS) $(LDFLAGS) -I$(PWD)/third_party/include -o $@ $^ $(LDLIBS)

这样,在运行可执行文件时,系统会到指定的运行时库路径中查找并加载动态库。

4.3 版本控制与清理

在项目开发过程中,版本控制和清理是非常重要的环节。Makefile 在这方面提供了强大的支持,能够实现增量构建和彻底清理。

增量构建
Makefile 通过文件的时间戳来实现增量构建。当执行make命令时,Makefile 会检查目标文件及其依赖文件的时间戳。如果依赖文件的修改时间比目标文件新,说明依赖文件发生了变化,Makefile 会自动重新构建目标文件;如果依赖文件没有变化,Makefile 会认为目标文件已经是最新的,不会重新构建。这种机制大大提高了编译效率,尤其是在大型项目中,避免了不必要的重复编译。例如,在之前的MathLibrary项目中,当src/add.cpp文件被修改后,执行make命令,Makefile 会检测到src/add.o的依赖文件src/add.cpp发生了变化,于是会重新编译src/add.cpp生成src/add.o,然后再根据math_app的依赖关系,重新链接生成math_app。而对于没有修改的src/sub.cpp、src/mul.cpp等文件,其对应的目标文件src/sub.o、src/mul.o不会被重新编译。

彻底清理
在项目开发中,有时需要彻底删除编译生成的文件,以重新开始构建或者清理工作目录。Makefile 中通常会定义clean目标来实现基本的清理操作,删除编译生成的目标文件和可执行文件。例如:

clean:rm -f $(OBJ) $(BIN)

而对于更彻底的清理,比如删除配置文件等,可以定义distclean目标。但并不是所有的 Makefile 都支持distclean目标,这取决于 Makefile 的编写者是否提供了该目标。如果 Makefile 支持distclean,运行make distclean即可;如果不支持,可以在clean目标的基础上,手动删除其他已知的构建产物,如由配置脚本生成的配置文件、自动工具生成的文件等。对于使用版本控制工具(如 Git)的项目,还可以使用git clean -xdf命令来删除所有未被跟踪的文件和目录,包括.gitignore中忽略的文件。在MathLibrary项目中,如果想要彻底清理,除了执行make clean删除目标文件和可执行文件外,还可以手动检查并删除可能存在的其他构建产物,如编译过程中生成的临时文件等。如果项目使用了自动工具(如 autoconf、automake),还可以运行make maintainer-clean来删除所有自动生成的文件。

五、总结与展望

Makefile 作为自动化编译的利器,在软件开发流程中扮演着至关重要的角色。通过定义清晰的规则和灵活的变量,它能够高效地管理项目的编译过程,实现从源文件到可执行文件的快速构建。从基础的变量定义、模式规则到伪目标的使用,再到实战项目中的多平台支持、第三方库集成以及版本控制与清理,Makefile 展现出了强大的功能和高度的可定制性。

在实际项目开发中,Makefile 不仅能够显著提高编译效率,减少手动编译的繁琐和错误,还能方便地管理项目的构建流程,使开发过程更加规范化和系统化。无论是小型项目还是大型工程,掌握 Makefile 的使用都能为开发者带来诸多便利。

展望未来,随着软件开发技术的不断发展和项目规模的日益庞大,Makefile 在自动化编译领域的重要性将愈发凸显。希望读者能够将本文所学应用到实际项目中,不断探索和实践 Makefile 的更多高级技巧和应用场景,让 Makefile 成为提升软件开发效率的得力助手。同时,也期待大家在使用 Makefile 的过程中,不断总结经验,分享心得,共同推动软件开发技术的进步。

http://www.dtcms.com/a/593982.html

相关文章:

  • 广州网站推广教程中国建设银行网站会员用户名
  • 怎么做分享软件的网站php网站开发框架搭建
  • 网站跟网页的区别jsp做的网页是网站吗
  • 根据docker服务保存日志脚本,时间可选版本
  • 九、神经网络的构建方式详解
  • 第五章 神经网络
  • 网站建设相关的工作鸿顺里网站建设公司
  • 自己做网站卖手机网站建设电话
  • TypeScript 元组
  • LLM - 大模型融合 LangChain 与 OpenRouter 的框架
  • 南宁建企业网站公司办公室装修设计怎么收费
  • 天气形势时间层的选择策略
  • 一站式网站建设多少钱网站怎么会k
  • JAVA实现国密算法SM2/SM3/SM4签名与验签(基于 BouncyCastle)
  • 专门做继电器的网站如何用源码建站
  • ZSAR报错解决
  • CE(Linux的例行性工作)
  • Django中的clean()方法和full_clean()方法
  • 外贸网站怎么注册商城网站开发视频
  • GIT修改用户名
  • 国内最大的摄影网站wordpress大图简约主题
  • hf中transformers库中generate的greedy_search
  • 网站建设优化安徽自己设计logo的软件
  • GetMapping自动截取List<String>字符
  • 以太网环境传感器在物联网系统中的集成与应用前景
  • windows系统启动redis报错
  • 一键部署禅道开源版
  • 【IoT开发选型】乐鑫ESP32-C6核心优势解析:为何它在无线连接中表现如此全面?
  • 在线开发培训网站建设厨师培训机构
  • Java Lambda表达式与函数式编程指南