《掌握工程管理工具:Makefile与CMake的静态库、动态库开发技巧》
本文介绍了在Linux环境下使用Makefile和CMake管理C语言项目的流程,包括静态库和动态库的封装与调用。首先讲解了C程序的编译过程(预处理、编译、汇编、链接),然后详细说明Makefile的编写规则和通配符用法,以及CMake的跨平台构建方法。文章还演示了如何创建静态库(.a)和动态库(.so),并说明动态库需要设置环境变量才能运行。最后建议利用AI工具辅助编写复杂的构建脚本,提高大型项
文章说明
本文将讲述在基于Linux环境下的c语言编程中,Makefile脚本与cmake脚本的安装、配置以及使用。静态库与动态库的封装和调用方法。目标是帮助读者能使用make与cmake来管理自己的工程项目以及在多人协作开发中的提升跨平台兼容性与减少重复代码,提升协作效率。
文章顺带说明编译的过程(预处理--编译--汇编--链接)方便读者更好的理解脚本工具的逻辑与作用。
注:本文使用vscode ssh连接了Ubuntu系统进行演示,所以界面与虚拟机中的界面不同。如需学习如何使用ssh连接,可以回顾笔者上一篇笔记《Linux系统共享工具、服务器》中ssh部分。
编译过程的了解:
在学习脚本管理开发工具之前我们应该先了解一下c语言的编译过程。
1.源文件(.c .cpp)与目标可执行文件
我们编写的源文件在机器眼里是毫无可读性与逻辑的文字,所以源文件需要经历以下步骤来变成机器可读的通过ASCII码组成的二进制文件。

2.编译过程
编译需要经历以下过程

a.预处理 (C Preprocessor)
C++预处理是在编译前对源代码进行文本替换和处理的阶段,主要包括宏展开、条件编译和文件包含等操作。
演示通过该命令将main.c 变成main.i并查看效果
gcc main.c -o main.i -E
main.c 源文件

生成main.i文件,我们可以看到该文件是对我们引用的头文件<stdio.h>(宏、条件编译---此处没有定义所以没有体现)进行展开与链接进来,这里是部分截图,可以看到右侧的代码量与.c 比翻了好几倍(ps :在.i 文件的末尾依然可以看到我们的源代码)


b.编译 ( C Compiler 1)
CC1编译是将C语言源代码通过GCC编译器前端(cc1程序)转换为低级中间表示(GIL)的过程,完成语法分析、语义检查及初步优化------ 生成汇编语言
注:汇编语言与处理器体系结构有关
演示通过该命令将main.i 变成main.s并查看效果
gcc main.i -o main.s -S
汇编语言:

c.汇编 ( Assembler)
汇编器(assembler)是将汇编语言转换为机器码的程序。
演示通过该命令将main.s 变成main.o并查看效果
gcc main.s -o main.o -c
生成目标二进制文件

d.链接 (Loader)
链接器(linker)是将多个目标文件合并为一个可执行文件的程序。
演示通过该命令将main.o (这里只有一个文件不太好体现)链接在一起 形成可执行文件main
注: 通常工程文件有大量.c文件(模块化实现各种功能)在项目整合到最终调用界面main时需要将他们的.o文件 一起链接于mian.o 中形成最后的main目标文件
gcc main.o -o main // (如果工程里有多个源文件,则是多个.o文件一起链接编译在工程中)
执行main

注:编译过程中生成中间文件的时候注意大小写区别 -E -S -c
现在回到我们的Makefile
一、Makefile脚本
作用:
Makefile 是一种用于自动化构建和管理项目的工具,主要用于编译和链接源代码文件。它通过定义规则和依赖关系,帮助开发者高效地管理复杂的构建过程。通过 Makefile,开发者只需输入简单的命令(如 make 或 make clean),即可执行复杂的构建或清理操作,无需手动输入冗长的编译命令。

安装:
终端命令1:sudo apt update #更新库,确保下载的是最新的包
终端命令2:sudo apt install make #安装make
终端命令3:make --version #检查make版本
执行该命令后如果成功可以看到自己的版本。(如下是笔者的版本)

使用方法:
首先我们要了解Makefile脚本语法的三大要素
Makefile 的核心由三个关键要素构成:目标(Target)、依赖(Prerequisites) 和 命令(Commands)。这些要素共同定义了构建规则,指导 make 工具如何编译和链接程序。
# 基本构成语法
目标:依赖
命令
# !!!注意 命令前面一定要用Tab分隔符,不能使用空格
注:一定要用分隔符,不能使用空格
Makefile语法格式示例
笔者新建了一个show_makefile文件夹并创建编写了func.h、func.c、main.c 进行简单的Makefile演示

编写一个Makefile文件
main:main.o func.o # 生成链接 main目标,需要链接main.o与func.o这两个依赖
gcc main.o func.o -o main # 命令
main.o:main.c # 生成编译main.o目标,需要链接main.c这个依赖
gcc -c main.c -o main.o # 命令
func.o:func.c # 生成编译func.o目标,需要链接func.c这个依赖
gcc -c func.c -o func.o # 命令
clean: #这种自定执行指令算伪目标 (此功能无需依赖)
rm -rf main #命令
在makefile目录路径终端中输入 make 指令
注:阅读以下指令时建议先观看前文提及的编译过程(源文件--编译--->.o---链接-->目标文件)这一核心

