1. 项目概述:为什么我们需要亲手打造DLL与LIB库?
在C++开发的世界里,尤其是Windows平台,DLL(动态链接库)和LIB(静态链接库)是构建复杂软件系统的基石。你可能无数次在项目属性里添加过某个xxx.lib,或者在程序启动时被“找不到xxx.dll”的弹窗搞得焦头烂额。这些库文件,本质上是一种代码复用和模块化开发的实践。直接使用现成的库固然方便,但当你需要封装自己的核心算法、设计跨项目的通用模块,或者为第三方提供SDK时,亲手创建和配置这些库就成了必备技能。
最近在社区里,诸如“vscode配置c++环境”、“dll文件丢失”、“error: a required dll could not be found”、“vs找不到.lib文件”这类问题频繁出现,这恰恰说明了理解库的生成、使用和部署全链路的重要性。很多人只停留在“引用”层面,一旦环境稍有变化,比如换了编译工具链、升级了Visual Studio版本,或者尝试在VSCode中配置,就会遇到各种链接错误和运行时异常。这背后的根本原因,是对库的生成机制、依赖关系以及不同配置(Debug/Release、x86/x64)之间的差异理解不够深入。
本文将从零开始,带你完整走一遍在Visual Studio(作为主流IDE代表)和CMake(作为跨平台构建代表)两种环境下,创建、编译、配置和使用你自己的DLL与LIB库的全过程。我会重点拆解那些官方文档一笔带过,但在实际项目中会让你踩坑的细节,比如导出符号的规范、运行时库的匹配、路径配置的玄学,以及如何设计一个“好用”的库接口。无论你是想封装自己的数学工具集,还是为团队提供一套稳定的中间件,这些经验都能让你少走弯路。
2. 核心概念与方案选型:静态库与动态库的抉择
在动手之前,我们必须厘清静态库(.lib)和动态库(.dll + .lib)的根本区别,这决定了你的架构设计。
2.1 静态库:合二为一的打包方式
静态库,通常以.lib(Windows)或.a(Linux/macOS)为后缀。它的工作方式非常直接:在编译链接阶段,编译器会将你的程序代码和静态库中所有被用到的代码“复制粘贴”到一起,最终生成一个独立的、庞大的可执行文件(.exe)。
优点:
- 部署简单:生成的可执行文件是自包含的,不需要额外携带库文件,避免了“DLL地狱”(Dll Hell)问题。
- 性能可能略有优势:由于所有代码都在一个模块内,函数调用没有额外的跳转开销(虽然现代系统优化下这点差异很小)。
- 版本控制单一:你只需要确保编译时链接的库版本正确即可。
缺点:
- 可执行文件体积大:如果多个程序使用同一个静态库,那么每个程序都会包含一份该库的代码副本,造成磁盘和内存空间的浪费。
- 更新困难:库代码有bug或需要升级时,你必须重新编译并发布整个应用程序。
- 无法动态加载:不能在程序运行时决定加载或卸载某个模块。
适用场景:小型工具、对部署简便性要求极高的应用(如单个绿色软件)、或者库代码非常稳定且几乎不会更改的核心基础模块。
2.2 动态库:分工合作的插件模式
动态库,在Windows上表现为.dll(动态链接库)文件,配合一个引导用的.lib(导入库)。它的思想是“共享”和“延迟绑定”。你的程序(主模块)和DLL模块是分开编译的。在链接时,程序只链接一个很小的导入库(.lib),这个导入库不包含实际代码,只包含如何找到DLL中函数的“地址簿”。直到程序运行时,操作系统才会将所需的DLL加载到内存中,并将函数调用“对接”上。
优点:
- 节省磁盘和内存:多个应用程序可以共享内存中同一份DLL代码。
- 更新灵活:修复库的bug或增加功能后,通常只需替换DLL文件,主程序无需重新编译(前提是接口兼容)。
- 支持插件架构:程序可以在运行时动态加载或卸载DLL,实现高度的可扩展性。
缺点:
- 部署复杂:你必须确保目标机器上存在正确版本的DLL,且路径能被系统找到。这就是“找不到xxx.dll”错误的根源。
- 存在依赖风险:如果DLL被意外替换或不兼容的版本覆盖,可能导致所有依赖它的程序崩溃(DLL地狱)。
- 轻微的运行时开销:涉及模块间的函数调用跳转。
适用场景:大型软件套件(如Office)、需要热更新功能的系统、为第三方提供SDK、或者模块需要独立升级的插件化系统。
注意:在Windows的VC++环境下,当你构建一个DLL项目时,编译器会生成两个关键文件:一个是包含实际代码的
.dll文件,另一个是较小的.lib(导入库)。应用程序链接时用的是这个导入库,运行时才需要.dll。而构建静态库项目,则只生成一个包含所有代码的.lib文件。务必分清这两个.lib文件的本质区别。
2.3 工具链选型:IDE与构建系统
- Visual Studio (MSVC):在Windows上进行C++开发的事实标准。它提供了最集成、最便捷的图形化界面来创建和管理库项目,对Windows特有的导出符号(
__declspec(dllexport))支持最好。适合快速原型开发、Windows专属项目或团队主要使用VS的情况。 - CMake + 编译器(MSVC/GCC/Clang):跨平台构建的首选。通过编写
CMakeLists.txt脚本,你可以用同一套配置为Visual Studio、Makefile、Ninja等生成器生成项目文件。这对于需要支持Linux/macOS,或者希望构建过程可版本化、可复现的项目至关重要。VSCode配置C++环境也高度依赖CMake。
我们的实操将覆盖这两种主流方式,确保你无论在哪套工具链下都能游刃有余。
3. 实战一:使用Visual Studio创建与配置库
我们首先使用Visual Studio 2022进行演示,这是最直观的方式。
3.1 创建动态链接库项目
- 新建项目:打开VS2022,选择“创建新项目”。在搜索框中输入“动态链接库”,选择“动态链接库(DLL)”模板,点击下一步。
- 配置项目:为项目命名,例如
MyMathDLL,选择合适的位置和解决方案名称。 - 理解生成的文件:项目创建后,你会看到几个核心文件:
pch.h/pch.cpp:预编译头文件,用于加速编译,对于小型库不是必须的,可以先忽略或禁用。dllmain.cpp:DLL的入口点。里面有一个DllMain函数,类似于可执行程序的main函数。除非你需要精细控制DLL加载/卸载时的行为(如初始化全局变量、创建线程局部存储),否则通常不需要修改它。对于纯算法库,保持其默认实现即可。framework.h:包含一些Windows头文件和宏定义。
3.2 设计并导出你的接口
这是创建DLL最核心也最容易出错的一步。我们需要明确哪些函数/类是对外公开的(需要导出),哪些是内部实现的(不需要导出)。
步骤1:定义导出宏为了代码能同时在构建DLL(导出)和使用DLL(导入)时编译,我们需要一个通用的宏。在MyMathDLL.h(可以新建此头文件,或直接在pch.h中定义)中添加:
// MyMathDLL.h #pragma once // 定义一个通用的导出/导入宏 #ifdef MYMATHDLL_EXPORTS #define MYMATH_API __declspec(dllexport) #else #define MYMATH_API __declspec(dllimport) #endif原理:当我们在DLL项目内部编译时,编译器定义MYMATHDLL_EXPORTS(VS项目属性中默认已定义),此时MYMATH_API扩展为__declspec(dllexport),告诉编译器这个符号需要被导出到DLL中。当其他项目包含此头文件并使用DLL时,由于没有定义MYMATHDLL_EXPORTS,MYMATH_API扩展为__declspec(dllimport),告诉编译器这个符号需要从外部DLL导入。
步骤2:声明和实现导出函数/类现在,我们用MYMATH_API来修饰要导出的内容。
// MyMathDLL.h (接上文) MYMATH_API int add(int a, int b); MYMATH_API double multiply(double a, double b); // 导出一个类 class MYMATH_API Calculator { public: Calculator(); double accumulate(double value); double getResult() const; private: double m_total; };// MyMathDLL.cpp #include "pch.h" // 或 #include "MyMathDLL.h" #include "MyMathDLL.h" // 实现普通导出函数 MYMATH_API int add(int a, int b) { return a + b; } MYMATH_API double multiply(double a, double b) { return a * b; } // 实现导出类的成员函数 Calculator::Calculator() : m_total(0.0) {} double Calculator::accumulate(double value) { m_total += value; return m_total; } double Calculator::getResult() const { return m_total; }3.3 编译生成与文件产出
- 在VS顶部的工具栏,选择正确的解决方案配置(Debug/Release)和解决方案平台(x86/x64)。请务必注意,使用库的应用程序必须与库的配置和平台完全匹配,否则会导致链接错误或运行时崩溃。
- 右键点击项目,选择“生成”。如果成功,你会在输出窗口看到类似“
MyMathDLL.dll和MyMathDLL.lib已生成”的信息。 - 打开项目输出目录(通常是
$(SolutionDir)$(Platform)$(Configuration)\,例如项目路径\x64\Debug\),你会找到:MyMathDLL.dll:动态库本体,运行时需要。MyMathDLL.lib:导入库,链接时需要。MyMathDLL.exp:导出文件,链接大型DLL时可能需要,一般可忽略。MyMathDLL.pdb:调试符号文件(Debug配置下),用于调试。
3.4 在另一个项目中调用我们的DLL
现在,我们新建一个控制台应用TestApp来使用刚才创建的DLL。
- 添加头文件路径:在
TestApp项目属性中,进入“C/C++” -> “常规” -> “附加包含目录”,添加MyMathDLL.h头文件所在的目录路径。 - 添加导入库路径:进入“链接器” -> “常规” -> “附加库目录”,添加
MyMathDLL.lib文件所在的目录(即DLL项目的输出目录)。 - 指定导入库:进入“链接器” -> “输入” -> “附加依赖项”,添加
MyMathDLL.lib。 - 编写测试代码:
// TestApp.cpp #include <iostream> #include "MyMathDLL.h" // 包含我们导出的头文件 int main() { std::cout << "Add: " << add(5, 3) << std::endl; std::cout << "Multiply: " << multiply(2.5, 4.0) << std::endl; Calculator calc; calc.accumulate(10.5); calc.accumulate(20.3); std::cout << "Calculator total: " << calc.getResult() << std::endl; return 0; } - 编译与运行:
- 编译链接:确保
TestApp的配置(Debug/Release, x86/x64)与MyMathDLL完全一致,然后生成。链接器会通过我们指定的.lib文件找到函数入口信息。 - 运行:直接运行
TestApp.exe可能会失败,并弹出“无法找到MyMathDLL.dll”的错误。这是因为系统在运行时搜索DLL的路径中,不包含我们生成DLL的目录。
- 编译链接:确保
3.5 解决运行时DLL查找问题
系统查找DLL的顺序通常是:1)应用程序所在目录;2)系统目录(如C:\Windows\System32);3)PATH环境变量中的目录。
解决方案(按推荐度排序):
- 将DLL复制到可执行文件目录:这是最简单可靠的方法。在
TestApp的生成后事件中,添加一个复制命令,或者手动将MyMathDLL.dll拷贝到TestApp.exe所在的输出目录。- 在VS中配置生成后事件:在
TestApp项目属性中,进入“生成事件” -> “生成后事件” -> “命令行”,添加:
(请根据实际项目路径调整)xcopy /y "$(SolutionDir)..\MyMathDLL\$(Platform)\$(Configuration)\MyMathDLL.dll" "$(OutDir)"
- 在VS中配置生成后事件:在
- 将DLL目录添加到系统PATH:不推荐用于开发调试,因为会污染全局环境。但在部署时,可以将你的应用目录加入PATH。
- 使用
SetDllDirectoryAPI(Windows):在应用程序启动时,通过代码临时添加DLL搜索路径。这给了你更多的控制权,但需要修改代码。
实操心得:在开发阶段,我强烈推荐第一种方法(配置生成后事件)。它清晰地将依赖关系绑定在项目配置里,任何拉取你代码的人,只要编译成功,就能直接运行,无需手动拷贝文件。这也是很多开源项目采用的方式。
3.6 创建静态库项目
静态库的创建更简单。在VS中新建项目时选择“静态库(.lib)”。代码编写上,无需任何__declspec导出修饰符,因为所有代码最终都会被直接链接进去。
// MyMathLib.h #pragma once int add(int a, int b); // 普通声明即可// MyMathLib.cpp #include "MyMathLib.h" int add(int a, int b) { return a + b; }编译后只生成一个MyMathLib.lib文件。在使用时,只需要在应用程序项目中配置“附加包含目录”(头文件)和“附加依赖项”(.lib文件)即可,无需处理运行时DLL。最终的可执行文件会变大,但它是独立的。
4. 实战二:使用CMake进行跨平台库管理
对于跨平台项目或希望构建过程更透明的团队,CMake是更好的选择。我们将创建同样的数学库,但使用CMake脚本。
4.1 项目目录结构
假设我们的项目根目录为MyMathCMake,结构如下:
MyMathCMake/ ├── CMakeLists.txt # 根CMake脚本 ├── include/ │ └── MyMathCMake/ # 公共头文件,通常按项目名再套一层,避免冲突 │ └── MyMath.h ├── src/ # 库的源代码 │ ├── CMakeLists.txt │ └── MyMath.cpp └── apps/ # 测试应用程序 ├── CMakeLists.txt └── test_app.cpp4.2 编写根CMakeLists.txt
# MyMathCMake/CMakeLists.txt cmake_minimum_required(VERSION 3.15) project(MyMathCMake LANGUAGES CXX) # 设置C++标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 设置输出目录,让生成的文件更规整 set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # 添加子目录 add_subdirectory(src) add_subdirectory(apps)4.3 编写库的CMakeLists.txt与源代码
# MyMathCMake/src/CMakeLists.txt # 添加一个动态库目标 add_library(MyMathShared SHARED MyMath.cpp) # 设置目标的头文件包含路径,PUBLIC属性意味着使用此库的目标也会自动包含这个路径 target_include_directories(MyMathShared PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include> $<INSTALL_INTERFACE:include> ) # 设置导出宏,在编译此库时定义MYMATH_EXPORTS target_compile_definitions(MyMathShared PRIVATE MYMATH_EXPORTS) # 添加一个静态库目标(可选,演示如何同时构建两种) add_library(MyMathStatic STATIC MyMath.cpp) target_include_directories(MyMathStatic PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include> $<INSTALL_INTERFACE:include> ) # 静态库无需导出宏定义 # 为了方便,我们可以设置一个别名,让外部通过一个统一的名字来链接 add_library(MyMath::Shared ALIAS MyMathShared) add_library(MyMath::Static ALIAS MyMathStatic)头文件设计(支持跨平台导出):
// MyMathCMake/include/MyMathCMake/MyMath.h #pragma once // 跨平台的导出宏定义 #if defined(_WIN32) && defined(MYMATH_EXPORTS) #define MYMATH_API __declspec(dllexport) #elif defined(_WIN32) #define MYMATH_API __declspec(dllimport) #else // Linux/macOS 或其他平台,通常使用编译器可见性属性,这里简单定义为空 #define MYMATH_API __attribute__((visibility("default"))) #endif MYMATH_API int add(int a, int b); MYMATH_API double multiply(double a, double b); class MYMATH_API Calculator { // ... 同上 ... };源文件实现:
// MyMathCMake/src/MyMath.cpp #include "MyMathCMake/MyMath.h" MYMATH_API int add(int a, int b) { return a + b; } MYMATH_API double multiply(double a, double b) { return a * b; } Calculator::Calculator() : m_total(0.0) {} double Calculator::accumulate(double value) { /*...*/ } double Calculator::getResult() const { /*...*/ }4.4 编写应用程序的CMakeLists.txt
# MyMathCMake/apps/CMakeLists.txt # 创建可执行文件 add_executable(test_app test_app.cpp) # 链接我们创建的动态库。使用target_link_libraries,CMake会自动处理头文件路径和库依赖。 target_link_libraries(test_app PRIVATE MyMath::Shared) # 如果你想链接静态库,只需改为: # target_link_libraries(test_app PRIVATE MyMath::Static)// MyMathCMake/apps/test_app.cpp #include <iostream> #include <MyMathCMake/MyMath.h> // 注意包含路径 int main() { // 测试代码同上 std::cout << "Add: " << add(5, 3) << std::endl; Calculator calc; // ... return 0; }4.5 构建与测试
- 生成构建系统:在项目根目录打开终端(或使用VSCode的CMake插件)。
mkdir build cd build cmake .. -G "Visual Studio 17 2022" -A x64 # Windows上生成VS解决方案 # 或者在Linux/macOS上: # cmake .. -DCMAKE_BUILD_TYPE=Debug - 编译:
cmake --build . --config Debug # 指定编译Debug版本 - 运行:编译后,在
build/bin/Debug(或build/bin)目录下找到test_app.exe(或test_app)。由于CMake已经帮我们设置了输出目录,并且可执行文件和DLL在同一个bin目录下,因此可以直接运行,不会出现找不到DLL的错误。
注意事项:CMake的
target_link_libraries命令非常强大。它不仅仅是在链接器命令中添加一个-lMyMathShared,还会自动传递所有相关的属性,如头文件包含路径、编译定义、链接的其他库等。这是现代CMake(Target-based)的最佳实践,避免了手动管理目录的繁琐和错误。
5. 高级议题与避坑指南
掌握了基本创建流程后,下面这些深水区的问题才是区分新手和老手的关键。
5.1 导出C接口与消除Name Mangling
C++编译器为了实现函数重载等特性,会对函数名进行“名字修饰”(Name Mangling),这导致导出的函数名变得不可读(如?add@@YAHHH@Z)。如果你希望DLL能被C语言、C#、Python等其他语言调用,就需要使用extern "C"来禁止名字修饰,并注意调用约定(通常是__stdcall或__cdecl,VC++默认__cdecl)。
// 在头文件中 #ifdef __cplusplus extern "C" { #endif // 导出为C风格函数,名字不会被修饰 MYMATH_API int __cdecl add_c(int a, int b); #ifdef __cplusplus } #endif使用extern "C"后,导出的函数名在DLL中就是简单的add_c,方便其他语言通过GetProcAddress等API动态加载。但代价是失去了C++的函数重载和类导出能力。
5.2 运行时库的匹配问题
这是“Debug和Release版本DLL混用”导致崩溃的元凶。在VS项目属性“C/C++” -> “代码生成” -> “运行时库”选项中,有四个值:
/MT:静态链接多线程运行时库(Release)。/MTd:静态链接多线程调试运行时库(Debug)。/MD:动态链接多线程运行时库(Release)。/MDd:动态链接多线程调试运行时库(Debug)。
黄金法则:你的应用程序和所有它链接的库(无论是静态.lib还是动态.dll)必须使用相同的运行时库选项。如果DLL用/MD编译,而主程序用/MT编译,它们会各自拥有一份不同的堆(heap)管理机制,在一个模块中分配的内存,在另一个模块中释放就会导致堆损坏,引发难以调试的崩溃。
解决方案:在项目配置中统一设置。对于要分发给他人的库,通常建议使用/MD或/MDd(动态链接运行时库),这样可以减少库文件大小,并且多个模块共享同一份运行时库。在你的库文档中必须明确说明所需的运行时库类型。
5.3 符号可见性与剥离
默认情况下,GCC/Clang编译的动态库会导出所有全局符号。这可能导致符号冲突,并增大库文件。可以使用编译器标志来控制:
- GCC/Clang:在编译时添加
-fvisibility=hidden,然后只在需要导出的函数/类上使用__attribute__((visibility("default")))(我们的MYMATH_API宏在非Windows平台就做了这个事)。这能显著减少动态库的导出表大小,提升加载速度,并增强安全性。 - MSVC:主要通过
__declspec(dllexport)来控制,不标记的默认不导出。
5.4 版本管理与二进制兼容性
当你需要升级库时,如何保证老版本的应用程序还能正常运行?
- 只添加,不修改或删除:保证原有的导出函数/类的接口(函数名、参数类型、顺序、调用约定)和内存布局(对于类,成员变量的顺序和类型)绝对不变。可以添加新的导出函数或新的类。
- 使用接口类(纯虚类):这是保证二进制兼容性的银弹。导出一个只包含纯虚函数的抽象接口类,具体的实现类在DLL内部创建并返回接口指针。只要接口类不变,其实现类可以任意修改。
这样,即使// ICalculator.h class MYMATH_API ICalculator { public: virtual ~ICalculator() = default; virtual double calculate(double input) = 0; // 工厂函数,在DLL中实现 static ICalculator* create(); };ICalculator的实现类CalculatorImpl增加了成员变量,因为用户代码只通过指针操作接口,内存布局的变化不会影响到调用者。 - 语义化版本号:为你的DLL文件命名加入版本号,如
MyLibrary_v1.2.dll,并在接口中提供版本查询函数。
5.5 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 链接错误 LNK2019: 无法解析的外部符号 | 1. 未正确链接.lib文件。 2. 函数声明与定义不匹配(如调用约定 __stdcallvs__cdecl)。3. C++函数名修饰问题,尝试用 extern "C"。4. 库的架构(x86/x64)与应用程序不匹配。 | 1. 检查“附加依赖项”和“附加库目录”。 2. 使用 dumpbin /exports YourDLL.dll查看导出的确切函数名。3. 确保项目和依赖项全部统一为x86或x64。 |
| 运行时错误:找不到xxx.dll | 1. DLL未放置在应用程序搜索路径中。 2. 依赖的次级DLL(如VC++运行时库 msvcp140.dll)缺失。 | 1. 将DLL复制到exe同目录,或修改PATH。 2. 使用Dependency Walker或 dumpbin /dependents查看DLL依赖,并确保所有依赖都存在。对于VC++运行时,可安装对应的“Microsoft Visual C++ Redistributable”。 |
| 程序崩溃,错误指向DLL中的内存操作 | 1. 运行时库不匹配(/MT vs /MD)。 2. DLL和exe使用了不同的堆管理器(一个分配,另一个释放)。 3. 接口边界传递了复杂对象(如STL容器),双方编译器版本不一致。 | 1.强制统一所有项目的运行时库设置。 2. 遵循“谁分配,谁释放”原则,在接口边界提供明确的创建/销毁函数。 3. 避免直接传递STL对象跨越DLL边界,改用POD类型(基本类型、结构体)或指针。 |
| Debug版正常,Release版崩溃或反之 | 1. 未初始化的变量在Debug版被编译器自动填充(如0xCDCDCDCD),而Release版没有。 2. 断言(assert)在Release版中被禁用,掩盖了逻辑错误。 3. 优化导致的行为差异。 | 1. 确保所有变量都正确初始化。 2. 使用日志代替断言来记录关键状态。 3. 在Release版中也开启基本调试信息(/Zi),并逐步关闭优化选项来定位。 |
6. 设计一个健壮的库:最佳实践总结
基于以上所有内容,要设计一个易于使用、易于维护、兼容性好的库,请遵循以下原则:
- 明确的接口边界:最小化导出内容,只暴露必要的函数和类。内部实现细节完全隐藏。
- 使用纯虚接口类保证二进制兼容性:对于需要长期维护和迭代的库,这是最稳妥的方案。
- 资源管理权责清晰:如果接口分配了内存或资源,必须提供对应的释放函数,并明确文档说明调用者的责任。
- 提供完整的头文件和文档:头文件就是你的用户手册。为每个导出函数和类编写清晰的注释,说明功能、参数、返回值、异常和线程安全性。
- 处理好异常:确保异常不会跨越DLL边界抛出,除非双方使用完全相同版本和配置的编译器运行时库。通常建议在接口边界捕获所有异常,并转换为错误码返回。
- 线程安全:明确声明你的库是否是线程安全的。如果不是,需要在文档中显著标出。
- 提供版本信息:在DLL资源中或通过专用函数提供版本号、编译时间、依赖项等信息。
- 统一的构建配置:使用CMake等现代构建系统,可以轻松生成导出头文件、管理依赖、并支持多种编译器和平台。
亲手创建和配置DLL/LIB库,是C++开发者从“使用者”迈向“架构者”的关键一步。这个过程充满了细节和陷阱,但每一次踩坑和解决问题的经历,都会让你对程序编译、链接和运行的机制有更深的理解。从简单的函数库开始尝试,逐步应用到你的实际项目中,你会发现模块化设计带来的可维护性和复用性提升是巨大的。