C++ 静态库与动态库详解及 CMake 构建指南

一、引言

在软件开发中,库(Library)\是指一组预先编写好的、可复用的代码模块,旨在简化开发过程、提高代码复用性和维护性。库本质上是一种可执行代码的二进制形式,可以被操作系统载入内存执行。根据链接方式的不同,库主要分为*静态库(Static Library)*和**动态库(Dynamic Library)

二、库的基本概念

1. 静态库(Static Library)

  • 文件后缀
    • Windows 下:.lib
    • Unix/Linux 下:.a
  • 特点
    • 编译时链接:在编译过程中,静态库的代码会被复制并链接到最终的可执行文件中。
    • 独立性:生成的可执行文件不依赖于外部库文件,运行时无需额外的库支持。
    • 体积较大:因为库代码被嵌入到每个使用它的可执行文件中,导致可执行文件体积增加。
    • 版本一致性:不同项目使用的静态库版本是固定的,更新库后需重新编译依赖的可执行文件以应用新版本。

2. 动态库(Dynamic Library)

  • 文件后缀
    • Windows 下:.dll
    • Unix/Linux 下:.so
  • 特点
    • 运行时链接:动态库在程序运行时加载,库代码不被嵌入到可执行文件中。
    • 共享性:多个可执行文件可以共享同一个动态库,节省内存和存储空间。
    • 灵活性:更新动态库后,所有依赖该库的程序无需重新编译即可使用新版本。
    • 依赖性:运行时需要确保动态库文件存在,否则程序无法启动。

三、静态库与动态库的对比

特性 静态库(.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

六、注意事项

  1. 宏定义与符号导出
    • 在动态库的头文件中,通过宏定义控制符号的导出和导入。在 Windows 平台,使用 __declspec(dllexport) 导出符号,__declspec(dllimport) 导入符号;而在 Unix/Linux 平台,通常不需要特殊标记。
  2. 跨平台兼容性
    • 不同平台的动态库文件后缀和符号导出方式不同。CMake 可以根据平台条件设置宏定义和库属性,确保库在各个平台上的正确构建和使用。
  3. 安装与部署
    • 使用 install 指令指定库文件和头文件的安装路径,使得其他项目可以方便地引用和链接这些库。
    • 动态库需要在运行时能够被找到,可以通过环境变量 PATH(Windows)或 LD_LIBRARY_PATH(Unix/Linux)设置库的查找路径,或将库文件放在系统默认路径下。
  4. 依赖管理
    • 对于较大的项目,可以使用 CMake 的 find_packageadd_subdirectory 来管理多个库和依赖,保持项目结构清晰。
  5. 版本控制
    • 设置库的版本号,有助于管理不同版本的库及其兼容性。例如,使用 VERSIONSOVERSION 属性控制动态库的主版本和次版本,便于动态链接器进行版本匹配。

封装任务管理系统

日志库设计

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月初春

results matching ""

    No results matching ""