C++ 静态库与动态库详解及 CMake 构建指南
一、引言
在软件开发中,库(Library)\是指一组预先编写好的、可复用的代码模块,旨在简化开发过程、提高代码复用性和维护性。库本质上是一种可执行代码的二进制形式,可以被操作系统载入内存执行。根据链接方式的不同,库主要分为*静态库(Static Library)*和**动态库(Dynamic Library)。
二、库的基本概念
1. 静态库(Static Library)
- 文件后缀:
- Windows 下:
.lib
- Unix/Linux 下:
.a
- Windows 下:
- 特点:
- 编译时链接:在编译过程中,静态库的代码会被复制并链接到最终的可执行文件中。
- 独立性:生成的可执行文件不依赖于外部库文件,运行时无需额外的库支持。
- 体积较大:因为库代码被嵌入到每个使用它的可执行文件中,导致可执行文件体积增加。
- 版本一致性:不同项目使用的静态库版本是固定的,更新库后需重新编译依赖的可执行文件以应用新版本。
2. 动态库(Dynamic Library)
- 文件后缀:
- Windows 下:
.dll
- Unix/Linux 下:
.so
- Windows 下:
- 特点:
- 运行时链接:动态库在程序运行时加载,库代码不被嵌入到可执行文件中。
- 共享性:多个可执行文件可以共享同一个动态库,节省内存和存储空间。
- 灵活性:更新动态库后,所有依赖该库的程序无需重新编译即可使用新版本。
- 依赖性:运行时需要确保动态库文件存在,否则程序无法启动。
三、静态库与动态库的对比
特性 | 静态库(.lib/.a) | 动态库(.dll/.so) |
---|---|---|
链接时间 | 编译时 | 运行时 |
文件大小 | 可执行文件较大 | 可执行文件较小,库单独存放 |
更新与维护 | 更新库需要重新编译所有依赖项 | 更新库无需重新编译,多个程序可共享库 |
内存使用 | 每个程序独立加载库代码 | 多个程序共享同一库的内存空间 |
平台依赖性 | 较少(生成的可执行文件独立) | 需要确保运行环境中存在相应的动态库 |
性能 | 链接时已完成,可能略快于动态库 | 运行时需加载库,初次加载可能稍慢 |
四、C++ 库的创建与使用
1. 创建库的源代码示例
以一个简单的数学库 mymath
为例,提供计算整数和浮点数之和的功能。
a. 头文件 mymath.h
#ifndef MYMATH_H
#define MYMATH_H
#ifdef MYMATH_EXPORTS
#define MYMATH_API __declspec(dllexport)
#else
#define MYMATH_API __declspec(dllimport)
#endif
extern "C" {
MYMATH_API int add_int(int a, int b);
MYMATH_API float add_float(float a, float b);
}
#endif // MYMATH_H
解释
:
MYMATH_API
宏用于控制符号的导出与导入。在构建动态库时定义MYMATH_EXPORTS
以导出符号;在使用库时不定义该宏,以导入符号。extern "C"
避免 C++ 的名字修饰(Name Mangling),确保库函数具有 C 风格的接口,便于跨语言调用。
b. 源文件 mymath.cpp
#include "mymath.h"
int add_int(int a, int b) {
return a + b;
}
float add_float(float a, float b) {
return a + b;
}
2. 使用 CMake 构建库
CMake 是一个跨平台的自动化构建系统,能够简化构建过程。以下介绍如何使用 CMake 构建静态库和动态库。
a. 项目结构
假设项目结构如下:
MyMathLib/
├── CMakeLists.txt
├── include/
│ └── mymath.h
└── src/
└── mymath.cpp
b. 创建静态库的 CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(MyMathLib)
# 指定库的头文件路径
include_directories(${CMAKE_SOURCE_DIR}/include)
# 创建静态库
add_library(mymath STATIC src/mymath.cpp)
# 设置库的版本号(可选)
set_target_properties(mymath PROPERTIES VERSION 1.0.0 SOVERSION 1)
# 安装目标
install(TARGETS mymath
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
# 安装头文件
install(FILES include/mymath.h DESTINATION include)
c. 创建动态库的 CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(MyMathLib)
# 定义宏用于导出符号(仅在 Windows 平台)
if (WIN32)
add_definitions(-DMYMATH_EXPORTS)
endif()
# 指定库的头文件路径
include_directories(${CMAKE_SOURCE_DIR}/include)
# 创建动态库
add_library(mymath SHARED src/mymath.cpp)
# 设置库的版本号(可选)
set_target_properties(mymath PROPERTIES VERSION 1.0.0 SOVERSION 1)
# 安装目标
install(TARGETS mymath
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
# 安装头文件
install(FILES include/mymath.h DESTINATION include)
d. 同时构建静态库和动态库的 CMakeLists.txt
如果需要同时生成静态库和动态库,可以在同一个 CMakeLists.txt
中添加多个库目标:
cmake_minimum_required(VERSION 3.0)
project(MyMathLib)
# 定义宏用于导出符号(仅在 Windows 平台)
if (WIN32)
add_definitions(-DMYMATH_EXPORTS)
endif()
# 指定库的头文件路径
include_directories(${CMAKE_SOURCE_DIR}/include)
# 创建静态库
add_library(mymath_static STATIC src/mymath.cpp)
# 创建动态库
add_library(mymath_shared SHARED src/mymath.cpp)
# 设置动态库的版本号(可选)
set_target_properties(mymath_shared PROPERTIES VERSION 1.0.0 SOVERSION 1)
# 安装静态库和动态库
install(TARGETS mymath_static mymath_shared
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
# 安装头文件
install(FILES include/mymath.h DESTINATION include)
3. 编译库
在项目根目录下执行以下命令以编译库:
mkdir build
cd build
cmake ..
cmake --build .
根据 CMakeLists.txt
的配置,这将在 build
目录下生成静态库和/或动态库文件。
五、在项目中使用库
1. 示例项目结构
假设有一个应用程序项目 MyApp
,其结构如下:
MyApp/
├── CMakeLists.txt
├── main.cpp
2. 编写应用程序源代码 main.cpp
#include "mymath.h"
#include <iostream>
int main() {
int sum_int = add_int(3, 4);
float sum_float = add_float(3.5f, 4.2f);
std::cout << "Sum (int): " << sum_int << std::endl;
std::cout << "Sum (float): " << sum_float << std::endl;
return 0;
}
3. 编写应用程序的 CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(MyApp)
# 指定库的头文件路径
include_directories(${CMAKE_SOURCE_DIR}/../MyMathLib/include)
# 添加可执行文件
add_executable(MyApp main.cpp)
# 链接静态库或动态库
# 静态库
target_link_libraries(MyApp ${CMAKE_SOURCE_DIR}/../MyMathLib/build/lib/libmymath_static.a)
# 或者动态库(需要确保运行时可以找到动态库)
# target_link_libraries(MyApp ${CMAKE_SOURCE_DIR}/../MyMathLib/build/lib/libmymath_shared.so)
注意:
- 对于 Windows 平台,动态库的链接和部署可能需要额外配置,确保
.dll
文件在可执行文件的同一目录或系统路径下。 - 使用相对路径链接库时,应确保路径的正确性,或考虑使用 CMake 的
find_library
等功能进行更灵活的库查找。
4. 构建并运行应用程序
在 MyApp
项目根目录下执行:
mkdir build
cd build
cmake ..
cmake --build .
./MyApp
运行后,应输出:
Sum (int): 7
Sum (float): 7.7
六、注意事项
- 宏定义与符号导出:
- 在动态库的头文件中,通过宏定义控制符号的导出和导入。在 Windows 平台,使用
__declspec(dllexport)
导出符号,__declspec(dllimport)
导入符号;而在 Unix/Linux 平台,通常不需要特殊标记。
- 在动态库的头文件中,通过宏定义控制符号的导出和导入。在 Windows 平台,使用
- 跨平台兼容性:
- 不同平台的动态库文件后缀和符号导出方式不同。CMake 可以根据平台条件设置宏定义和库属性,确保库在各个平台上的正确构建和使用。
- 安装与部署:
- 使用
install
指令指定库文件和头文件的安装路径,使得其他项目可以方便地引用和链接这些库。 - 动态库需要在运行时能够被找到,可以通过环境变量
PATH
(Windows)或LD_LIBRARY_PATH
(Unix/Linux)设置库的查找路径,或将库文件放在系统默认路径下。
- 使用
- 依赖管理:
- 对于较大的项目,可以使用 CMake 的
find_package
或add_subdirectory
来管理多个库和依赖,保持项目结构清晰。
- 对于较大的项目,可以使用 CMake 的
- 版本控制:
- 设置库的版本号,有助于管理不同版本的库及其兼容性。例如,使用
VERSION
和SOVERSION
属性控制动态库的主版本和次版本,便于动态链接器进行版本匹配。
- 设置库的版本号,有助于管理不同版本的库及其兼容性。例如,使用
封装任务管理系统
日志库设计
Logger.h
声明不变
#ifndef LOGGER_LOGGER_H
#define LOGGER_LOGGER_H
#include <fstream>
#include <mutex>
#ifdef _WIN32
#ifdef LOGGER_BUILDING_DLL
#define LOGGER_API __declspec(dllexport)
#else
#define LOGGER_API __declspec(dllimport)
#endif
#else
#define LOGGER_API
#endif
LOGGER_API class Logger {
public:
//获取单例
static Logger& getInstance();
//禁止拷贝和赋值
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
//记录日志
void log(const std::string & message);
~Logger();
private:
Logger();
std::ofstream logFile;
std::mutex mutex;
};
#endif //LOGGER_LOGGER_H
Logger.cpp
具体实现也不变
#include "Logger.h"
Logger& Logger::getInstance() {
static Logger logger;
return logger;
}
Logger::Logger(){
logFile.open("log.txt", std::ios::app);
if(!logFile.is_open()){
throw std::runtime_error("Failed to open log file");
}
}
Logger::~Logger(){
if(logFile.is_open()){
logFile.close();
}
}
void Logger::log(const std::string& message){
std::lock_guard<std::mutex> lock(mutex);
if(logFile.is_open()){
auto now = std::chrono::system_clock::now();
auto now_time = std::chrono::system_clock::to_time_t(now);
char buffer[100];
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", std::localtime(&now_time));
logFile << std::string(buffer) << ": " << message << std::endl;
}
}
Logger库编写
cmake_minimum_required(VERSION 3.28)
project(Logger)
set(CMAKE_CXX_STANDARD 17)
add_library(Logger SHARED Logger.cpp
)
target_compile_definitions(Logger PRIVATE LOGGER_BUILDING_DLL)
纯虚类设计
我们之前采用CRTP方式封装了任务管理系统,这一节用封装继承多态的思想封装这个任务,我们期望将Command
以及任务类等统一封装到一个libCommand.dll
中,对外只提供Command.h
文件,外界无法知晓Command
内部实现细节。
Command.h
仅提供纯虚类的声明,以及唯一一个获取纯虚类指针的函数GetCommand
#ifndef COMMAND_COMMAND_H
#define COMMAND_COMMAND_H
#include <string>
#include <memory>
#ifdef _WIN32
#ifdef COMMAND_BUILDING_DLL
#define COMMAND_API __declspec(dllexport)
#else
#define COMMAND_API __declspec(dllimport)
#endif
#else
#define COMMAND_API
#endif
class COMMAND_API Command{
public:
virtual void execute(const std::string& args) = 0;
};
COMMAND_API std::shared_ptr<Command> GetCommand(const std::string& command);
#endif //COMMAND_COMMAND_H
Command.cpp
提供GetCommand
的具体实现
#include "Command.h"
#include "CommandImpl.h"
#include <memory>
TaskManager mgsr;
std::shared_ptr<Command> GetCommand(const std::string& command){
if(command == "add"){
return std::make_shared<AddCommand>(mgsr);
}
if(command == "del"){
return std::make_shared<DeleteCommand>(mgsr);
}
if(command == "list"){
return std::make_shared<ListCommand>(mgsr);
}
if(command == "update"){
return std::make_shared<UpdateCommand>(mgsr);
}
return nullptr;
}
Command.cpp
中创建了不同的命令,并且定义了全局的TaskManager
重写纯虚类
命令的具体实现
//
// Created by secon on 2025/3/14.
//
#ifndef COMMAND_COMMANDIMPL_H
#define COMMAND_COMMANDIMPL_H
#include "Command.h"
#include "TaskManager.h"
#include "Logger.h"
class AddCommand: public Command {
public:
AddCommand(TaskManager& manager):taskManager(manager){}
void execute(const std::string& args){
//解析参数
size_t pos1 = args.find(',');
size_t pos2 = args.find(',',pos1+1);
if(pos1 == std::string::npos || pos2 == std::string::npos){
// Logger::getInstance().log("参数格式错误");
std::cout << "参数格式错误。请使用: add <描述>,<优先级>,<截止日期>" << std::endl;
return;
}
std::string description = args.substr(0,pos1);
int priority = std::stoi(args.substr(pos1+1,pos2-pos1-1));
std::string dueDate = args.substr(pos2+1);
taskManager.addTask(description,priority,dueDate);
std::cout << "任务添加成功."<< std::endl;
}
private:
TaskManager& taskManager;
};
class DeleteCommand: public Command {
public:
DeleteCommand(TaskManager& manager):taskManager(manager){}
void execute(const std::string& args){
try{
size_t pos ;
int id = std::stoi(args,&pos);
if(pos != args.length()){
std::cout << "参数格式错误。请使用: delete <ID>" << std::endl;
return;
}
taskManager.deleteTask(id);
std::cout << "任务删除成功."<< std::endl;
}catch(const std::invalid_argument& e){
Logger::getInstance().log("参数格式错误");
return;
}catch(const std::out_of_range& e){
Logger::getInstance().log("参数格式错误");
return;
}
}
private:
TaskManager& taskManager;
};
class ListCommand: public Command {
public:
ListCommand(TaskManager& manager):taskManager(manager){}
void execute(const std::string& args){
try{
int sortOption = 0;
if(!args.empty()){
sortOption = std::stoi(args);
}
std::cout << "当前任务列表:"<< std::endl;
taskManager.listTasks(sortOption);
}catch (const std::invalid_argument& e){
Logger::getInstance().log("参数格式错误");
return;
}catch (const std::out_of_range& e){
Logger::getInstance().log("参数格式错误");
return;
}
}
private:
TaskManager& taskManager;
};
class UpdateCommand: public Command {
public:
UpdateCommand(TaskManager& manager):taskManager(manager){}
void execute(const std::string& args){
try{
size_t pos1 = args.find(',');
size_t pos2 = args.find(',',pos1+1);
size_t pos3 = args.find(',',pos2+1);
if(pos1 == std::string::npos || pos2 == std::string::npos || pos3 == std::string::npos){
std::cout << "参数格式错误。请使用: update <ID>,<描述>,<优先级>,<截止日期>" << std::endl;
return;
}
int id = std::stoi(args.substr(0,pos1));
std::string description = args.substr(pos1+1, pos2-pos1-1);
int priority = std::stoi(args.substr(pos2+1, pos3-pos2-1));
std::string dueDate = args.substr(pos3+1);
taskManager.updateTask(id, description, priority, dueDate);
std::cout << "任务更新成功."<< std::endl;
}catch (const std::invalid_argument& e){
Logger::getInstance().log("参数格式错误");
return;
}catch (const std::out_of_range& e){
Logger::getInstance().log("参数格式错误");
return;
}
}
private:
TaskManager& taskManager;
};
#endif //COMMAND_COMMANDIMPL_H
TaskManager
和之前一样,无需改动
//
// Created by secon on 2025/3/14.
//
#ifndef COMMAND_TASKMANAGER_H
#define COMMAND_TASKMANAGER_H
#include "Task.h"
#include <vector>
#include <iostream>
#include <string>
#include <algorithm>
#include <fstream>
#include <sstream>
class TaskManager {
public:
TaskManager();
void addTask(const std::string& description, int priority, const std::string& date);
void deleteTask(int id);
void updateTask(int id, const std::string& description, int priority, const std::string& date);
void listTasks(int sortOption) const ; // 0: 按ID, 1:按优先级升序,2:按日期升序
void loadTasks();
void saveTasks() const;
private:
std::vector<Task> tasks;
int nextId;
static bool compareByPriority(const Task& a, const Task& b);
static bool compareByDueDate(const Task& a, const Task& b);
};
#endif //COMMAND_TASKMANAGER_H
具体实现
//
// Created by secon on 2025/3/14.
//
#include "TaskManager.h"
#include "TaskManager.h"
#include "Logger.h"
#include <iostream>
TaskManager::TaskManager():nextId(1) {
loadTasks();// 始化任务管理器,例如设置默认的线程池大小等。
}
void TaskManager::addTask(const std::string &description, int priority, const std::string &dueDate) {
Task task;
task.id = nextId++;
task.description = description;
task.priority = priority;
task.dueDate = dueDate;
tasks.push_back(task);
Logger::getInstance().log("Task added: " + task.toString());
saveTasks();// 保存任务到文件。
}
void TaskManager::deleteTask(int id){
auto it = std::find_if(tasks.begin(), tasks.end(), [id](const Task &task){
return task.id == id;
});
if(it != tasks.end()){
tasks.erase(it);
Logger::getInstance().log("Task deleted: " + it->toString());
saveTasks();// 保存任务到文件。
}else{
std::cout << "Task not found." << std::endl;
}
}
void TaskManager::updateTask(int id, const std::string &description, int priority, const std::string &date) {
auto it = std::find_if(tasks.begin(), tasks.end(), [id](const Task &task){
return task.id == id;
});
if(it != tasks.end()){
it->description = description;
it->priority = priority;
it->dueDate = date;
Logger::getInstance().log("Task updated: " + it->toString());
saveTasks();// 保存任务到文件。
}else{
std::cout << "Task not found." << std::
endl;
}
}
void TaskManager::saveTasks() const {
std::ofstream outFile("tasks.txt");
if(!outFile.is_open()) {
Logger::getInstance().log( "Failed to open tasks file for writing.");
return;
}
for(const auto &task : tasks){
outFile << task.id << "," << task.description << "," << task.priority << "," << task.dueDate << std::endl;
}
outFile.close();
Logger::getInstance().log("Tasks saved successfully.");
}
void TaskManager::listTasks(int sortOption) const {
std::vector<Task> sortedTasks = tasks;
switch (sortOption) {
case 1:
std::sort(sortedTasks.begin(), sortedTasks.end(), compareByPriority);
break;
case 2:
std::sort(sortedTasks.begin(), sortedTasks.end(), compareByDueDate);
break;
default:
// 不排序,直接输出原始顺序。
break;
}
for(const auto &task : sortedTasks){
std::cout << task.toString() << std::endl;
}
}
void TaskManager::loadTasks() {
std::ifstream inFile("tasks.txt");
if(!inFile.is_open()) {
Logger::getInstance().log( "Failed to open tasks file.");
return;
}
std::string line;
while(std::getline(inFile,line)){
std::istringstream iss(line);
Task task;
char delimiter;
iss >> task.id >> delimiter;
std::getline(iss, task.description,',');
iss >> task.priority >> delimiter;
iss >> task.dueDate;
tasks.push_back(task);
if(task.id >= nextId){
nextId = task.id + 1;
}
}
inFile.close();
Logger::getInstance().log("Tasks loaded successfully.");
}
bool TaskManager::compareByPriority(const Task &a, const Task &b) {
return a.priority < b.priority;
}
bool TaskManager::compareByDueDate(const Task &a, const Task &b) {
return a.dueDate < b.dueDate;
}
Task
实现也没有改变
#ifndef UNTITLED_TASK_H
#define UNTITLED_TASK_H
#include <string>
#include <sstream>
#include <iomanip>
class Task {
public:
int id;
std::string description;
int priority;
std::string dueDate;
std::string toString() const{
std::ostringstream oss;
oss << "ID: " << id
<<", 描述: " << description
<<", 优先级: " << priority
<<", 截止日期: " << dueDate;
return oss.str();
}
};
Command库编写
我们重写Command库的CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(Command)
set(CMAKE_CXX_STANDARD 17)
add_library(Command SHARED Command.cpp
Command.h
CommandImpl.cpp
CommandImpl.h
TaskManager.cpp
TaskManager.h
Task.cpp
Task.h)
# 定义导出宏
target_compile_definitions(Command PRIVATE COMMAND_BUILDING_DLL)
# 添加 Logger 的包含目录
target_include_directories(Command PRIVATE
${CMAKE_CURRENT_SOURCE_DIR} # 包含当前目录的头文件(如CommandExport.h)
../Logger # 包含Logger的头文件
)
# 指定Logger的库文件路径并链接
target_link_directories(Command PRIVATE ../Logger) # 假设Logger.lib和Logger.dll在此目录
target_link_libraries(Command PRIVATE Logger) # 链接Logger库
主函数调用
#include <iostream>
#include "Command.h"
#include "Logger.h"
#include "unordered_map"
int main() {
system("chcp 65001 > nul");
std::unordered_map<std::string, std::shared_ptr<Command>> commands;
commands.emplace("add", GetCommand("add"));
commands.emplace("delete", GetCommand("delete"));
commands.emplace("list", GetCommand("list"));
commands.emplace("update", GetCommand("update"));
std::cout << "欢迎使用任务管理系统!" << std::endl;
std::cout << "可用命令: add, delete, list, update, exit" << std::endl;
std::string input;
while (true) {
std::cout << "\n> ";
std::getline(std::cin, input);
if (input.empty()) continue;
// 分离命令和参数
size_t spacePos = input.find(' ');
std::string cmd = input.substr(0, spacePos);
std::string args;
if (spacePos != std::string::npos) {
args = input.substr(spacePos + 1);
}
if (cmd == "exit") {
std::cout << "退出程序。" << std::endl;
break;
}
auto it = commands.find(cmd);
if (it != commands.end()) {
it->second->execute(args);
} else {
std::cout << "未知命令:" << cmd << std::endl;
}
}
return 0;
}
CMakeLists.txt编写
cmake_minimum_required(VERSION 3.28)
project(untitled)
set(CMAKE_CXX_STANDARD 17)
add_executable(untitled main.cpp
)
# 添加 Logger 的包含目录
target_include_directories(untitled PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/Logger
${CMAKE_CURRENT_SOURCE_DIR}/Command
)
# 指定Logger的库文件路径并链接
target_link_directories(untitled PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/Logger
${CMAKE_CURRENT_SOURCE_DIR}/Command
)
target_link_libraries(untitled PRIVATE
Logger
Command
) # 链接Logger库
完结撒花
从2024年8月开始了第一个零基础C++视频的创作,历时七个多月完成了这一系列的创作,很多粉丝留言跟着我的节奏学会了C++基础用法,也能自己动手开发一些功能了,这让我很开心,比我自己学会了一套知识带来的快感还多。这套教程涵盖了C++98到C++20的内容,旨在帮助更多迷茫中找不到学习道路的人。
这套教程做完了,是结束也是新的开始,我会回归到聊天项目第二季的开发中,我觉得分享最好的状态就是陪伴,所以大家不用担心我更新完基础就离开。很多人问我为什么做免费教程,知识付费的时代做付费教程已成为大趋势,但我和别人不同的一点是我是真的喜欢技术的那类人,也喜欢把自己的认知分享出来,哪怕以后被时代淘汰,找不到合适的开发工作,甚至去做了其他行业,我也会在业余时间学习和分享编程技术,编程已经成为我生活中不可分割的一个乐趣。在分享中与大家交流,让我不再孤单,很多人素未相识,但是通过共同的兴趣走到一起,没有任何利益关系,这是一种君子之交淡如水的快乐,这种快乐和解忧杂货店那本书有些共鸣。
也祝福大家能学会C++并且找到合适的工作。再次感谢大家一路陪伴。 -------------------------恋恋风辰 写于2025年3月初春