这样我们就得到了可执行文件main(相当于脚本自动化执行了gcc func.c mian.c -o main这个指令)
这里看看makefile脚本化高效的另一个地方
Makefile 通过比较目标文件和依赖文件的时间戳(timestamp)来决定是否需要重新构建目标。时间戳记录了文件的最后修改时间,Makefile 利用这一机制实现增量编译,避免重复构建未变化的文件。
(笔者修改了main文件内容,其他保持不变并进行编译)
可以发现没有修改过func.c没有被重复编译,这样可以在超大量工程文件管理,修改部分功能时时减少编译时间。
当然Makefile的强大之处在于它的通配写法,通过定义变量与通配符 无需手动输入入func,c main.c func.o等依赖(适用于大型项目过程文件多)-------因为篇幅问题笔者仅演示提供学习建议。
通配写法----------例子
通过指定路径与定义变量名实现自动化
CC = gcc # 编译器的版本
ELF = ./Executable/main # 生成的可执行文件的位置
SRCS = $(wildcard ./Source/*.c) # 获取指定模式的文件列表,代表Source目录下的所有.c文件
OBJS = $(patsubst ./Source/%.c, ./Build/%.o, $(SRCS)) # 字符串替换, 将Source目录下的所有.c,替换成./Build/*.o文件
CFLAGS = -I ./Include -L ./Library -lpthread -Wall -g -O1
# -I ./Include:指定头文件的路径,在当前Include文件夹下
# -L ./Library:指定库文件的路径,在当前Library文件夹下(动静态库)
# -lpthread:链接线程库(系统编程)
# -Wall:只要有一点不符合C语言程序规范,编译器都将其指出来
# -g:输出调试信息(GDB调试)
# -O1:优化等级
# 生成可执行文件
all:$(ELF) # 最终目标,约定俗成的,告诉你最终生成的是$(ELF) ->./Executable/main
$(ELF):$(OBJS) # 你要生成$(ELF),需要依赖$(OBJS) ->Build目录下所有.o(Source下的.c文件生成的)
@mkdir -p $(@D) # 当前目录没有Executable目录,就创建
$(CC) $(OBJS) -o $(ELF) $(CFLAGS) # 相当于:gcc ./Build/*.o ./main.c -o ./Executable/main -I ./Include -L ./Library -lpthread -Wall -g -O1
# 通配规则:将一个个.c文件编译为一个个.o文件
./Build/%.o:./Source/%.c # 将Source目录下一个个.c文件编译为Build目录下一个个.o文件
@mkdir -p $(@D) # 当前目录没有Build目录,就创建
$(CC) -c $< -o $@ $(CFLAGS) # 相当于:gcc -c Source目录下一个个.c文件 -o Build目录下一个个.o文件 -I ./Include -L ./Library -lpthread -Wall -g -O1
# 注意:上个语句,有几个.c文件就执行多少次上面的语句
# 清理构建产物的伪目标
.PHONY: clean run help #避免make将目标视为同名文件,强制执行命令
clean:
rm -rf ./Build ./Executable
run:
./Executable/main
help:
@echo "1、环境:ubuntu22.04"
@echo "2、编译:去到Makefile在地方,执行make命令即可编译"
@echo "3、执行:make run"
笔者建议利用ai人工智能降低学习成本。以低学习成本做到:能读懂协作者的makefile脚本,能使用makefile脚本,管理自己的工程项目
通配写法学习建议
1.先使用tree指令生成对应的文件目录结构(以笔者的学生信息管理系统项目为例)

注如果没有tree指令可以执行以下指令下载tree包
sudo apt update
sudo apt upgrade # 更新apt
sudo apt-get install tree #下载tree
打开ai智能体粘贴目录结构,明确的表达你的需求(以deep seek为例)

根据ai给的makefile 顺利执行

注 :如遇到问题报错复制报错信息给ai让其重新生成
二、cmake
作用:
CMake 是一个跨平台的构建系统生成工具,用于自动化管理软件构建过程。它通过编写平台无关的配置文件(CMakeLists.txt)生成目标平台所需的构建文件(如 Makefile、Visual Studio 项目文件等)。ps :能自动生成makefile
安装:
sudo apt update #更新库,确保下载的是最新的包
sudo apt install build-essential #安装GNU的环境
sudo apt install cmake #安装cmake
cmake --version #检查cmake版本
安装成功后可以看到自己的版本号(如笔者这里是3.22.1.)

使用方法:
步骤:1.编写CMakeLists.txt文件 2.执行cmake命令 3.执行make命令
语法:
# 指定CMake的最低版本要求
cmake_minimum_required() # 如cmake_minimum_required(VERSION 3.10)
# 定义项目名称
project() # 名称自拟,如project(MyProject)
# 添加一个可执行目标
# 语法: add_executable(目标名 源文件1 源文件2...)
# add_executable(main main.c) #写法1:可以自己定义生成的可执行文件
add_executable(${PROJECT_NAME} main.c) #写法2:生成的可执行文件和项目名称一致
使用cmake首先我们要先构建一个文件夹,并切换到文件夹目录内使用cmake指令
mkdir build # 创建目录(可以自己定义名称,这里笔者选为build)
cd build # cd指令进入build文件夹
# 在build目录下使用cmake指令 “..表示上一级目录让cmake去找工程文件”
cmake ..
演示:


注:本质是cmake根据你指定的工程文件,为你生成Makefile 所以还需要make来生成可执行目标文件。(创建文件夹是为了避免生成文件影响工程目录结构-----找个地方装着)
顺带一提cmake也有通配写法,学习建议和Makefile一样,对于大型工程的管理很有用(用AI帮助通配符的理解、以及生成你的脚本文件),这里展示一下例子大家可以感受一下。
# 指定CMake的最低版本
cmake_minimum_required(VERSION 3.10)
# 定义项目名称
project(MyProject)
# 设置编译工具链
set(CMAKE_C_COMPILE gcc)
set(CMAKE_CXX_COMPILE g++)
# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 将所有的源文件添加到一个可执行目标中
# add_executable($(PROJECT_NAME) test1.c test2.c main.c)
# 也可以使用变量,更加清晰
set(SOURCES test1.c test2.c main.c)
# add_executable(main ${SOURCES}) #写法1:可以自己定义生成的可执行文件
add_executable(${PROJECT_NAME} ${SOURCES}) #写法2:生成的可执行文件和项目名称一致
三、静态库
作用:
静态库(Static Library)是一种预编译的二进制文件,包含多个目标文件的集合,用于在程序编译时直接链接到可执行文件中。它的主要作用是提供代码复用和模块化管理,将常用的功能封装成库,避免重复编译相同代码,提高开发效率。静态库在链接阶段会被完整地合并到最终的可执行文件中,因此生成的程序独立运行,无需依赖外部库文件,但会增加可执行文件的体积。常见于对性能要求高或需要单文件部署的场景。(ps :可以把你的源文件.c打包为静态库发给被人,这样别人可以调用你的模块功能但看不见你的源程序.c是怎么是实现的,一定程度上可以保护知识产权)
使用方法:
笔者依旧使用这几个项目文件进行演示(想看具体文件内容,请跳转到Makefile脚本的使用方法介绍处)

笔者这里将func.c封装为自己的静态库进行演示
1.首先我们要先将需要封装的源文件编译为汇编程序(.o)
gcc -c func.c -o func.o

2.创建静态库
ar rcs libfunc.a func.o
# 说明:静态库文件名字通常以"lib"开头,以".a"结尾
# 这里创建了一个名为func的库文件 依赖为func.o

可以看到已经这个libfunc.a就是我们的静态库
3.链接编译
这里我们可以把func.c移到别的文件夹或者删除,保留头文件(.h)测试编译main.c是否还需要依赖于func.c。
gcc main.c -o main -L ./ -lfunc
# 后面的是小L lxxx xxx代表你的静态库名字,笔者的静态库是func
# -L 后面接你的库文件(.a)的路径,请根据实际路径修改

可以看到当前目录下已经没有func.c了但是通过链接静态库还是可以调用func.c的函数功能。
注:记得保留func.c的头文件func.h(通常需要在头文件中添加使用注释,方便协作者调用你的函数功能)
如果感兴趣的伙伴还可以学习一下将静态库与Makefile结合(通配写法)实现脚本管理自动化。
四、动态库
作用:
动态库的作用在于提供代码的模块化共享,允许程序在运行时加载所需功能,减少内存占用并支持灵活更新。(比静态库灵活)
使用方法:

1.首先我们要先生成位置无关目标文件(.o)
注:比静态库多了个-fPIC(Fully Position Independent Code(完全位置无关代码))
解释:fPIC是一种编译技术,主要用于共享库(动态链接库,如.so文件)的生成,确保代码在内存中的任意位置加载时都能正确运行,无需重定位修改。
gcc -c -fPIC func.c -o func.o
2.看到func.o文件生成后,就可以创建动态库(.so)
gcc -shared -o libfunc.so func.o
# 后缀的格式和静态库不同(.a 与 .so) lib xxx .so ,xxx为你的动态库名称
# func.o 为依赖

3.链接编译
gcc main.c -o main -L ./ -lfunc
# 格式和链接静态库一样

现在你的可执行目标文件main 知道执行需要找libfunc.so就可以了,我们可以测试一下把libfunc.so的路径放入临时系统环境中。而把main移到其他路径下看看能不能顺利执行。
将动态库放入环境变量前:main不能执行

将动态库临时放入系统环境变量中(重启后失效)----- 永久挂载需要修改系统配置文件
export LD_LIBRARY_PATH=/mnt/hgfs/share/show_makefile:$LD_LIBRARY_PATH
# 将库文件所在的文件目录路径放入系统变量中(临时)
PATH = xxx xxx为你的动态库所在文件夹的绝对路径

可以看到修改后可以执行main了
更多推荐




所有评论(0)