CPP-Summit-2020 学习:C/C++ 之构建设计与重构
一、什么是 C/C++ 构建(Build)?
构建(Build) = 从源代码到可执行目标的加工过程
简单理解:
源代码(.c/.cpp) ↓ 构建工具 (gcc / clang / cmake / make) ↓ 目标产物 (可执行文件 / 静态库 / 动态库) 二、构建的本质
构建本质是一个“转换函数”:
设:
- 源代码集合为 S S S
- 构建工具为 B B B
- 输出目标为 T T T
那么构建过程可以抽象为:
T = B ( S ) T = B(S) T=B(S)
其中: - S = f i l e 1 . c p p , f i l e 2 . c p p , . . . S = {file_1.cpp, file_2.cpp, ...} S=file1.cpp,file2.cpp,...
- T T T 可以是:
- 可执行文件
- 静态库
- 动态库
三、C/C++ 构建完整流程
C/C++ 构建通常分为 3 个阶段:
预处理 → 编译 → 链接 1⃣ 预处理(Precompile / Preprocess)
输入:
main.cpp 处理内容:
- 处理
#include - 展开
#define - 条件编译
#ifdef
例如:
#include<iostream>#definePI3.14intmain(){ std::cout << PI << std::endl;}预处理后大概变成:
// iostream 内容被展开(非常巨大)intmain(){ std::cout <<3.14<< std::endl;}生成文件:
main.i (中间文件) 2⃣ 编译(Compile)
把预处理后的代码转换为汇编或机器码。
数学理解:
机器码 = 编译器 ( 预处理结果 ) 机器码 = 编译器(预处理结果) 机器码=编译器(预处理结果)
生成文件:
main.o (目标文件 object file) 示例:
g++ -c main.cpp 解释:
-c表示只编译,不链接- 输出
.o文件
3⃣ 链接(Link)
多个 .o 文件合并,解析函数引用。
例如:
main.o util.o 链接后:
a.out 或 main.exe 命令:
g++ main.o util.o -o app 四、目标类型
构建的结果可以有 3 种:
1⃣ 可执行文件(Executable)
app.exe a.out 可以直接运行:
./app 2⃣ 静态库(Static Library)
文件:
libxxx.a (Linux) xxx.lib (Windows) 生成方式:
ar rcs libmath.a math.o 特点:
- 编译时直接拷贝进可执行文件
- 文件体积变大
- 运行时不依赖外部库
3⃣ 动态库(Shared Library)
文件:
libxxx.so (Linux) xxx.dll (Windows) 生成方式:
g++ -shared -fPIC math.cpp -o libmath.so 特点:
- 运行时加载
- 可执行文件更小
- 可以共享
五、完整构建流程图
源代码 main.cpp util.cpp │ ▼ 预处理阶段 (展开宏和头文件) │ ▼ 编译阶段 main.o util.o │ ▼ 链接阶段 │ ▼ 最终产物 app.exe / lib.so 六、用代码解释“构建是什么”
我们写一个简单工程:
main.cpp
// main.cpp#include<iostream>#include"add.h"intmain(){// 调用 add 函数 std::cout <<add(3,5)<< std::endl;}add.h
// add.h#pragmaonce// 声明函数intadd(int a,int b);add.cpp
// add.cpp#include"add.h"// 定义函数intadd(int a,int b){return a + b;}七、构建步骤
第一步:编译
g++ -c main.cpp g++ -c add.cpp 生成:
main.o add.o 第二步:链接
g++ main.o add.o -o app 生成:
app 八、Makefile 构建示例(带详细注释)
# 指定编译器 CXX = g++ # 编译选项 CXXFLAGS = -Wall -std=c++17 # 目标文件 TARGET = app # 所有源文件 SRCS = main.cpp add.cpp # 自动生成 .o 文件列表 OBJS = $(SRCS:.cpp=.o) # 默认目标 all: $(TARGET) # 链接阶段 $(TARGET): $(OBJS) $(CXX) $(OBJS) -o $(TARGET) # 编译阶段 %.o: %.cpp $(CXX) $(CXXFLAGS) -c $< -o $@ # 清理 clean: rm -f $(OBJS) $(TARGET) 九、构建工具是什么?
常见构建工具:
- GNU Make
- CMake
- Ninja
- Bazel
它们做什么?
本质是:
构建系统 = 依赖分析 + 自动化调用编译器 构建系统 = 依赖分析 + 自动化调用编译器 构建系统=依赖分析+自动化调用编译器
十、依赖关系的数学理解
假设:
- 文件 A 依赖 B
- 文件 B 依赖 C
则依赖图为:
A → B → C A \to B \to C A→B→C
构建顺序必须满足:
C → B → A C \rightarrow B \rightarrow A C→B→A
否则会链接失败。
十一、总结(核心理解)
C/C++ 构建是什么?
易构构建度OS + HardwareC/C++ srcgcc clangmsvcBinary targetRust srccargoBinary targetJava srcJdkJarJreRuby srcGemRuby构建工具标准化引入IR层(class)源码运行
一句话:
把人类可读的源代码,转换为机器可执行文件的自动化加工过程。
构建包含:
| 阶段 | 做什么 |
|---|---|
| 预处理 | 展开宏和头文件 |
| 编译 | 变成机器码 |
| 链接 | 解析符号引用 |
| 输出 | exe / 静态库 / 动态库 |
本质公式
目标文件 = 编译器 ( 源代码 ) 目标文件 = 编译器(源代码) 目标文件=编译器(源代码)
可执行文件 = 链接器 ( 多个目标文件 ) 可执行文件 = 链接器(多个目标文件) 可执行文件=链接器(多个目标文件)
一、整体对比的核心思想
你的图表达的是一个趋势:
从左到右,构建复杂度逐渐降低
根本原因只有一句话:
语言离硬件越近,构建越复杂 语言离硬件越近,构建越复杂 语言离硬件越近,构建越复杂
二、C/C++ 构建模型
图中左侧:
C/C++ src ↓ gcc / clang / msvc ↓ Binary target ↓ OS + Hardware 本质结构
C/C++:
B i n a r y = C o m p i l e r ( S o u r c e ) Binary = Compiler(Source) Binary=Compiler(Source)
并且:
B i n a r y ≈ M a c h i n e C o d e Binary \approx MachineCode Binary≈MachineCode
也就是说:
编译结果直接是机器指令
示例:C++ 构建
main.cpp
#include<iostream>// main 函数是程序入口intmain(){ std::cout <<"Hello\n";return0;}构建命令:
g++ main.cpp -o app 流程实际是:
预处理 → 编译 → 汇编 → 链接 最终得到:
ELF / PE 可执行文件 这个文件:
- 直接包含机器码
- 依赖操作系统 ABI
- 依赖 CPU 架构
例如: - x86_64
- ARM64
https://godbolt.org/z/EWEqnh6qM
.LC0:.string "Hello\n" main: push rbp mov rbp, rsp mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:std::cout call std::basic_ostream<char, std::char_traits<char>>& std::operator<<<std::char_traits<char>>(std::basic_ostream<char, std::char_traits<char>>&,charconst*) mov eax,0 pop rbp ret C/C++ 构建的特点
1⃣ 没有统一构建系统
2⃣ 编译器众多(gcc / clang / msvc)
3⃣ ABI 不统一
4⃣ 直接面对 OS + 硬件
所以构建复杂度高。
三、Rust 构建模型
图中第二列:
Rust src ↓ cargo ↓ Binary target 核心差异
Rust 自带:
- 官方编译器 rustc
- 官方构建系统 cargo
- 官方包管理 crates.io
也就是说:
B u i l d S y s t e m ∈ L a n g u a g e E c o s y s t e m BuildSystem \in Language Ecosystem BuildSystem∈LanguageEcosystem
示例:Rust 构建
创建项目:
cargo new hello 运行:
cargo run 源码:
// main.rsfnmain(){println!("Hello\n");}Cargo 自动完成:
- 依赖下载
- 编译
- 链接
- 运行
数学抽象
C++:
B i n a r y = L i n k ( C o m p i l e ( P r e p r o c e s s ( S 1 ) ) , . . . , C o m p i l e ( S n ) ) Binary = Link(Compile(Preprocess(S_1)), ..., Compile(S_n)) Binary=Link(Compile(Preprocess(S1)),...,Compile(Sn))
Rust:
B i n a r y = C a r g o ( S ) Binary = Cargo(S) Binary=Cargo(S)
因为 Cargo 内部封装了所有步骤。
https://godbolt.org/z/MKsnEca9W
core::fmt::Arguments::from_str::h67f06e6cf63be818: mov rax, rdi mov qword ptr [rsp -16], rax mov qword ptr [rsp -8], rsi lea rdx,[rsi + rsi +1] ret example::main::h503294ee80f72bf1: push rax lea rdi,[rip +.Lanon.878600290a216ce05976eac0f0d7180e.0] mov esi,14 call core::fmt::Arguments::from_str::h67f06e6cf63be818 mov rdi, rax mov rsi, rdx call qword ptr [rip + std::io::stdio::_print::h526c462071e58c18@GOTPCREL] pop rax ret .Lanon.878600290a216ce05976eac0f0d7180e.0:.ascii "Hello, world!\n"四、Java 构建模型
图中第三列:
Java src ↓ JDK ↓ Jar ↓ JRE 核心差异:引入 IR 层
Java 不直接生成机器码,而是生成:
字节码(Bytecode)
也叫中间表示 IR。
数学表示:
B y t e c o d e = C o m p i l e r ( S o u r c e ) Bytecode = Compiler(Source) Bytecode=Compiler(Source)
M a c h i n e C o d e = J V M ( B y t e c o d e ) MachineCode = JVM(Bytecode) MachineCode=JVM(Bytecode)
也就是:
M a c h i n e C o d e = J V M ( C o m p i l e r ( S o u r c e ) ) MachineCode = JVM(Compiler(Source)) MachineCode=JVM(Compiler(Source))
示例:Java 构建
Hello.java:
// Hello.javapublicclassHello{publicstaticvoidmain(String[] args){System.out.println("Hello");}}编译:
javac Hello.java 生成:
Hello.class 运行:
java Hello JVM 负责:
- 加载 class
- JIT 编译
- 执行
Java 的构建优势
1⃣ 不依赖 CPU 架构
2⃣ 统一虚拟机
3⃣ 一次编译,到处运行
即:
P o r t a b l e = I R + V M Portable = IR + VM Portable=IR+VM
https://godbolt.org/z/Kxf8q3qdG
classHello{Hello();0: aload_0 1: invokespecial #1// Method java/lang/Object."<init>":()V4:returnpublicstaticvoidmain(java.lang.String[]);0: getstatic #7// Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #13// String Hello5: invokevirtual #15// Method java/io/PrintStream.println:(Ljava/lang/String;)V8:return}五、Ruby 构建模型
图中最右:
Ruby src ↓ Gem ↓ Ruby 解释器 Ruby 是解释型语言。
数学模型:
E x e c u t i o n = I n t e r p r e t e r ( S o u r c e ) Execution = Interpreter(Source) Execution=Interpreter(Source)
没有独立编译步骤。
示例:Ruby
# hello.rb puts "Hello"运行:
ruby hello.rb 解释器直接执行源码。
https://godbolt.org/z/nhYa6ET8c
六、本质差异总结(核心)
我们可以用一张表总结:
| 语言 | 是否有 IR | 是否需要链接 | 是否依赖硬件 |
|---|---|---|---|
| C/C++ | ✗ 无 | ✓ 需要 | ✓ 强依赖 |
| Rust | ✗ 无 | ✓ 需要 | ✓ 依赖 |
| Java | ✓ 有 | ✗ 无 | ✗ 不依赖 |
| Ruby | ✗ 无 | ✗ 无 | ✗ 不依赖 |
七、构建复杂度的数学抽象
定义:
- H H H = 与硬件耦合程度
- S S S = 构建步骤数量
- I I I = 是否存在 IR 层
构建复杂度可以抽象为:
C o m p l e x i t y ∝ H + S − I Complexity \propto H + S - I Complexity∝H+S−I
解释: - C++: H H H 大, S S S 多, I = 0 I=0 I=0 → 复杂
- Rust: H H H 大,但工具统一 → 中等
- Java: H H H 小, I = 1 I=1 I=1 → 简化
- Ruby: H H H 很小 → 极简
八、为什么 C++ 构建难?
根本原因:
C + + = 系统级语言 C++ = 系统级语言 C++=系统级语言
它的设计目标是:
- 操作系统
- 驱动
- 高性能计算
而不是: - 快速部署
- 跨平台虚拟机
九、构建抽象层级对比
我们从“离硬件距离”看:
Ruby ↑ Java (IR + VM) ↑ Rust (统一工具链) ↑ C/C++ ↓ OS + Hardware 离硬件越近:
- 性能越高
- 构建越复杂
- 需要处理 ABI / 链接 / 架构
十、最终总结(你图的真正含义)
你的图想表达的是:
C/C++ 构建难的根源不是语言语法
而是:
它直接面对操作系统和硬件
而 Java / Ruby 通过:
- 引入 IR
- 引入虚拟机
- 标准化构建工具
把复杂性“转移”到了运行时。
一句话终极总结
C / C + + = 直接生成机器码 C/C++ = 直接生成机器码 C/C++=直接生成机器码
J a v a = 生成中间码 + 虚拟机解释 Java = 生成中间码 + 虚拟机解释 Java=生成中间码+虚拟机解释
R u b y = 解释器直接执行源码 Ruby = 解释器直接执行源码 Ruby=解释器直接执行源码
一、C/C++ 构建复杂性的本质
一句话:
C / C + + 构建复杂性 = 预处理复杂性 + 链接复杂性 + 依赖复杂性 C/C++ 构建复杂性 = 预处理复杂性 + 链接复杂性 + 依赖复杂性 C/C++构建复杂性=预处理复杂性+链接复杂性+依赖复杂性
也可以抽象为:
B u i l d = C o m p i l e ( P r e p r o c e s s ( S o u r c e , M a c r o , I n c l u d e P a t h ) ) + L i n k ( O b j e c t , L i b , P a t h ) Build = Compile(Preprocess(Source, Macro, IncludePath)) + Link(Object, Lib, Path) Build=Compile(Preprocess(Source,Macro,IncludePath))+Link(Object,Lib,Path)
二、一个典型复杂命令
gcc dd.c \ -Dccc_feature=1\ -I/usr/local/bbb/include \ -L/usr/local/bbb/lib \ -laaa \ -O2 \ -Wall 我们逐项解释。
三、条件宏(-D)
1⃣ 宏定义
-Dccc_feature=1等价于源码顶部:
#defineccc_feature1示例代码
#include<stdio.h>intmain(){#ifdefccc_featureprintf("Feature enabled\n");#elseprintf("Feature disabled\n");#endifreturn0;}如果加:
gcc main.c -Dccc_feature 输出:
Feature enabled 宏本质
宏发生在:
预处理阶段
数学表达:
S o u r c e ′ = P r e p r o c e s s ( S o u r c e , M a c r o S e t ) Source' = Preprocess(Source, MacroSet) Source′=Preprocess(Source,MacroSet)
宏使得:
编译结果 = f ( 宏集合 ) 编译结果 = f(宏集合) 编译结果=f(宏集合)
这意味着:
构建结果依赖编译参数
而不是只依赖源码。
这就引入了“隐式构建依赖”。
四、头文件搜索路径(-I)
-I/usr/local/bbb/include 表示:
在该路径下查找头文件
示例
#include"aaa.h"编译器查找顺序:
- 当前目录
- -I 指定目录
- 系统目录
搜索路径优先级
如果两个目录都有:
aaa.h 那么:
I n c l u d e R e s u l t = F i r s t M a t c h ( S e a r c h P a t h O r d e r ) IncludeResult = FirstMatch(SearchPathOrder) IncludeResult=FirstMatch(SearchPathOrder)
这意味着:
构建结果依赖路径顺序
这就是 C/C++ 构建“隐式耦合”的来源之一。
五、静态库(-lxxx)
-laaa 表示链接:
libaaa.a 静态库生成
ar rcs libaaa.a a.o b.o 静态链接过程
E x e c u t a b l e = L i n k ( O b j e c t F i l e s + S t a t i c L i b ) Executable = Link(ObjectFiles + StaticLib) Executable=Link(ObjectFiles+StaticLib)
链接时:
- 把 libaaa.a 中需要的符号拷贝进最终可执行文件
六、动态库(.so / .dll)
如果链接:
gcc main.o -laaa 且存在:
libaaa.so 则生成:
运行时加载
动态链接模型
E x e c u t a b l e = L i n k S t u b + R u n t i m e R e s o l v e Executable = LinkStub + RuntimeResolve Executable=LinkStub+RuntimeResolve
也就是说:
最终符号解析发生在运行时 最终符号解析发生在运行时 最终符号解析发生在运行时
这又增加一个复杂维度。
七、库搜索路径(-L)
-L/usr/local/bbb/lib 表示:
在该目录查找 libaaa.a / libaaa.so
搜索顺序问题
和 include 类似:
L i b r a r y = F i r s t M a t c h ( L i b r a r y P a t h O r d e r ) Library = FirstMatch(LibraryPathOrder) Library=FirstMatch(LibraryPathOrder)
路径顺序改变 → 可能链接不同版本库
这就是:
ABI 地雷
八、编译选项(-O2 / -Wall)
-O2 -Wall 它们改变:
- 优化级别
- 代码生成方式
- 是否内联
- 是否消除符号
例如:
B i n a r y = C o m p i l e ( S o u r c e , O p t i m i z a t i o n L e v e l ) Binary = Compile(Source, OptimizationLevel) Binary=Compile(Source,OptimizationLevel)
不同优化级别可能产生: - 不同性能
- 不同行为(未定义行为场景)
九、依赖管理复杂性
C/C++ 没有内建依赖系统。
比如:
#include<boost/asio.hpp>编译必须:
-I/path/to/boost 而且版本可能不同:
boost 1.70 boost 1.80 依赖复杂度公式
设:
- N N N = 源文件数
- M M M = 依赖库数量
- P P P = 路径数量
- V V V = 版本数量
则构建复杂度近似:
C o m p l e x i t y ∝ N + M + P + V Complexity \propto N + M + P + V Complexity∝N+M+P+V
当项目变大: - 依赖爆炸
- 版本冲突
- ABI 不兼容
十、软件设计与构建设计耦合
在 C/C++ 中:
构建方式会反向影响代码设计
例如:
1⃣ 头文件过多 → 编译慢
#include<vector>#include<map>#include<boost/xxx.hpp>会导致:
编译时间 ∝ 头文件展开量 编译时间 \propto 头文件展开量 编译时间∝头文件展开量
2⃣ 头文件循环依赖
// A.h#include"B.h"// B.h#include"A.h"直接构建失败。
3⃣ 动态库边界设计
如果你把类暴露给 DLL:
classAPI_EXPORT Foo { std::vector<int> data;};那么:
- STL ABI 版本必须一致
- 编译器必须一致
否则崩溃。
十一、为什么 C/C++ 构建复杂?
根本原因:
1⃣ 预处理是文本替换
不是语法级别处理。
2⃣ 没有 IR 层
直接生成机器码:
S o u r c e → M a c h i n e C o d e Source → MachineCode Source→MachineCode
3⃣ 没有标准包管理
4⃣ ABI 不统一
十二、和 Java 对比
Java:
S o u r c e → B y t e c o d e → J V M → M a c h i n e C o d e Source → Bytecode → JVM → MachineCode Source→Bytecode→JVM→MachineCode
所有库都是:
.class .jar 统一格式。
而 C++:
- 编译器不同
- ABI 不同
- 标准库实现不同
十三、终极总结
C/C++ 构建复杂性的真正根源是:
语言设计 = 系统级 语言设计 = 系统级 语言设计=系统级
所以:
- 需要处理宏
- 需要处理头文件
- 需要处理链接顺序
- 需要处理 ABI
- 需要处理硬件架构
一句话终极抽象
C / C + + 构建复杂度 = 系统级自由度 C/C++ 构建复杂度 = 系统级自由度 C/C++构建复杂度=系统级自由度
自由度越高:
- 可控性越强
- 性能越高
- 构建越复杂
一、C/C++ 构建工具分层模型
可以抽象为三层:
┌──────────────────────┐ │ 依赖管理层 │ Conan / Vcpkg / Hunter / CPM ├──────────────────────┤ │ 构建描述层(生成器) │ CMake / Gyp / GN ├──────────────────────┤ │ 原生执行层 │ Make / Ninja / Bazel / XMake └──────────────────────┘ 数学抽象:
设:
- D D D = 依赖管理
- G G G = 构建生成器
- E E E = 构建执行器
那么:
B u i l d = E ( G ( D ( S o u r c e ) ) ) Build = E(G(D(Source))) Build=E(G(D(Source)))
二、第一层:依赖构建工具(Dependency Management)
工具
- Conan
- vcpkg
- Hunter
- CPM.cmake
它们解决什么问题?
解决:
第三方库如何下载?
如何保证版本一致?
如何跨平台?
例子:使用 Conan
安装 boost
conan install boost/1.83.0@ 它做了:
1⃣ 下载源码
2⃣ 编译
3⃣ 生成本地缓存
4⃣ 生成 CMake 配置
数学抽象:
L o c a l L i b = P a c k a g e M a n a g e r ( R e m o t e L i b , V e r s i o n ) LocalLib = PackageManager(RemoteLib, Version) LocalLib=PackageManager(RemoteLib,Version)
CMake 中使用 Conan 生成的配置
# CMakeLists.txt # 查找 Boost 包 find_package(Boost REQUIRED) # 创建可执行文件 add_executable(app main.cpp) # 链接 Boost 库 target_link_libraries(app Boost::boost) 为什么需要依赖工具?
C/C++ 没有内置包管理。
对比 Java:
- Maven
- Gradle
对比 Rust: - Cargo
C++ 需要额外工具。
三、第二层:封装构建工具(构建生成器)
工具
- CMake
- GYP
- GN
它们做什么?
不直接编译代码。
而是:
生成 Makefile / Ninja 文件 / IDE 工程文件
数学表达:
N a t i v e B u i l d F i l e = G e n e r a t o r ( B u i l d D e s c r i p t i o n ) NativeBuildFile = Generator(BuildDescription) NativeBuildFile=Generator(BuildDescription)
示例:CMake
# CMakeLists.txt # 指定最低版本 cmake_minimum_required(VERSION 3.20) # 项目名称 project(MyApp) # 添加可执行文件 add_executable(app main.cpp) 执行:
cmake -S . -B build 生成:
build/Makefile 或者:
cmake -G Ninja -S . -B build 生成:
build/build.ninja 为什么说抽象级别更高?
因为你只写:
add_executable(app main.cpp) 而不需要写:
app: main.o g++ main.o -o app 四、第三层:原生构建工具(执行器)
工具
- GNU Make
- Ninja
- Bazel
- XMake
1⃣ Make
最经典。
示例:
# Makefile # 编译规则 main.o: main.cpp g++ -c main.cpp # 链接规则 app: main.o g++ main.o -o app 执行:
make2⃣ Ninja
特点:
- 更快
- 更简单
- 专注性能
示例:
rule cc command = g++ -c $in -o $out rule link command = g++ $in -o $out build main.o: cc main.cpp build app: link main.o 执行:
ninja 3⃣ Bazel
强调:
- 可复现构建
- 分布式构建
- 大规模工程
示例:
cc_binary( name ="app", srcs =["main.cpp"],)五、为什么说“更关注性能”?
构建时间可以表示为:
T = ∑ i = 1 n C o m p i l e T i m e i + L i n k T i m e T = \sum_{i=1}^{n} CompileTime_i + LinkTime T=i=1∑nCompileTimei+LinkTime
现代构建工具优化目标:
- 并行构建
- 增量构建
- 依赖最小化
例如 Ninja:
T ≈ T o t a l W o r k C P U 核心数 T \approx \frac{TotalWork}{CPU核心数} T≈CPU核心数TotalWork
六、为什么“抽象级别更高”?
因为构建语言从:
g++ main.o util.o -lboost -lpthread 升级为:
target_link_libraries(app Boost::boost pthread) 抽象掉:
- 平台差异
- 编译器差异
- 路径问题
七、构建系统演进趋势
可以抽象为三代模型:
第一代:命令式
Make:
写清楚怎么编译 第二代:声明式
CMake:
描述目标是什么 第三代:可复现 + 依赖感知
Bazel:
声明依赖图 系统自动最优执行 八、依赖图模型(核心)
现代构建系统本质是:
B u i l d = G r a p h T r a v e r s a l ( D A G ) Build = GraphTraversal(DAG) Build=GraphTraversal(DAG)
其中:
- 节点 = 文件
- 边 = 依赖关系
必须满足:
无环图( D A G ) 无环图(DAG) 无环图(DAG)
否则无法构建。
九、总结三层的作用
| 层级 | 解决问题 |
|---|---|
| 依赖工具 | 下载和版本管理 |
| 生成器 | 跨平台描述构建 |
| 执行器 | 实际编译和并行优化 |
十、终极理解
C/C++ 构建之所以复杂,是因为:
系统级语言 + 无统一官方构建系统 系统级语言 + 无统一官方构建系统 系统级语言+无统一官方构建系统
所以生态分裂成三层。
而 Rust:
C a r g o = 依赖 + 构建 + 执行 Cargo = 依赖 + 构建 + 执行 Cargo=依赖+构建+执行
Java:
M a v e n / G r a d l e = 官方标准生态 Maven/Gradle = 官方标准生态 Maven/Gradle=官方标准生态
一句话总结
C/C++ 构建工具体系:
是分层协作模型,而不是单一工具模型。
一、大型嵌入式系统构建的整体模型
和小项目不同,大型嵌入式系统构建可以抽象为:
B u i l d S y s t e m = E n v i r o n m e n t + T o o l c h a i n + D e p e n d e n c y + T a s k G r a p h + P a c k a g i n g BuildSystem = Environment + Toolchain + Dependency + TaskGraph + Packaging BuildSystem=Environment+Toolchain+Dependency+TaskGraph+Packaging
更具体:
A r t i f a c t = P a c k a g e ( L i n k ( C o m p i l e ( P r e p r o c e s s ( S o u r c e ) ) ) ) Artifact = Package(Link(Compile(Preprocess(Source)))) Artifact=Package(Link(Compile(Preprocess(Source))))
但在工程层面,它变成:
环境准备 ↓ 工具链准备 ↓ 依赖安装 ↓ 任务调度 ↓ 模块构建 ↓ 文件组装 ↓ 部署打包 二、第一部分:工具链设计(Toolchain)
在嵌入式系统中:
构建机 ≠ 运行机
例如:
- 构建机:x86 Linux
- 目标机:ARM Cortex-A53
这叫:
C r o s s C o m p i l e CrossCompile CrossCompile
即:
B u i l d H o s t ≠ T a r g e t P l a t f o r m BuildHost \neq TargetPlatform BuildHost=TargetPlatform
示例:交叉编译工具链文件
CMake 工具链文件:
# toolchain-arm.cmake # 指定目标系统 set(CMAKE_SYSTEM_NAME Linux) # 指定交叉编译器 set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) # 指定目标 sysroot(目标系统的头文件和库) set(CMAKE_SYSROOT /opt/arm-sysroot) 构建:
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain-arm.cmake ..设计重点
- 工具链版本固定
- sysroot 固定
- 编译器固定
否则:
B i n a r y = f ( C o m p i l e r V e r s i o n ) Binary = f(CompilerVersion) Binary=f(CompilerVersion)
编译器变化 → 二进制变化。
三、第二部分:构建环境准备
在云构建中通常使用:
- Docker
- CI/CD 系统
目标:
S a m e I n p u t → S a m e O u t p u t SameInput → SameOutput SameInput→SameOutput
即:
D e t e r m i n i s t i c B u i l d DeterministicBuild DeterministicBuild
示例:Dockerfile
# 使用固定版本基础镜像 FROM ubuntu:22.04 # 安装固定版本工具链 RUN apt-get update && \ apt-get install -y \ gcc-10 \ g++-10 \ cmake \ ninja-build # 设置默认编译器版本 ENV CC=gcc-10 ENV CXX=g++-10 这样可以保证:
- 所有构建机环境一致
- 构建结果可重复
四、第三部分:构建任务管理
大型系统通常有:
- Bootloader
- Kernel
- Driver
- App
- Middleware
可以抽象为一个 DAG:
B u i l d G r a p h = ( V , E ) BuildGraph = (V, E) BuildGraph=(V,E)
其中: - V V V = 模块
- E E E = 依赖关系
示例:模块依赖关系
App → Middleware → Driver → HAL 必须满足:
H A L → D r i v e r → M i d d l e w a r e → A p p HAL → Driver → Middleware → App HAL→Driver→Middleware→App
CMake 模块示例
# 添加静态库模块 add_library(driver STATIC driver.cpp) # 添加应用模块 add_executable(app main.cpp) # 指定依赖关系 target_link_libraries(app driver) CMake 自动生成:
- 依赖图
- 构建顺序
五、模块编译构建流程
每个模块都会经历:
预处理 → 编译 → 汇编 → 链接 数学表示:
. o = C o m p i l e ( P r e p r o c e s s ( . c p p ) ) .o = Compile(Preprocess(.cpp)) .o=Compile(Preprocess(.cpp))
l i b . a = A r c h i v e ( . o ) lib.a = Archive(.o) lib.a=Archive(.o)
a p p = L i n k ( l i b . a + . o ) app = Link(lib.a + .o) app=Link(lib.a+.o)
六、文件组装与打包
嵌入式系统常见需求:
- 制作 rootfs
- 打包固件
- 生成镜像
示例:文件组装脚本
#!/bin/bash# 创建打包目录mkdir -p rootfs/bin # 复制可执行文件cp build/app rootfs/bin/ # 设置文件权限chmod755 rootfs/bin/app # 打包为 tar 包tar -czf firmware.tar.gz rootfs/ 数学抽象
F i r m w a r e = P a c k a g e ( F i l e S e t , P e r m i s s i o n , L a y o u t ) Firmware = Package(FileSet, Permission, Layout) Firmware=Package(FileSet,Permission,Layout)
七、安装脚本设计
例如安装到目标设备:
#!/bin/bash# 解压固件tar -xzf firmware.tar.gz -C / # 设置执行权限chmod +x /bin/app # 创建服务 systemctl enable app.service 这属于:
D e p l o y P h a s e DeployPhase DeployPhase
八、构建设计的四大关注点
1⃣ 构建可重复
目标:
S a m e S o u r c e + S a m e E n v = S a m e B i n a r y SameSource + SameEnv = SameBinary SameSource+SameEnv=SameBinary
关键措施:
- 固定编译器版本
- 固定依赖版本
- 固定环境
- 使用容器
2⃣ 构建性能高
构建时间公式:
T = ∑ C o m p i l e T i m e i + L i n k T i m e T = \sum CompileTime_i + LinkTime T=∑CompileTimei+LinkTime
优化方法:
- 并行构建(Ninja)
- 增量构建
- 分布式构建
- 缓存(ccache)
3⃣ 构建代码化(Infrastructure as Code)
不要:
- 手工安装依赖
- 手工执行命令
要: - Dockerfile
- CMakeLists.txt
- CI 脚本
示例:
# GitLab CI 示例build:script:- cmake -B build - cmake --build build -j8 4⃣ 构建可视化
大型项目需要:
- 构建状态监控
- 构建时间统计
- 依赖图可视化
例如:
cmake --graphviz=graph.dot 生成依赖图。
九、云构建架构
现代嵌入式系统常用:
开发者 → 提交代码 ↓ CI系统 ↓ Docker构建环境 ↓ 分布式并行构建 ↓ 产出固件 ↓ 自动部署测试 数学抽象:
C l o u d B u i l d = D i s t r i b u t e d ( D e t e r m i n i s t i c B u i l d ) CloudBuild = Distributed(DeterministicBuild) CloudBuild=Distributed(DeterministicBuild)
十、终极理解
大型嵌入式构建设计的本质不是“编译代码”,而是:
工程系统设计问题 工程系统设计问题 工程系统设计问题
它涉及:
- 依赖管理
- 版本控制
- 跨平台
- 自动化
- 可视化
- 性能优化
- 可追溯性
一句话总结
小项目构建是:
编译程序
大型嵌入式构建是:
设计一个稳定、可重复、可扩展的自动化生产系统
一、模块编译构建的本质
模块构建可以抽象为:
M o d u l e = ( S o u r c e , D e p e n d e n c y , T o o l c h a i n , C o n f i g ) Module = (Source, Dependency, Toolchain, Config) Module=(Source,Dependency,Toolchain,Config)
构建结果为:
A r t i f a c t i = B u i l d ( M o d u l e i ) Artifact_i = Build(Module_i) Artifacti=Build(Modulei)
整个系统:
S y s t e m = ⋃ i = 1 n A r t i f a c t i System = \bigcup_{i=1}^{n} Artifact_i System=i=1⋃nArtifacti
当模块数量 n n n 增大时:
- 依赖关系指数级复杂
- 构建时间急剧增长
- 修改影响范围扩大
1⃣ 构建层面的 artifact
在构建(Build)过程中,artifact 指的是 由源代码经过编译、打包、链接等生成的产物。例如:
- 可执行文件(Executable /
.exe/.out) - 静态库(Static Library /
.a/.lib) - 动态库(Shared Library /
.so/.dll) - Jar 包(Java /
.jar) - Web 打包产物(JS、CSS bundle 等)
示例:
# C++构建示例 g++ main.cpp utils.cpp -o myapp # myapp 就是 artifact ar rcs libutils.a utils.o # libutils.a 是静态库 artifact数学上可以抽象表示:
A r t i f a c t i = B u i l d ( S o u r c e i , D e p e n d e n c i e s i , C o m p i l e r , F l a g s ) Artifact_i = Build(Source_i, Dependencies_i, Compiler, Flags) Artifacti=Build(Sourcei,Dependenciesi,Compiler,Flags)
即 artifact 是构建函数的输出。
2⃣ 软件工程过程中的 artifact
在软件开发生命周期(SDLC)中,artifact 不仅包括构建产物,还包括 任何有助于开发、测试、部署的文档或文件,例如:
- 设计文档(UML 图、架构说明)
- 配置文件(YAML、JSON)
- 测试报告 / 覆盖率报告
- 数据库迁移脚本
- API 文档
也就是说,artifact 是 任何有价值的输出物,用于支持软件开发、交付或运维。
3⃣ Artifact 的分类
| 类型 | 举例 | 使用场景 |
|---|---|---|
| Binary artifact | .exe, .dll, .so, .jar | 构建产物,可直接运行或链接 |
| Source artifact | 源代码压缩包 | 发布源代码 |
| Documentation artifact | UML、API 文档 | 开发与维护参考 |
| Test artifact | 测试报告、覆盖率 | 测试和质量保障 |
| Deployment artifact | Docker 镜像、Kubernetes YAML | 部署与运维 |
核心理解
- artifact = 生成物
- 它是构建或开发流程的可交付产物
- 在 CI/CD 中,artifact 往往会被 存储、传递和复用,例如:
源码 -> 编译 -> Artifact -> 部署 在 Maven/Gradle/CMake 中,artifact 都是关键概念。
二、构建核心领域的“变化方向”
大型系统的构建演进通常经历:
单体构建 ↓ 模块化构建 ↓ 组件化构建 ↓ 图驱动构建(DAG) ↓ 云分布式构建 数学表达:
B u i l d S y s t e m : S c r i p t → G r a p h → D i s t r i b u t e d G r a p h BuildSystem: Script → Graph → DistributedGraph BuildSystem:Script→Graph→DistributedGraph
三、问题一:产品组合逻辑复杂
什么是产品组合逻辑?
比如:
- 标准版
- 高级版
- 定制版
- ARM版本
- x86版本
- Debug版本
- Release版本
这会形成:
P r o d u c t = P l a t f o r m × F e a t u r e × B u i l d T y p e Product = Platform \times Feature \times BuildType Product=Platform×Feature×BuildType
如果: - 平台 3 种
- 功能组合 4 种
- 构建类型 2 种
则组合数量:
3 × 4 × 2 = 24 3 \times 4 \times 2 = 24 3×4×2=24
组合爆炸。
传统做法(问题来源)
# 不推荐写法:大量 if 判断 if(PLATFORM_ARM) add_definitions(-DARM_PLATFORM) endif() if(FEATURE_A) add_definitions(-DFEATURE_A) endif() if(RELEASE) set(CMAKE_BUILD_TYPE Release) endif() 问题:
- 构建逻辑和产品逻辑耦合
- 修改一个条件会影响多个模块
- 可维护性差
四、问题二:构建修改难度大
根本原因:
H i g h C o u p l i n g = S h a r e d G l o b a l S t a t e HighCoupling = SharedGlobalState HighCoupling=SharedGlobalState
例如:
- 全局宏污染
- 全局 include 路径
- 全局编译选项
错误示例
# 全局设置(污染所有模块) include_directories(/usr/local/xxx/include) add_definitions(-DENABLE_TEST) 所有模块都会被影响。
正确方式:模块隔离
add_library(moduleA moduleA.cpp) # 只对 moduleA 生效 target_compile_definitions(moduleA PRIVATE ENABLE_TEST) target_include_directories(moduleA PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ) 这样:
I m p a c t ( m o d u l e A ) ⇏ I m p a c t ( m o d u l e B ) Impact(moduleA) \not\Rightarrow Impact(moduleB) Impact(moduleA)⇒Impact(moduleB)
五、问题三:模块独立构建测试难
原因:
- 强耦合
- 隐式依赖
- 全局变量
数学表示:
B u i l d ( m o d u l e A ) → R e q u i r e s ( S y s t e m ) Build(moduleA) \rightarrow Requires(System) Build(moduleA)→Requires(System)
理想状态:
B u i l d ( m o d u l e A ) → R e q u i r e s ( D e p e n d e n c i e s A ) Build(moduleA) \rightarrow Requires(DependenciesA) Build(moduleA)→Requires(DependenciesA)
解决方法:接口化依赖
add_library(interfaceLib INTERFACE) target_include_directories(interfaceLib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) add_library(moduleA moduleA.cpp) target_link_libraries(moduleA PRIVATE interfaceLib) 这样 moduleA 不依赖整个系统。
六、问题四:外部依赖复杂
外部依赖包括:
- 第三方库
- 系统库
- SDK
问题: - 版本不一致
- ABI 不兼容
- 本地与 CI 不一致
数学抽象
B i n a r y = f ( C o m p i l e r , F l a g s , D e p e n d e n c y V e r s i o n ) Binary = f(Compiler, Flags, DependencyVersion) Binary=f(Compiler,Flags,DependencyVersion)
如果 DependencyVersion 变化:
B i n a r y ≠ P r e v i o u s B i n a r y Binary \neq PreviousBinary Binary=PreviousBinary
解决:版本锁定
例如使用 Conan:
conan install. --build=missing 锁定版本文件:
conan.lock 保证:
D e t e r m i n i s t i c D e p e n d e n c y DeterministicDependency DeterministicDependency
七、问题五:内部模块依赖混乱
如果模块之间随意引用:
A → B B → C C → A 形成循环依赖:
C y c l e ( A , B , C ) Cycle(A,B,C) Cycle(A,B,C)
会导致:
- 无法并行构建
- 难以测试
- 链接错误
解决方法:分层架构
Application ↓ Service ↓ Domain ↓ Platform 必须满足:
L a y e r i → L a y e r i − 1 Layer_i \rightarrow Layer_{i-1} Layeri→Layeri−1
禁止反向依赖。
八、问题六:构建性能差
构建时间:
T = ∑ C o m p i l e T i m e i + L i n k T i m e T = \sum CompileTime_i + LinkTime T=∑CompileTimei+LinkTime
若模块间依赖过重:
M o d i f y ( m o d u l e A ) ⇒ R e b u i l d ( W h o l e S y s t e m ) Modify(moduleA) \Rightarrow Rebuild(WholeSystem) Modify(moduleA)⇒Rebuild(WholeSystem)
这是性能灾难。
优化方法
1⃣ 减少头文件污染
不要:
// 不推荐#include<vector>#include<map>#include<iostream>推荐前向声明:
// 只声明,不引入完整头文件classFoo;2⃣ 使用 Ninja 并行构建
cmake -G Ninja .. ninja -j16 并行度提升:
T ≈ T o t a l C o m p i l e T i m e C P U _ C o r e s T \approx \frac{TotalCompileTime}{CPU\_Cores} T≈CPU_CoresTotalCompileTime
3⃣ 使用 ccache
缓存机制:
I f ( H a s h ( s o u r c e , f l a g s ) = S a m e ) ⇒ S k i p C o m p i l e If(Hash(source, flags) = Same) \Rightarrow SkipCompile If(Hash(source,flags)=Same)⇒SkipCompile
九、问题七:工具链选择
工具链影响:
- 性能
- ABI
- 兼容性
- 标准支持
例如: - GCC
- Clang
- ARM GCC
- MSVC
不同工具链:
A B I ( G C C ) ≠ A B I ( M S V C ) ABI(GCC) \neq ABI(MSVC) ABI(GCC)=ABI(MSVC)
不能混用。
工具链选择原则
- 是否支持目标架构
- 是否支持所需 C++ 标准
- 是否稳定
- 是否可长期维护
十、构建核心领域的未来方向
现代构建趋势:
S c r i p t B u i l d → G r a p h B u i l d → D i s t r i b u t e d B u i l d ScriptBuild \rightarrow GraphBuild \rightarrow DistributedBuild ScriptBuild→GraphBuild→DistributedBuild
目标:
- 模块独立
- 依赖可视化
- 版本可追溯
- 构建可重复
- 支持多产品线
十一、终极总结
大型系统模块构建的核心问题,本质是:
复杂度管理问题 复杂度管理问题 复杂度管理问题
具体表现为:
- 产品组合爆炸
- 依赖耦合
- 模块边界不清
- 构建性能下降
- 工具链混乱
解决方向: - 模块化
- 图驱动构建
- 版本锁定
- 分层架构
- 并行与缓存优化
设计原则:
分离不同变化方向
不同层构建脚本独立
通过组合完成系统构建
1⃣ 模块编译构建图理解(SVG 部分)
· 构建配置层· 外部依赖层· 核心构建层Product AProduct BfmtXmlJsonCacheProtobufABCDEF
你的 SVG 描述了 大型嵌入式系统或复杂软件构建的模块层次与依赖关系:
图中层次
- 构建配置层(顶部)
- 主要包括 产品组合 Product A / Product B
- 这里体现了不同产品的 构建目标 和 特定的构建配置
- 可以理解为 构建参数和宏配置
- 外部依赖层(中间)
- 包括
fmt,Xml,Json等第三方库 - 还有
Cache,Protobuf等功能性库 - 表示 模块依赖关系
- 每个依赖的箭头指向核心构建层的模块(A~F)
- 包括
- 核心构建层(底部或右侧)
- 圆形节点 A~F
- 表示产品最终生成的 核心模块或业务逻辑模块
- 模块之间可能有 交叉依赖或调用
- 红色虚线箭头表示 依赖注入或特定依赖关系
- 橙色虚线表示 模块间的数据流或消息流
核心理解
- 构建的核心问题是 模块依赖关系复杂,构建顺序需要按 DAG(有向无环图)来调度
- 每个模块 A~F 可以看作一个 构建单元 artifact
- 外部依赖需要 先构建/准备,才能在核心构建中使用
2⃣ Mermaid 图理解(交叉编译层)
交叉编译层
CC
gcc
Clang
Centos
Ubuntu
理解
- CC
- 通用编译器接口,表示 编译器抽象层
- 源代码对 CC 屏蔽具体编译器实现
- gcc / Clang
- CC 的具体实现
- 提供 编译、优化、预处理功能
- 操作系统平台
- CentOS / Ubuntu
- 提供 交叉编译环境、头文件和库路径
构建流程公式化表示
C/C++ 构建可以抽象为一个函数:
A r t i f a c t = B u i l d ( S o u r c e , C o m p i l e r , P l a t f o r m , F l a g s , D e p e n d e n c i e s ) Artifact = Build(Source, Compiler, Platform, Flags, Dependencies) Artifact=Build(Source,Compiler,Platform,Flags,Dependencies)
其中:
- S o u r c e Source Source — 源代码文件
- C o m p i l e r Compiler Compiler — 编译器(gcc、clang、MSVC 等)
- P l a t f o r m Platform Platform — 目标平台(Ubuntu、CentOS、嵌入式硬件)
- F l a g s Flags Flags — 编译选项(宏定义、优化选项)
- D e p e n d e n c i e s Dependencies Dependencies — 外部库或模块
例如:
// 构建目标示例 g++-I/usr/local/include -L/usr/local/lib -O2 main.cpp utils.cpp -o myapp -I指定头文件搜索路径-L指定库文件搜索路径-O2编译优化级别myapp就是生成的 artifact
3⃣ 模块构建关注点
结合 SVG 的模块 A~F 与 Mermaid 的编译器层次:
- 构建顺序问题
M o d u l e i depends on M o d u l e j ⇒ M o d u l e j must build first Module_i \text{ depends on } Module_j \Rightarrow Module_j \text{ must build first} Modulei depends on Modulej⇒Modulej must build first - 依赖管理
- 外部依赖(fmt、Json、Protobuf)
- 内部模块依赖(Cache、核心模块 A~F)
- 构建性能与可重复性
- 可用 增量构建:只重编译修改过的模块
- 构建可视化:通过 DAG 显示模块依赖
4⃣ 核心总结
- 构建是将源码变成 artifact 的过程
- 包括 预编译、编译、链接
- 生成目标可能是 可执行文件、静态库、动态库
- 模块化与依赖管理是复杂性来源
- 产品组合、宏定义、平台差异
- 交叉编译器选择(gcc / clang / MSVC)
- C/C++ 构建工具分层
- 依赖工具:Conan, Vcpkg
- 封装工具:CMake, Gyp
- 原生工具:Make, Ninja
- 数学公式化
A r t i f a c t = B u i l d ( S o u r c e , D e p e n d e n c i e s , C o m p i l e r , P l a t f o r m , F l a g s ) Artifact = Build(Source, Dependencies, Compiler, Platform, Flags) Artifact=Build(Source,Dependencies,Compiler,Platform,Flags)
A r t i f a c t i must satisfy ∀ j ∈ D e p i , A r t i f a c t j built first Artifact_i \text{ must satisfy } \forall j \in Dep_i, Artifact_j \text{ built first} Artifacti must satisfy ∀j∈Depi,Artifactj built first
好的,我们来详细解析你提供的 核心构建层设计 SVG 图,并结合理解、代码片段注释以及数学公式化表达。
1⃣ 图整体结构
SVG 图分为两个主要区域:
- 逻辑视图(Logical View) - 左侧灰色区域
- 表示 开发时的模块视角
- 节点(圆形 A~J)代表 开发阶段的模块
- 节点之间的黑色箭头表示 模块间依赖或调用关系
- 红色虚线箭头表示 逻辑视图与部署视图之间的差距(Large gap)
- 这个区域体现的问题:
- 开发者“远离构建”(开发关注逻辑,未关注构建细节)
- 构建粒度太粗(一个大模块包含太多逻辑)
- 构建脚本复杂(逻辑与构建强耦合)
- 部署视图(Deployment View) - 右侧灰色区域
- 表示 构建和部署后的实际产物
- 节点(圆形 A~E)代表 最终生成的模块/服务/artifact
- 黑色箭头表示 模块间运行时调用关系
- 红色虚线箭头表示 逻辑视图到部署视图映射的差距
逻辑视图与部署视图之间存在“gap”,导致开发与构建脱节。
逻辑视图部署视图Large gapABCEFDHJIGABCDE
2⃣ 数学公式化表示
我们可以用 映射函数 描述逻辑视图到部署视图的关系:
- 假设逻辑模块集合为 L = A , B , C , D , E , F , G , H , I , J L = {A,B,C,D,E,F,G,H,I,J} L=A,B,C,D,E,F,G,H,I,J
- 假设部署模块集合为 D = A , B , C , D , E D = {A,B,C,D,E} D=A,B,C,D,E
定义 映射函数:
ϕ : L → D \phi: L \rightarrow D ϕ:L→D - 映射描述了逻辑模块到部署模块的归属关系
- 红色虚线表示映射的不精确性或“gap”:
gap = x ∈ L ∣ ϕ ( x ) 复杂或模糊 \text{gap} = { x \in L \mid \phi(x) \text{复杂或模糊} } gap=x∈L∣ϕ(x)复杂或模糊
3⃣ 核心构建问题抽象
构建粒度粗
- 当前逻辑模块(A~J)可能 多个逻辑模块被打包成一个部署 artifact
- 增量构建难度大:
B u i l d ( A r t i f a c t ) ≠ ∑ B u i l d ( M o d u l e i ) Build(Artifact) \neq \sum Build(Module_i) Build(Artifact)=∑Build(Modulei) - 理想情况是:
A r t i f a c t j = B u i l d ( M o d u l e i ∣ i ∈ s u b s e t j ) Artifact_j = Build({ Module_i \mid i \in subset_j }) Artifactj=Build(Modulei∣i∈subsetj) - 这样可以 支持增量构建 和 模块独立测试
构建脚本复杂
- 黑色箭头表示模块依赖,构建脚本需要 按照 DAG 拓扑顺序 调用编译、链接命令
- 如果依赖关系复杂,脚本复杂度为 O ( ∣ E ∣ ) O(|E|) O(∣E∣),其中 ∣ E ∣ |E| ∣E∣ 是依赖边数量
开发远离构建
- 开发者看到的是逻辑视图 L L L
- 构建实际关注部署视图 D D D
- 两者脱节导致:
D e v F o c u s ∩ B u i l d F o c u s ≈ ∅ DevFocus \cap BuildFocus \approx \emptyset DevFocus∩BuildFocus≈∅ - 即开发者对构建的关注很少
4⃣ 代码片段示例(CMake 风格,表示核心构建层)
# 核心构建层 CMakeLists.txt 示例 cmake_minimum_required(VERSION 3.22) project(CoreBuildLayer) # 模块逻辑定义(逻辑视图) add_library(ModuleA src/A.cpp) add_library(ModuleB src/B.cpp) add_library(ModuleC src/C.cpp) # 模块依赖关系 target_link_libraries(ModuleB PRIVATE ModuleA) target_link_libraries(ModuleC PRIVATE ModuleA ModuleB) # 最终部署产物(部署视图) add_executable(Product1 main.cpp) target_link_libraries(Product1 PRIVATE ModuleA ModuleB ModuleC) 注释理解:
add_library对应 逻辑模块 A~Ctarget_link_libraries对应 黑色箭头依赖add_executable对应 部署视图节点 Artifact- 红色虚线 gap 表示 逻辑模块到部署 artifact 的映射复杂
5⃣ 总结
- 核心构建层问题主要来源于:
- 逻辑模块与部署模块的映射不清晰
- 模块依赖复杂,导致脚本难维护
- 构建粒度过粗,无法支持增量构建
- 开发远离构建,开发者只关注逻辑,缺少构建感知
- 解决方向:
- 设计 清晰的模块分层,让逻辑模块与部署模块映射明确
- 拆分构建粒度,支持增量构建
- 自动化构建脚本,通过 CMake/Ninja 等工具管理 DAG 依赖
- 可视化构建依赖,缩小逻辑与部署 gap
“构建支撑更大粒度的封装”
ACB文件级 / Class级.h/.hpp.c/.cpp模块级 / 组件级.h/.hpp.h/.hpp.c/.cpp.c/.cpp.c/.cpp.c/.cpp.h/.hpp.h/.hppBuild script逻辑视图
1⃣ 图整体结构
SVG 图表达了 文件级 / Class 级 → 模块级 / 组件级 → 构建节点对象化 的概念:
(1) 文件级 / Class 级
- 左下方的小方块表示 源文件或类文件:
.h/.hpp:头文件.c/.cpp:源文件
- 小方块之间有箭头指向下一层,表示 构建依赖关系
- 核心思想:
- 构建系统从 单个文件 或 类文件 开始
- 小粒度,但依赖管理复杂
(2) 模块级 / 组件级
- 中间椭圆区域(灰色)表示 模块或组件
- 模块内部包含若干文件(多个
.c/.cpp和.h/.hpp) - 每个模块可以看作 一个构建节点对象(Build Node Object)
- 目标:
- 构建与物理设计保持一致
- 支持模块级增量构建
- 构建粒度放大,减少构建脚本复杂度
(3) 构建节点对象化
- 图右上角的圆圈 A/B/C 表示 对象化的构建节点
- 节点之间的箭头表示 依赖关系
- 优点:
- 可以像操作对象一样操作构建节点
- 支持 封装依赖和构建逻辑
- 构建粒度一致,易于扩展和维护
2⃣ 数学公式化描述
设:
- 文件级节点集合为 F = f 1 , f 2 , . . . , f n F = {f_1, f_2, ..., f_n} F=f1,f2,...,fn
- 模块级节点集合为 M = m 1 , m 2 , . . . , m k M = {m_1, m_2, ..., m_k} M=m1,m2,...,mk
- 构建节点对象集合为 B = b 1 , b 2 , . . . , b p B = {b_1, b_2, ..., b_p} B=b1,b2,...,bp
定义 封装映射函数:
ϕ 1 : F → M , ϕ 2 : M → B \phi_1: F \rightarrow M, \quad \phi_2: M \rightarrow B ϕ1:F→M,ϕ2:M→B - 每个文件归属到某个模块,每个模块归属到某个构建节点对象
- 构建流程公式化为:
B u i l d ( B i ) = L i n k ( ⋃ m j ∈ ϕ 2 − 1 ( B i ) C o m p i l e ( ϕ 1 − 1 ( m j ) ) ) Build(B_i) = Link\left(\bigcup_{m_j \in \phi_2^{-1}(B_i)} Compile(\phi_1^{-1}(m_j))\right) Build(Bi)=Linkmj∈ϕ2−1(Bi)⋃Compile(ϕ1−1(mj))
解释:ϕ 1 − 1 ( m j ) \phi_1^{-1}(m_j) ϕ1−1(mj):模块 m j m_j mj 的所有源文件C o m p i l e ( ϕ 1 − 1 ( m j ) ) Compile(\phi_1^{-1}(m_j)) Compile(ϕ1−1(mj)):模块内部所有文件的编译L i n k ( . . . ) Link(...) Link(...):模块内部编译产物汇总,形成构建节点对象
3⃣ 理解总结
- 构建粒度提升:
- 文件级 → 模块级 → 构建节点对象
- 每个构建节点对象可独立管理依赖和构建流程
- 构建与物理设计保持一致:
- 模块划分符合实际物理组件或部署逻辑
- 避免逻辑模块与部署模块错位
- 构建节点对象化:
- 每个模块/组件封装成 对象
- 可以像操作对象一样操作节点(增量构建、依赖管理)
4⃣ CMake 构建节点对象化示例
# 构建节点对象化示例 # 文件级构建 add_library(File_A src/A.cpp) add_library(File_B src/B.cpp) # 模块级封装 add_library(Module_X $<TARGET_OBJECTS:File_A> $<TARGET_OBJECTS:File_B> ) # 构建节点对象 add_executable(Product_Node Module_X) target_link_libraries(Product_Node PRIVATE Module_X) 注释说明:
add_library(File_A ...)对应 文件级节点Module_X对应 模块级封装- 使用
TARGET_OBJECTS将文件级节点聚合
- 使用
Product_Node对应 构建节点对象- 通过模块级封装简化构建脚本
5⃣ 总结
- 思想:从细粒度文件构建到模块、再到对象化构建节点
- 优点:
- 构建粒度统一,增量构建高效
- 模块依赖清晰,脚本简洁
- 与物理设计一致,方便部署
- 数学描述:
- 使用映射函数 ϕ 1 , ϕ 2 \phi_1, \phi_2 ϕ1,ϕ2 明确文件 → 模块 → 构建节点的关系
- 构建公式: B u i l d ( B i ) = L i n k ( ⋃ m j ∈ ϕ 2 − 1 ( B i ) C o m p i l e ( ϕ 1 − 1 ( m j ) ) ) Build(B_i) = Link\left(\bigcup_{m_j \in \phi_2^{-1}(B_i)} Compile(\phi_1^{-1}(m_j))\right) Build(Bi)=Link(⋃mj∈ϕ2−1(Bi)Compile(ϕ1−1(mj)))
外部依赖层(External Dependency Layer)**
▶ 外部依赖层源代码库二级制库公共头文件底层接口外部依赖库- 自构建- 闭包性- 标准化
- 定义:系统中不由构建系统自身提供的代码元素。通常包括第三方库、源代码库、二进制库、公共头文件等。
- 核心挑战:
- 头文件构建选择:如何在不同构建环境中选择正确的头文件。
- 精确依赖控制:控制哪些模块依赖哪些外部库,避免不必要的耦合。
- 编译宏选择:不同编译条件下选择不同宏定义,保证兼容性与功能正确性。
图中内容解读:
- 左侧黄色矩形:具体的外部依赖元素,包括
- 源代码库
- 二进制库
- 公共头文件
- 底层接口
- 中间灰色矩形:外部依赖库(汇总和封装的外部库)
- 右侧灰蓝色矩形:外部依赖的设计原则
- 自构建
- 闭包性
- 标准化
依赖关系箭头:
- 左侧元素通过白色箭头指向中间外部依赖库
- 红色虚线箭头表示依赖接口与原则之间的关系(受标准化、闭包性等约束)
3⃣ 进一步数学/逻辑理解
如果用 集合与函数 的角度来理解外部依赖:
- 设外部依赖元素集合为 D D D:
D = 源代码库 , 二级制库 , 公共头文件 , 底层接口 D = { \text{源代码库}, \text{二级制库}, \text{公共头文件}, \text{底层接口} } D=源代码库,二级制库,公共头文件,底层接口 - 外部依赖库集合 L L L 是 D D D 的封装函数 f f f:
L = f ( D ) L = f(D) L=f(D) - 设计原则(自构建、闭包性、标准化)可以看作约束集合 P P P,对 L L L 有约束作用:
L ⊆ D 且满足 P L \subseteq D \quad \text{且满足 } P L⊆D且满足 P
箭头表示映射关系或依赖关系,即每个元素通过函数 f f f 映射到外部依赖库 L L L,再受约束 P P P 调整。
构建配置层 + 外部依赖层 + 核心构建层
ProductAtoolchain: arm64dependList:- platA- msgBtargetList:- aaa.so- bbb.soBuild.sh• 构建配置层• 外部依赖层platAMsgAplatBMsgB• 核心构建层aaabbbcccdddFE
1⃣ 概念理解
图层分解
- 构建配置层(ProductA 配置)
- 包含
toolchain(工具链,比如 CPU 架构 arm64) dependList(依赖列表)targetList(构建目标,如aaa.so、bbb.so)Build.sh脚本用于驱动整个构建过程
- 包含
- 外部依赖层
- 各种外部组件,如
platA、platB、MsgA、MsgB - 不同产品可以灵活选择外部依赖
- 各种外部组件,如
- 核心构建层
- 产出的核心组件,如
aaa、bbb、ccc、ddd - 圆形
F、E表示功能模块或内部依赖
- 产出的核心组件,如
优点分析
- 支持多种 CPU 硬件类型
- 产品可以灵活选择外部依赖
- 产品可以灵活选择构建组件
3⃣ 数学/逻辑理解
可以用 集合 + 映射函数 描述整个依赖与构建过程:
- 外部依赖集合:
D = platA , platB , MsgA , MsgB D = { \text{platA}, \text{platB}, \text{MsgA}, \text{MsgB} } D=platA,platB,MsgA,MsgB - 构建配置层映射为核心构建层函数 f f f:
f : ( toolchain , dependList , targetList ) ↦ 核心构建组件 = a a a , b b b , c c c , d d d f: (\text{toolchain}, \text{dependList}, \text{targetList}) \mapsto \text{核心构建组件} = {aaa, bbb, ccc, ddd} f:(toolchain,dependList,targetList)↦核心构建组件=aaa,bbb,ccc,ddd - 核心构建层内部模块依赖可表示为有向图 G = ( V , E ) G = (V, E) G=(V,E),其中 V = F , E , a a a , b b b , c c c , d d d V = {F, E, aaa, bbb, ccc, ddd} V=F,E,aaa,bbb,ccc,ddd,箭头表示依赖方向。
两个产品(Product A 和 Product B)的组件依赖和演进路径
Product AAfmtBJsonCfmtProduct BDfmtBJsonEfmt小步快跑Product AProduct BJsonfmtABDCE
1⃣ 理解
图层与元素说明
- 整体背景
- 黑色背景,突出图形层次。
- 产品轮廓
- 两个棱形/三角形结构分别代表 Product A 和 Product B。
- 填充色
#E8A87C,描边白色,文字标明产品名。
- 组件表示
- 圆形:产品中的核心模块(如
A,B,C,D,E) - 黄色小矩形:模块所依赖的外部库/工具(如
fmt,Json) - 箭头表示模块依赖方向。
- 圆形:产品中的核心模块(如
- 小步快跑
- 黄色路径框表示迭代演进(敏捷开发)概念
- 文字标识 “小步快跑”,说明产品快速迭代、持续集成的理念。
- 右侧统一依赖汇总
Json和fmt在右侧统一列出- 圆形模块连接箭头表示模块对统一库的依赖
- 说明不同产品共享外部库,提高复用性。
2⃣ 逻辑解析(依赖关系)
以 Product A 为例:
- 模块集合 M A = A , B , C M_A = {A, B, C} MA=A,B,C
- 外部库集合 L = fmt , Json L = {\text{fmt}, \text{Json}} L=fmt,Json
- 依赖函数 f f f:
f : M A → 2 L f: M_A \rightarrow 2^L f:MA→2L
例如:
f ( A ) = fmt , f ( B ) = Json , f ( C ) = fmt f(A) = {\text{fmt}}, \quad f(B) = {\text{Json}}, \quad f(C) = {\text{fmt}} f(A)=fmt,f(B)=Json,f(C)=fmt
箭头表示模块间依赖或模块对外部库的依赖: - A → C A \rightarrow C A→C
- B → C B \rightarrow C B→C
类似 Product B: - 模块集合 M B = D , B , E M_B = {D, B, E} MB=D,B,E
- 外部库共享 L = fmt , Json L = {\text{fmt}, \text{Json}} L=fmt,Json
- 依赖函数 g g g:
g ( D ) = fmt , g ( B ) = Json , g ( E ) = fmt g(D) = {\text{fmt}}, \quad g(B) = {\text{Json}}, \quad g(E) = {\text{fmt}} g(D)=fmt,g(B)=Json,g(E)=fmt
最终右侧统一依赖库可表示为 依赖汇总集合:
L common = fmt , Json L_\text{common} = {\text{fmt}, \text{Json}} Lcommon=fmt,Json
4⃣ 数学/逻辑抽象
模块依赖映射函数
对于 Product A:
f A : M A → 2 L , M A = A , B , C , L = f m t , J s o n f_A: M_A \rightarrow 2^L, \quad M_A = {A,B,C}, \quad L = {fmt, Json} fA:MA→2L,MA=A,B,C,L=fmt,Json
- f A ( A ) = f m t f_A(A) = {fmt} fA(A)=fmt
- f A ( B ) = J s o n f_A(B) = {Json} fA(B)=Json
- f A ( C ) = f m t f_A(C) = {fmt} fA(C)=fmt
类似 Product B:
f B : M B → 2 L , M B = D , B , E , L = f m t , J s o n f_B: M_B \rightarrow 2^L, \quad M_B = {D,B,E}, \quad L = {fmt, Json} fB:MB→2L,MB=D,B,E,L=fmt,Json - f B ( D ) = f m t f_B(D) = {fmt} fB(D)=fmt
- f B ( B ) = J s o n f_B(B) = {Json} fB(B)=Json
- f B ( E ) = f m t f_B(E) = {fmt} fB(E)=fmt
模块间依赖
可以表示为有向图 G = ( V , E ) G=(V,E) G=(V,E):
- V = M A ∪ M B V = M_A \cup M_B V=MA∪MB
- E = ( A , C ) , ( B , C ) , ( D , E ) , ( B , E ) E = {(A,C),(B,C),(D,E),(B,E)} E=(A,C),(B,C),(D,E),(B,E)
箭头表示模块间依赖。右侧统一库表示共享外部依赖:
L common = f m t , J s o n L_\text{common} = {fmt, Json} Lcommon=fmt,Json
C/C++ 构建重构
1⃣ 理解
构建重构(Build Refactoring):
- 指在 不改变软件构建目标功能行为 的前提下,对构建系统、构建脚本进行优化与整理。
- 核心目的:
- 提高可读性:让脚本更容易理解,降低新成员学习成本。
- 降低修改成本:减少因为修改构建脚本而引入错误的风险。
- 提升构建效率:优化依赖检查、并行构建策略等。
主要目标细分:
| 序号 | 目标 | 说明 |
|---|---|---|
| 1 | 正确完成构建目标功能 | 构建输出不变,确保 exe、库文件、头文件等生成正确 |
| 2 | 提高构建效率 | 通过增量构建、并行构建、缓存机制等手段降低构建时间 |
| 3 | 消除构建脚本重复 | 合并重复命令、复用构建逻辑,避免 copy-paste 代码块 |
| 4 | 提高构建脚本可理解性 | 使用清晰变量名、模块化逻辑和注释,使逻辑清楚 |
| 5 | 减少冗余 | 去掉无用文件、重复依赖和多余的构建步骤 |
2⃣ 数学化/逻辑理解
- 构建过程函数化
设整个构建过程为一个函数 B B B:
B : S × D → O B: S \times D \rightarrow O B:S×D→O
- S S S:构建脚本
- D D D:源代码、头文件、依赖库等输入
- O O O:构建产物(可执行文件、库、目标文件)
构建重构的目标是 在 O O O 不变的前提下优化 S S S:
B ( S old , D ) = B ( S new , D ) = O B(S_\text{old}, D) = B(S_\text{new}, D) = O B(Sold,D)=B(Snew,D)=O
其中, S new S_\text{new} Snew 满足以下优化指标: - 可读性 R ( S new ) > R ( S old ) R(S_\text{new}) > R(S_\text{old}) R(Snew)>R(Sold)
- 构建效率 E ( S new ) > E ( S old ) E(S_\text{new}) > E(S_\text{old}) E(Snew)>E(Sold)
- 冗余度 D ( S new ) < D ( S old ) D(S_\text{new}) < D(S_\text{old}) D(Snew)<D(Sold)
这里可以把可读性、效率和冗余度用函数量化(例如代码行数、重复行数、构建耗时等)。
- 依赖消除与模块化
假设构建脚本包含多个重复命令块 C i C_i Ci,构建重构通过函数提取:
C common = extract ( C 1 , C 2 , … , C n ) C_\text{common} = \text{extract}(C_1, C_2, \dots, C_n) Ccommon=extract(C1,C2,…,Cn)
然后调用通用函数替代重复命令:
S new = S old − ⋃ i = 1 n C i + call ( C common ) S_\text{new} = S_\text{old} - \bigcup_{i=1}^{n} C_i + \text{call}(C_\text{common}) Snew=Sold−i=1⋃nCi+call(Ccommon)
这样可以消除重复,提高可读性。
3⃣ CMake/Makefile 示例(带注释)
# =========================== # CMake 构建重构示例 # =========================== cmake_minimum_required(VERSION 3.16) project(MyProject) # --------------------------- # 定义源文件列表 # --------------------------- set(SOURCES src/main.cpp src/module1.cpp src/module2.cpp ) # --------------------------- # 提取公共编译选项,避免重复 # --------------------------- set(COMMON_COMPILE_OPTIONS -Wall -Wextra -O2 ) # --------------------------- # 定义可执行文件 # --------------------------- add_executable(MyApp ${SOURCES}) # --------------------------- # 应用公共编译选项 # --------------------------- target_compile_options(MyApp PRIVATE ${COMMON_COMPILE_OPTIONS}) # --------------------------- # 链接外部库(避免重复配置) # --------------------------- find_package(fmt REQUIRED) target_link_libraries(MyApp PRIVATE fmt::fmt) # --------------------------- # 优化:启用并行构建 # --------------------------- # 在命令行 cmake --build . --parallel N 注释说明:
COMMON_COMPILE_OPTIONS:消除重复的编译选项,减少脚本冗余target_compile_options:统一应用选项,提升可理解性find_package+target_link_libraries:避免多次重复配置库依赖- 并行构建参数可减少构建时间,提高效率
增量构建(Incremental Build)是 C/C++ 构建系统中的一个重要优化概念。它的核心思想是:只重新编译或链接那些自上次构建以来发生变化的文件或模块,而不是每次都从头构建整个项目。
1⃣ 理解
- 传统全量构建:每次执行构建命令,整个工程的所有源文件都被编译,所有目标文件都被链接,构建时间随工程规模增长而线性增加。
- 增量构建:构建系统会检查文件的 修改时间 或 内容哈希,只对那些 发生变化的源文件 重新编译,并更新受其影响的依赖模块。
特点:
| 特性 | 描述 |
|---|---|
| 构建速度快 | 只构建变动部分,减少冗余操作 |
| 自动依赖管理 | 构建系统会追踪源文件依赖关系 |
| 节约资源 | 减少 CPU、IO 和存储消耗 |
| 与全量构建等价 | 构建结果与全量构建一致,只是更高效 |
2⃣ 数学/逻辑抽象
设整个项目包含源文件集合:
S = s 1 , s 2 , … , s n S = {s_1, s_2, \dots, s_n} S=s1,s2,…,sn
构建函数:
B : S → O B: S \rightarrow O B:S→O
- O O O:构建产物集合(可执行文件、库等)
- 全量构建:
B full ( S ) = O B_\text{full}(S) = O Bfull(S)=O - 增量构建:设修改文件集合为 S Δ ⊆ S S_\Delta \subseteq S SΔ⊆S,增量构建只对 S Δ S_\Delta SΔ 及其依赖重新执行:
B incremental ( S Δ ) = O updated B_\text{incremental}(S_\Delta) = O_\text{updated} Bincremental(SΔ)=Oupdated
其中 O updated O_\text{updated} Oupdated 与全量构建 O O O 等价,但只重新生成受影响产物。
3⃣ C/C++ 构建系统示意
- Makefile 例子(简化):
# 所有源文件 SRC = main.cpp module1.cpp module2.cpp # 对应目标文件 OBJ = main.o module1.o module2.o # 可执行文件 MyApp: $(OBJ) g++ -o $@ $(OBJ) # 每个目标文件的依赖 main.o: main.cpp g++ -c main.cpp -o main.o module1.o: module1.cpp g++ -c module1.cpp -o module1.o module2.o: module2.cpp g++ -c module2.cpp -o module2.o 说明:
- 如果只有
module1.cpp被修改,make会:- 重新编译
module1.cpp → module1.o - 链接
MyApp(因为module1.o更新)
- 重新编译
main.o和module2.o不会被重复编译,这就是增量构建。
4⃣ 实践中的关键点
- 依赖追踪
- 构建系统必须知道每个源文件对哪些头文件、库文件有依赖。
- CMake、Make、Ninja 都能自动生成依赖信息(如
.d文件)。
- 文件变化检测
- 通过 时间戳 或 内容哈希 比较,判断文件是否发生变化。
- 缓存与并行构建
- 有些构建系统(如 ccache, sccache)可以缓存编译结果,进一步加快增量构建。
总结
增量构建就是 智能地只重建发生变化的部分,在大规模 C/C++ 工程中能显著降低构建时间,同时保持构建结果正确性。
构建重构 和 软件重构 的概念,并结合 物理设计
1⃣ 理解
你的内容可以理解为 两类重构:
| 类别 | 调整对象 | 特点 | 社区经验/工具支持 |
|---|---|---|---|
| 构建重构 | 构建脚本元素(Makefile/CMakeLists.txt等) | - 目的是提升构建效率和可维护性 - 不改变软件功能 | 社区经验较少,但方法成熟,有工具支撑(如 CMake, Ninja, ccache) |
| 软件重构 | 软件代码元素(源文件、函数、类) | - 有软件设计指导 - 目的是提高代码可读性、可维护性和性能 - 通常有成熟方法论(如 Martin Fowler 的 Refactoring) | 社区经验丰富,工具支撑成熟(IDE 内建重构工具) |
核心区别:构建重构主要关注物理设计层面的脚本和构建流程软件重构关注逻辑设计和代码实现
2⃣ 逻辑关系示意
可以用一个简单的层次图表示:
┌───────────────┐ │ 软件重构 │ │ (逻辑设计层) │ │ - 有设计指导 │ └───────┬───────┘ │ ▼ ┌───────────────┐ │ 构建重构 │ │ (物理设计层) │ │ - 无设计指导 │ └───────────────┘ - 软件重构和构建重构是 平行且互补的活动
- 软件重构更强调逻辑与架构
- 构建重构更强调构建效率和物理流程优化
3⃣ 数学化理解
假设:
- 软件代码元素集合 C = c 1 , c 2 , … , c n C = {c_1, c_2, \dots, c_n} C=c1,c2,…,cn
- 构建脚本元素集合 B = b 1 , b 2 , … , b m B = {b_1, b_2, \dots, b_m} B=b1,b2,…,bm
① 构建重构函数
构建重构可以表示为函数 R b R_b Rb:
R b : B old → B new , 目标: F ( B old , C ) = F ( B new , C ) R_b: B_\text{old} \rightarrow B_\text{new}, \quad \text{目标: } F(B_\text{old}, C) = F(B_\text{new}, C) Rb:Bold→Bnew,目标: F(Bold,C)=F(Bnew,C)
- 也就是说,调整构建脚本元素 B B B 后,构建结果不变
- 优化指标:
- 构建效率 E ( B new ) > E ( B old ) E(B_\text{new}) > E(B_\text{old}) E(Bnew)>E(Bold)
- 冗余度 D ( B new ) < D ( B old ) D(B_\text{new}) < D(B_\text{old}) D(Bnew)<D(Bold)
- 可读性 R ( B new ) > R ( B old ) R(B_\text{new}) > R(B_\text{old}) R(Bnew)>R(Bold)
② 软件重构函数
软件重构函数 R c R_c Rc:
R c : C old → C new , 目标: O ( C old ) = O ( C new ) R_c: C_\text{old} \rightarrow C_\text{new}, \quad \text{目标: } O(C_\text{old}) = O(C_\text{new}) Rc:Cold→Cnew,目标: O(Cold)=O(Cnew)
- O ( C ) O(C) O(C) 表示软件功能/输出
- 优化指标:
- 可读性 R ( C new ) > R ( C old ) R(C_\text{new}) > R(C_\text{old}) R(Cnew)>R(Cold)
- 可维护性 M ( C new ) > M ( C old ) M(C_\text{new}) > M(C_\text{old}) M(Cnew)>M(Cold)
- 设计一致性 S ( C new ) > S ( C old ) S(C_\text{new}) > S(C_\text{old}) S(Cnew)>S(Cold)
区别在于: R b R_b Rb 关注物理流程和构建效率, R c R_c Rc 关注软件逻辑设计和可维护性。
4⃣ 构建重构示例(CMake)
# ========================= # 构建重构示例 # ========================= cmake_minimum_required(VERSION 3.16) project(RefactorExample) # ------------------------- # 提取公共编译选项,避免重复 # ------------------------- set(COMMON_OPTIONS -Wall -Wextra -O2) # ------------------------- # 源文件 # ------------------------- set(SOURCES main.cpp module1.cpp module2.cpp ) # ------------------------- # 定义可执行文件 # ------------------------- add_executable(MyApp ${SOURCES}) # ------------------------- # 应用公共选项(提高可读性和维护性) # ------------------------- target_compile_options(MyApp PRIVATE ${COMMON_OPTIONS}) # ------------------------- # 链接外部库示例 # ------------------------- # find_package(fmt REQUIRED) # target_link_libraries(MyApp PRIVATE fmt::fmt) 注释说明:
- 公共编译选项 → 避免重复,提高可读性
- 源文件列表统一管理 → 降低修改成本
- 可执行文件目标与库链接模块化 → 支持增量构建和复用
5⃣ 软件重构示例(C++)
// 原始代码voidprintMessage(){ std::cout <<"Hello"<< std::endl; std::cout <<"World"<< std::endl;}// 重构后:提升可维护性voidprintMessage(){for(auto msg :{"Hello","World"}){ std::cout << msg << std::endl;}}- 功能不变
- 可读性和可扩展性提高
- 软件重构关注代码逻辑设计
总结
- 构建重构
- 调整构建脚本
- 目标:效率、可读性、可维护性
- 无软件设计指导
- 软件重构
- 调整代码元素
- 目标:逻辑可维护性、设计一致性
- 有设计指导,方法成熟
- 数学/逻辑映射
R b : B old → B new , R c : C old → C new , B new 与 C new 最终生成结果不变 R_b: B_\text{old} \rightarrow B_\text{new}, \quad R_c: C_\text{old} \rightarrow C_\text{new}, \quad B_\text{new} \text{与 } C_\text{new} \text{最终生成结果不变} Rb:Bold→Bnew,Rc:Cold→Cnew,Bnew与 Cnew最终生成结果不变 - 实践建议
- 构建重构结合增量构建工具(CMake/Ninja/ccache)
- 软件重构结合 IDE 或重构工具(Clion, VSCode, Resharper)
构建多态 vs 运行多态 的概念
构建多态静态库链接文件选择宏选择运行多态动态库加载对象多态指针构建依赖增强,性能更高更加灵活,开销变大
1⃣ 理解
核心概念
- 构建多态(Compile-time Polymorphism)
- 发生在 编译阶段,通过 静态选择 实现功能的多态。
- 典型方式:
- 静态库链接:根据选择的库文件在编译阶段决定实现
- 文件选择:不同实现文件在编译时选择
- 宏选择:使用宏控制条件编译
- 优点:
- 运行效率高(编译期确定调用目标)
- 构建依赖增强(编译期已解决依赖)
- 缺点:
- 灵活性有限
- 需要重新编译才能切换实现
- 运行多态(Run-time Polymorphism)
- 发生在 运行阶段,通过 动态选择 实现多态。
- 典型方式:
- 动态库加载(Shared Library / DLL)
- 对象多态(虚函数、接口)
- 指针 / 引用 作为多态调用接口
- 优点:
- 灵活性高(运行时选择实现)
- 可动态扩展功能
- 缺点:
- 开销更大(虚函数表查找、动态加载)
- 构建依赖减少,但运行效率稍低
图示理解:左侧圆圈:构建多态右侧圆圈:运行多态箭头表示从构建多态可以通过运行多态实现灵活性,但反过来不一定
2⃣ 逻辑关系
构建多态 (编译期多态) ──────► 运行多态 静态库链接 动态库加载 文件选择 对象多态 宏选择 指针调用 ↑ 性能更高 ↑ 灵活性增强 ↓ 构建依赖强 ↓ 开销增加 - 构建多态 → 性能高,构建依赖强
- 运行多态 → 灵活性高,但开销增加
- 软件设计通常从 构建多态演进到运行多态,逐步提高系统灵活性
3⃣ 数学化理解
构建多态
设 功能接口 为 F F F,不同实现为 I 1 , I 2 , … , I n I_1, I_2, \dots, I_n I1,I2,…,In:
F compile ( x ) = I k ( x ) , k 在编译期确定 F_\text{compile}(x) = I_k(x), \quad k \text{在编译期确定} Fcompile(x)=Ik(x),k在编译期确定
- k k k 可以通过宏定义或静态链接选择
- 编译期多态保证性能最优
运行多态
F run ( x ) = I k ( x ) , k 在运行期动态确定 F_\text{run}(x) = I_k(x), \quad k \text{在运行期动态确定} Frun(x)=Ik(x),k在运行期动态确定
- 通过虚函数或动态库实现
- 灵活性更高,但有额外开销
注:构建多态可以通过运行多态实现(例如先编译不同实现,再动态选择加载),但运行多态不一定能退化成构建多态。
4⃣ C++ 代码示例
构建多态示例(静态选择)
// 宏控制不同实现#defineUSE_IMPL_A#ifdefUSE_IMPL_Avoidrun(){ std::cout <<"Implementation A"<< std::endl;}#elsevoidrun(){ std::cout <<"Implementation B"<< std::endl;}#endifintmain(){run();// 编译期已确定调用哪个实现return0;}- 编译时决定调用哪个实现
- 性能高,无虚函数开销
运行多态示例(虚函数/动态选择)
#include<iostream>#include<memory>// 接口类classCar{public:virtualvoiddrive()=0;// 运行多态virtual~Car()=default;};// 实现类AclassCarA:publicCar{public:voiddrive()override{ std::cout <<"CarA driving"<< std::endl;}};// 实现类BclassCarB:publicCar{public:voiddrive()override{ std::cout <<"CarB driving"<< std::endl;}};intmain(){ std::unique_ptr<Car> car;bool useA =true;// 可以运行期决定if(useA) car = std::make_unique<CarA>();else car = std::make_unique<CarB>(); car->drive();// 运行期动态绑定return0;}- 运行期选择具体实现
- 灵活性高,但有虚函数开销
总结
| 特性 | 构建多态 | 运行多态 |
|---|---|---|
| 选择时机 | 编译期 | 运行期 |
| 技术手段 | 宏、静态库、文件选择 | 虚函数、动态库、指针调用 |
| 灵活性 | 低 | 高 |
| 性能 | 高 | 低 |
| 构建依赖 | 强 | 弱 |
软件设计趋势:
- 系统初期可能用构建多态以优化性能
- 随系统复杂度增加,逐步演进到运行多态,提高灵活性
C/C++ 构建重构安全网 的概念
1⃣ 理解
核心概念
- 构建重构安全网
- 指在 不改变软件功能 的前提下,对构建系统(Makefile、CMake、构建脚本等)进行重构。
- 安全网作用:确保构建重构不会破坏原有构建结果或引入隐藏错误。
- 核心问题:构建重构不像软件重构那样直接有软件逻辑可测试,它主要作用于 物理层(构建脚本和依赖管理)。
- 自动化测试验证
- 理论上,可以通过自动化测试来验证构建重构是否安全。
- 具体方式:
- 执行构建 → 生成可执行文件/库
- 执行自动化测试用例,验证功能行为是否一致
- 实际挑战:
- 没有自动化测试用例 → 无法覆盖构建重构的验证
- 自动化用例不全 → 只能覆盖部分功能
- 自动测试执行时间过长 → 安全网反馈周期长,不适合作为快速验证
- 结论
- 构建重构安全网不能完全依赖自动化测试
- 需要结合:
- 增量构建验证
- 构建产物哈希对比
- 小规模功能测试
2⃣ 逻辑关系示意
┌────────────────────────┐ │ 构建重构安全网 │ │ │ │ - 验证构建脚本修改 │ │ - 保证构建结果一致 │ └─────────┬────────────┘ │ ┌────────────────┴─────────────────┐ │ │ 自动化测试可用? 手动/增量验证 - 完整测试集 → 可验证构建安全 - 哈希对比 - 不完整或耗时 → 只能部分验证 - 小规模回归测试 3⃣ 数学化理解
设:
- 源代码集合: C = c 1 , c 2 , … , c n C = {c_1, c_2, \dots, c_n} C=c1,c2,…,cn
- 构建脚本集合: B = b 1 , b 2 , … , b m B = {b_1, b_2, \dots, b_m} B=b1,b2,…,bm
- 构建产物函数: F ( B , C ) → O F(B, C) \rightarrow O F(B,C)→O
构建重构安全网公式
- 构建重构函数 R b R_b Rb:
B new = R b ( B old ) B_\text{new} = R_b(B_\text{old}) Bnew=Rb(Bold) - 构建安全网验证目标:
F ( B new , C ) = ? F ( B old , C ) F(B_\text{new}, C) \stackrel{?}{=} F(B_\text{old}, C) F(Bnew,C)=?F(Bold,C)
- O new = F ( B new , C ) O_\text{new} = F(B_\text{new}, C) Onew=F(Bnew,C)
- O old = F ( B old , C ) O_\text{old} = F(B_\text{old}, C) Oold=F(Bold,C)
如果自动化测试集合 T = t 1 , t 2 , … , t k T = {t_1, t_2, \dots, t_k} T=t1,t2,…,tk 完整覆盖:
∀ t i ∈ T , t i ( O new ) = t i ( O old ) \forall t_i \in T, \quad t_i(O_\text{new}) = t_i(O_\text{old}) ∀ti∈T,ti(Onew)=ti(Oold) - 则可判定构建重构是安全的
- 但如果 T T T 不完整或执行时间过长,则安全性无法完全保证
4⃣ C++ 构建安全网示例(哈希校验)
#include<iostream>#include<fstream>#include<string>#include<openssl/sha.h>// 读取文件并计算SHA256 std::string sha256_file(const std::string &filename){ std::ifstream file(filename, std::ios::binary);if(!file)return""; SHA256_CTX ctx;SHA256_Init(&ctx);char buf[4096];while(file.read(buf,sizeof(buf))){SHA256_Update(&ctx, buf, file.gcount());}if(file.gcount()>0){SHA256_Update(&ctx, buf, file.gcount());}unsignedchar hash[SHA256_DIGEST_LENGTH];SHA256_Final(hash,&ctx); std::string hash_str;for(int i =0; i < SHA256_DIGEST_LENGTH;++i){char tmp[3];sprintf(tmp,"%02x", hash[i]); hash_str += tmp;}return hash_str;}intmain(){ std::string old_hash =sha256_file("MyApp_old"); std::string new_hash =sha256_file("MyApp_new");if(old_hash == new_hash) std::cout <<"构建重构安全网验证通过 ✓"<< std::endl;else std::cout <<"构建重构可能破坏安全网 "<< std::endl;}- 通过计算构建产物哈希,快速验证 构建结果一致性
- 不依赖完整自动化测试
- 可以作为构建重构的 快速安全网
5⃣ 总结
- 构建重构安全网
- 目的是确保构建重构不会破坏构建产物
- 自动化测试可以验证,但有局限
- 自动化测试限制
- 用例缺失 → 无法完全验证
- 执行时间长 → 验证反馈慢
- 安全网补充手段
- 哈希校验构建产物
- 小规模回归测试
- 增量构建验证
- 数学公式概念:
R b ( B old ) = B new , F ( B new , C ) = ? F ( B old , C ) R_b(B_\text{old}) = B_\text{new}, \quad F(B_\text{new}, C) \stackrel{?}{=} F(B_\text{old}, C) Rb(Bold)=Bnew,F(Bnew,C)=?F(Bold,C)
- T T T 完整时可通过测试验证
- T T T 不完整时,使用哈希或增量构建辅助验证
C/C++ 构建重构安全网 的问题:如何在构建重构后 保证生成二进制行为不变
1. Cut extra dependenceMacroStructFun declFunMacroStructFun declFun.h/.c/.cpp2. Change Link orderaa.obb.occ.oBuild targetStatic libshare libExecute file
1⃣ 理解
现象
在构建重构过程中,可能出现以下现象:
- 二进制大小会改变
- 可能是因为编译选项、宏定义或链接顺序发生变化
- 并不意味着功能一定改变,但需要验证
- 二进制内容会改变
- 文件头信息、调试符号、编译器生成的填充字节、静态库链接顺序等会导致内容不同
- 不同类型文件格式不同
.o、.a、.so、可执行文件都有不同格式- 构建顺序和链接顺序都会影响生成的二进制布局
核心问题
如何保证构建生成的二进制行为不变?
关键在于 功能等价验证,而不是二进制字节完全一致。
保证方法
- Cut extra dependence(裁剪额外依赖)
- 移除不必要的宏定义、结构体或函数声明
- 减少构建复杂性
- 避免二进制中多余的内容影响行为
- Change link order(改变链接顺序)
- 链接顺序可能影响符号解析和最终二进制布局
- 保证功能行为不变时,可通过自动化测试或哈希校验验证
- 最终 Build Target(构建目标)
- 静态库、共享库、可执行文件
- 验证方法:
- 自动化测试执行结果是否一致
- 哈希或内容差异分析(仅对非调试部分比较)
- 小规模回归测试
2⃣ 逻辑流程示意
源文件 (.h/.c/.cpp) │ ▼ 裁剪额外依赖 (Macro/Struct/Fun decl) │ ▼ 编译生成 .o 文件 │ ▼ 改变链接顺序 (aa.o / bb.o / cc.o) │ ▼ 生成 Build Target ┌──────────────┐ │ Static lib │ │ Share lib │ │ Executable │ └──────────────┘ │ ▼ 验证行为一致性 - 自动化测试 - 哈希/增量构建 - 回归验证 3⃣ 数学化理解
设:
- 源文件集合 S = s 1 , s 2 , … , s n S = {s_1, s_2, \dots, s_n} S=s1,s2,…,sn
- 构建脚本集合 B = b 1 , b 2 , … , b m B = {b_1, b_2, \dots, b_m} B=b1,b2,…,bm
- 编译产物集合 O = o 1 , o 2 , … , o k O = {o_1, o_2, \dots, o_k} O=o1,o2,…,ok
- 构建目标 T = F ( S , B ) T = F(S, B) T=F(S,B)
构建重构函数
B new = R b ( B old ) B_\text{new} = R_b(B_\text{old}) Bnew=Rb(Bold)
- 构建重构后,目标行为需保持:
F ( S , B new ) = ? F ( S , B old ) F(S, B_\text{new}) \stackrel{?}{=} F(S, B_\text{old}) F(S,Bnew)=?F(S,Bold)
链接顺序影响
假设编译后生成对象文件集合 O = a a . o , b b . o , c c . o O = {aa.o, bb.o, cc.o} O=aa.o,bb.o,cc.o,不同链接顺序:
T 1 = l i n k ( a a . o , b b . o , c c . o ) , T 2 = l i n k ( c c . o , a a . o , b b . o ) T_1 = link(aa.o, bb.o, cc.o), \quad T_2 = link(cc.o, aa.o, bb.o) T1=link(aa.o,bb.o,cc.o),T2=link(cc.o,aa.o,bb.o)
- 二进制内容可能不同: T 1 ≠ T 2 T_1 \neq T_2 T1=T2(字节层面)
- 功能行为应保持一致: O ( T 1 ) = O ( T 2 ) O(T_1) = O(T_2) O(T1)=O(T2)
这里 O ( T ) O(T) O(T) 表示二进制行为(执行结果、功能输出)
4⃣ C++ 构建安全网代码片段示例
自动化测试验证行为一致性
#include<iostream>#include<cstdlib>intmain(){// 假设我们有两个构建目标:MyApp_old / MyApp_newint ret1 =system("./MyApp_old > output_old.txt");int ret2 =system("./MyApp_new > output_new.txt");if(ret1 ==0&& ret2 ==0){// 使用 diff 验证输出一致性int diffRet =system("diff output_old.txt output_new.txt > /dev/null");if(diffRet ==0) std::cout <<"构建重构安全网验证通过 ✓"<< std::endl;else std::cout <<"输出不一致 ,构建可能破坏安全网"<< std::endl;}}哈希验证二进制内容(可选)
sha256sum MyApp_old MyApp_new - 注意:字节不同不一定说明功能不同
- 功能一致才是关键
5⃣ 总结
- 构建重构安全网目标:保证生成二进制行为不变,而不是字节完全一致
- 现象:
- 二进制大小变化
- 内容变化
- 不同类型文件格式差异
- 保障方法:
- 裁剪额外依赖(宏、结构体、函数声明)
- 控制链接顺序
- 使用自动化测试、哈希、增量构建验证
- 数学公式概念:
T new = F ( S , B new ) , T old = F ( S , B old ) , O ( T new ) = O ( T old ) T_\text{new} = F(S, B_\text{new}), \quad T_\text{old} = F(S, B_\text{old}), \quad O(T_\text{new}) = O(T_\text{old}) Tnew=F(S,Bnew),Told=F(S,Bold),O(Tnew)=O(Told)
核心原则:功能行为一致,二进制内容可允许变化
C/C++ 构建测试安全网 的校验内容,以及它是否能确保 100% 安全
BuildtargetOs toolsNmObjdumpReadelfLdd00000000f10 T _block_storage_charge00000000f70 T _block_storage_registerU dyld_stub_binder· 符号表linux-gate.so.1 => (0xffffe000)libdl.so.2 => /lib/libdl.so.2libc.so.6 => /lib/libc.so.6· 链接信息[Nr] Name Type Address Offset[ 1] .text PROGBITS 00000040[ 2] .data PROGBITS 00000128[ 3] .bss NOBITS 00000128· 代码段长度
1⃣ 理解
校验内容
构建测试安全网通常会检查以下几个方面:
- 符号表和类型
- 使用工具如
nm查看目标文件或库的符号表 - 确认函数、变量符号存在并且类型正确
- T 表示该符号在 代码段,U 表示未定义符号(依赖外部库)
- 使用工具如
- 动态库链接信息
- 使用
ldd或readelf -d检查共享库依赖 - 确保二进制文件链接到正确的动态库路径
- 使用
- 代码段/数据段长度
- 使用
readelf -S或objdump -h查看段长度 - 保证主要段大小没有异常变化,可间接判断逻辑一致性
- 使用
例如:
[ 1] .text PROGBITS 00000040 [ 2] .data PROGBITS 00000128 [ 3] .bss NOBITS 00000128 例如:
linux-gate.so.1 => (0xffffe000) libdl.so.2 => /lib/libdl.so.2 libc.so.6 => /lib/libc.so.6 例如:
00000000f10 T _block_storage_charge 00000000f70 T _block_storage_register U dyld_stub_binder 构建测试安全网能否确保 100% 安全?
答案:不能
原因如下:
| 方法 | 可保证内容 | 局限性 |
|---|---|---|
| 符号表检查 | 函数/变量存在 | 不能保证逻辑正确性或实现细节完全一致 |
| 链接信息检查 | 正确库依赖 | 无法保证运行时行为正确 |
| 段长度检查 | 数据/代码范围合理 | 不保证算法或逻辑结果一致 |
| 自动化功能测试 | 功能行为验证 | 测试用例不全或覆盖不足可能漏掉问题 |
总结:安全网能降低风险,但无法 100% 确保安全。构建重构仍需要多层验证,包括自动化测试、回归测试、哈希校验、增量构建验证等。
2⃣ 可视化逻辑(流程图)
Build Target │ ▼ +------------------+ | Os Tools | | Nm / Objdump | | Readelf / Ldd | +------------------+ │ ▼ +------------------+ | 校验内容 | | · 符号表/类型 | | · 链接信息 | | · 段长度 | +------------------+ │ ▼ 是否100%安全? ┌─────────┐ │ 否 │ │ 依赖多层验证 │ │ 自动化测试/哈希/回归 | └─────────┘ 3⃣ 数学化表示
设:
- 源代码集合 C = c 1 , c 2 , . . . , c n C = {c_1, c_2, ..., c_n} C=c1,c2,...,cn
- 构建脚本 B B B
- 构建目标函数 F ( C , B ) → T F(C, B) \rightarrow T F(C,B)→T(生成二进制)
定义安全网验证函数 V V V:
V ( T ) = { 1 , 符号表、链接信息、段长度满足规则 0 , 否则 V(T) = \begin{cases} 1, & \text{符号表、链接信息、段长度满足规则} \\ 0, & \text{否则} \end{cases} V(T)={1,0,符号表、链接信息、段长度满足规则否则
问题:即使 V ( T new ) = 1 V(T_\text{new}) = 1 V(Tnew)=1,仍然有可能:
行为 ( T new ) ≠ 行为 ( T old ) \text{行为}(T_\text{new}) \neq \text{行为}(T_\text{old}) 行为(Tnew)=行为(Told) - 因此安全网验证是 必要但不充分条件
4⃣ C++ 构建安全网示例(符号表 + 链接信息校验)
#include<cstdlib>#include<iostream>intmain(){// 检查符号表int ret_nm =system("nm MyApp_new > sym_new.txt");int ret_readelf =system("readelf -d MyApp_new > link_new.txt");int ret_objdump =system("objdump -h MyApp_new > section_new.txt");if(ret_nm ==0&& ret_readelf ==0&& ret_objdump ==0){ std::cout <<"构建产物基本校验完成 ✓"<< std::endl;}else{ std::cout <<"构建产物校验失败 "<< std::endl;}// 注意:这只是校验,不能保证100%安全}5⃣ 总结
- 校验内容:
- 符号表、类型
- 动态库链接信息
- 代码段/数据段长度
- 安全性局限:
- 构建测试安全网只能 降低风险
- 无法100%保证行为一致性
- 仍需结合:
- 自动化功能测试
- 增量构建验证
- 哈希/差异对比
- 核心原则:
V ( T ) = 1 ⇒ 产物基本正确,但行为仍需进一步验证 V(T) = 1 \quad \Rightarrow \text{产物基本正确,但行为仍需进一步验证} V(T)=1⇒产物基本正确,但行为仍需进一步验证
安全网是 必要但非充分条件
安全网流程分为三个阶段
1. 备份构建结果
- 在进行任何构建重构前,先保存现有构建产物,以便对比和回滚。
- 校验内容包括:
- 二进制文件
- 符号表
- 安装路径
2. 清空构建结果 & 对比
- 清理旧的构建产物,确保新的构建是干净环境下的。
- 对比新旧产物:
- 二进制文件对比 (
cmp或diff) - 符号表对比 (
nm输出) - 安装位置校验(检查目标路径下文件完整性)
- 二进制文件对比 (
- 遍历不同 工具链 进行构建,确保兼容性。
3. 构建重构与校验
- 遍历 产品宏定义,进行不同条件下的构建,检查:
- 构建是否成功
- 构建时间是否合理
- 产物大小和内容变化
- 自动化 Shell 脚本可以串联这些步骤,形成闭环测试安全网。
2⃣ 数学化表示
设:
- B old B_\text{old} Bold:旧的构建产物集合
- B new B_\text{new} Bnew:新构建产物集合
- 构建函数 F ( C , M , T ) → B F(C, M, T) \rightarrow B F(C,M,T)→B
- C C C:源码
- M M M:宏定义集合
- T T T:工具链集合
- 对比函数 D ( B old , B new ) → 0 , 1 D(B_\text{old}, B_\text{new}) \rightarrow {0,1} D(Bold,Bnew)→0,1
- 1 1 1 表示产物一致
- 0 0 0 表示不一致
遍历宏定义与工具链:
∀ m ∈ M , ∀ t ∈ T , D ( F ( C , m , t ) ∗ new , B ∗ old ) = 1 \forall m \in M, \forall t \in T, \quad D(F(C, m, t)*\text{new}, B*\text{old}) = 1 ∀m∈M,∀t∈T,D(F(C,m,t)∗new,B∗old)=1
- 若条件成立,则构建重构安全网通过验证
- 否则,需要进一步检查重构或环境差异
3⃣ Shell 脚本示例(自动化构建安全网)
#!/bin/bash# ===============================# Step 1: 备份旧构建结果# ===============================mkdir -p backup_build cp -r build/* backup_build/ # ===============================# Step 2: 清空旧构建并新建# ===============================rm -rf build/* mkdir -p build # 遍历工具链 TOOLCHAINSTOOLCHAINS=("gcc""clang")MACROS=("DEBUG""RELEASE")forTOOLin"${TOOLCHAINS[@]}";doexportCC=$TOOLforMin"${MACROS[@]}";doecho"Building with TOOL=$TOOL, MACRO=$M..."# 设置宏定义exportBUILD_MACRO=$M# 调用构建脚本 ./build.sh # ===============================# Step 3: 校验构建产物# ===============================echo"Checking binary differences..."cmp build/myapp backup_build/myapp ||echo"Binary changed!"echo"Checking symbol table..." nm build/myapp > build_sym.txt nm backup_build/myapp > old_sym.txt diff build_sym.txt old_sym.txt ||echo"Symbols differ!"echo"Checking installation location..."if[! -f /usr/local/bin/myapp ];thenecho"Installed file missing!"fiecho"Build complete for TOOL=$TOOL, MACRO=$M"donedoneecho"=== 构建安全网测试完成 ==="注释说明
- 保存旧产物用于对比
- 保证新构建环境干净
- 遍历工具链和宏定义
- 遍历不同构建条件,检查重构是否破坏兼容性
- 确保功能一致性
- 安装位置校验
- 检查最终执行文件是否正确安装
二进制和符号表对比
cmp build/myapp backup_build/myapp nm build/myapp > build_sym.txt diff build_sym.txt old_sym.txt 清空构建目录
rm -rf build/* mkdir -p build 备份构建结果
cp -r build/* backup_build/ 4⃣ 关键点总结
- 自动化安全网基于:
- 工具链遍历
- 宏定义遍历
- 二进制、符号表、安装位置校验
- 保证范围:
- 可以大幅降低构建重构引入的问题
- 无法保证100%安全,仍需功能回归测试
- 优势:
- 快速发现构建脚本或环境导致的产物差异
- 支撑多工具链、多配置的兼容性验证
C/C++ 构建重构坏味道
1⃣ 理解:C/C++ 构建重构坏味道
所谓 坏味道 (Bad Smell),指的是在构建脚本和物理设计中存在的潜在问题,会导致维护难度大、易出错、构建效率低。
常见坏味道分类:
A. 构建脚本层面
- 代码重复
- 多个构建模块重复编写相同逻辑。
- 问题:修改某逻辑需在多处同步更新,易出错。
- 过多的全局变量
- 构建脚本依赖全局变量存储路径、配置或状态。
- 问题:不同构建任务互相干扰,增加维护成本。
- 构建脚本与模块位置不一致
- 构建逻辑与源代码模块物理位置不匹配。
- 问题:难以定位依赖关系,容易出现路径错误。
- 变量作用域太长
- 构建变量未局部化,作用域覆盖整个脚本。
- 问题:容易被意外修改或污染。
- 构建脚本中包含大量绝对路径
- 写死路径
/usr/local/lib/...或/home/user/... - 问题:不利于移植和多人协作。
- 写死路径
- 依赖冗余的动态库
- 链接了不必要的动态库
.so/.dll - 问题:增加构建复杂度和运行依赖。
- 链接了不必要的动态库
- 过长的构建脚本
- 脚本逻辑过多、行数过长,难以理解和维护。
- 过多逻辑判断
- if/else 判断堆叠过多,例如判断平台、配置等。
- 冗余编译宏
- 使用不再需要或重复的宏定义。
- 过多依赖环境变量
- 构建依赖
PATH,LD_LIBRARY_PATH等外部环境配置,容易受外部环境干扰。
- 构建依赖
B. 物理设计层面
- 过长头文件
- 一个头文件包含太多内容,模块之间耦合高。
- 解决方式:头文件拆分,物理设计调整。
- 头文件位置不合理
- 头文件目录结构不清晰,构建系统难以自动找到依赖。
- 解决方式:移动头文件、调整目录结构。
2⃣ 构建重构方法
针对坏味道,有 新增、替换、删除 三类重构操作:
A. 新增
- 新增测试
- 为构建脚本和产物增加自动化测试,形成安全网。
- 提取依赖库
- 将重复依赖提取成独立库:
- 静态库
.a - 动态库
.so/.dll
- 静态库
- 将重复依赖提取成独立库:
- 提取构建函数
- 将重复构建逻辑封装成函数,减少重复代码。
B. 替换
- 提取构建变量
- 将常量、路径、宏定义集中管理,减少全局变量。
- 删除全局变量
- 将全局变量改为局部变量或函数参数传递。
- 头文件拆分
- 将大头文件拆成多个模块化小头文件,减少耦合。
C. 删除
- 头文件移动
- 将头文件移动到合理目录,提高构建可维护性。
3⃣ Shell / CMake 构建重构示例
示例:提取构建变量
#!/bin/bash# ✗ 坏味道:全局变量写死路径# BUILD_DIR="/home/user/project/build"# ✓ 改进:通过函数和参数传递变量build_project(){localbuild_dir=$1mkdir -p "$build_dir"echo"Building project in $build_dir..."# 调用构建命令make -C "$build_dir"}# 调用函数 build_project "./build/debug" build_project "./build/release"示例:头文件拆分
// ✗ 坏味道:big_header.h 包含所有模块#include<stdio.h>#include<stdlib.h>#include"module_a.h"#include"module_b.h"#include"module_c.h"// ✓ 改进:每个模块独立头文件#include"module_a.h"// 仅包含需要的模块#include"module_c.h"4⃣ 总结
- 构建坏味道会降低可维护性、可读性和构建效率。
- 重构策略:
- 新增:测试、提取依赖库、提取函数
- 替换:提取变量、删除全局变量、头文件拆分
- 删除:头文件移动、冗余宏、重复逻辑
- 自动化构建和安全网可以进一步减少重构风险。
好的,我们来详细分析 C/C++ 构建重构中头文件路径、链接顺序和构建脚本位置对编译结果和二进制文件的影响,并加上解释、注释和示例。
1⃣ 调整头文件路径依赖顺序
理解
- 在 C/C++ 中,编译器查找头文件的顺序是 从最先指定的路径开始查找。
- 如果不同路径下有同名头文件,头文件查找顺序会决定最终包含的内容。
示例:
// include路径顺序影响结果// 假设目录结构如下:// /usr/include/foo.h// ./include/foo.h#include"foo.h"- 编译命令:
g++ -I./include -I/usr/include main.cpp -o main - 编译器会先在
./include查找foo.h,找不到再去/usr/include。 - 结论:如果两个
foo.h内容不同,调整 include 路径顺序会影响编译结果。
代码片段注释:
// ✗ 坏味道:多个路径下有同名头文件,顺序不明确#include"foo.h"// 哪个 foo.h 被引用取决于 -I 参数顺序// ✓ 改进:避免同名头文件冲突,或明确路径#include"./include/foo.h"2⃣ 调整源文件链接顺序
理解
- 在 C/C++ 中,链接器 从左到右处理对象文件和库。
- 链接顺序会影响:
- 是否成功解析符号
- 静态库中符号是否被选中
- 二进制文件大小(静态库可能有未用函数被剔除或保留)
示例:
# 假设 libA.a 包含 func1, func2# main.o 调用 func1 g++ main.o -L. -lA -o app # 链接顺序 A 在 main.o 后 g++ -L. -lA main.o -o app # 链接顺序 A 在 main.o 前#未使用的符号被保留 或 链接失败。
- 结论:调整源文件或库链接顺序可能 影响二进制文件大小,尤其是静态库和重复符号存在时。
代码片段注释:
# ✗ 坏味道:随意调整静态库顺序 g++ main.o -lA -lB -lC -o app # ✓ 改进:按照依赖顺序明确库顺序 g++ main.o -lC -lB -lA -o app 3⃣ 调整构建脚本位置
理解
- 构建脚本(Makefile / CMakeLists.txt / shell 脚本)位置本身不会直接改变二进制内容。
- 但可能间接影响:
- 生成的调试信息
.dbg或符号表路径(如编译器会写入源文件路径) - 构建产物中的文件路径信息和调试信息大小
- 生成的调试信息
- 结论:调整构建脚本位置不会改变执行文件功能,但可能影响 dbg 文件大小 或符号路径信息。
示例:
# 改变脚本位置 project/ ├─ src/ ├─ build/ └─ scripts/build.sh # 移动到 project/build/build.sh# 执行构建cd build ../scripts/build.sh # 编译器会记录源文件相对路径,dbg 文件可能不同4⃣ 总结表
| 操作 | 是否影响编译结果 | 是否影响二进制文件大小 | 是否影响 dbg 文件大小 |
|---|---|---|---|
| 调整头文件路径依赖顺序 | ✓ 会影响 | 可能(宏和类型不同) | 可能 |
| 调整源文件/库链接顺序 | ✗ 功能不变 | ✓ 可能 | 可能 |
| 调整构建脚本位置 | ✗ 功能不变 | ✗ 一般不变 | ✓ 可能(路径信息) |
5⃣ 总结理解
- 头文件路径顺序 → 决定最终使用的宏、结构体、函数声明 → 影响编译结果
- 链接顺序 → 决定静态库符号是否被选中 → 影响二进制文件大小
- 构建脚本位置 → 不影响功能,但会改变 dbg / 调试信息中路径 → 可能影响 dbg 文件大小