C++:类与对象,封装,继承,多态,构造/析构,重载/重写

面向对象编程(OOP)

OOP的核心思想是用对象来模拟现实世界,将数据(属性)和操作数据的方法(行为)捆绑在一起。

1. 类与对象(基础)

  • :它定义了对象长什么样(成员变量)和能做什么(成员函数)。
  • 对象:是根据类创建出来的具体实体,占用内存空间。
  • 关键点 :访问修饰符(public公有,private私有,protected受保护)。private是实现封装的关键,隐藏内部细节,只暴露必要的接口。

2. 封装

封装包含两层含义:

  • 数据与操作的捆绑:将对象的属性(成员变量)和操作这些属性的行为(成员函数)捆绑在一起,形成一个独立的"类"单元。
  • 信息隐藏(信息隐蔽) :将对象内部的实现细节隐藏起来,只对外暴露必要的接口。外界只能通过你提供的公有(public)成员函数来访问或修改对象的状态,而无法直接操作内部数据。

C++中如何实现封装?

通过访问修饰符(Access Specifiers) 来控制访问权限:

修饰符 访问权限
public 完全公开。类外部、派生类、任何地方都可以访问。
private 完全私有只有 本类的成员函数(以及友元)可以访问。派生类也不能访问。这是最推荐的默认选择。
protected 受保护 。本类的成员函数和派生类可以访问,但类外部不能访问。用于继承体系中希望子类能使用的接口。

为什么需要封装?(封装的好处)

  • 安全性:防止外部代码随意修改对象内部状态,避免数据进入无效或不一致的状态。

    • 反面例子 :如果年龄(age)是公有成员,外部可以直接赋值为 -5。如果通过 setAge(int age) 方法,你可以在函数内部加入校验逻辑(如 if (age > 0 && age < 150)),保证数据的有效性
  • 降低耦合性(松耦合) :外部代码只依赖于类的公开接口(方法名、参数),而不依赖于内部实现。当你需要优化或修改内部逻辑(比如更换算法、改变成员变量类型)时,只要保持公开接口不变,外部调用方的代码无需改动,也不会编译报错 。这大大提升了代码的可维护性

  • 可控的访问粒度 :你可以选择只读(只提供getter)不提供setter,或者提供只写、读写等各种组合,精确控制外界对数据的操作权限。

封装的编程实践(经典"三原则")

  • 成员变量一律设为 private (极少数情况下才用 protected)。绝不设为 public。外部无法直接访问或修改它们。
  • 通过公有的 get / set 函数来间接访问(即使当前什么都不做,也为未来预留了扩展空间)。
  • 只暴露必要的接口 ,内部辅助函数(Helper Functions)应设为 private,对外不可见。

封装与"信息隐藏"的关系

很多人把封装等同于"把数据藏起来"。实际上:

  • 封装是手段(语法层面上的打包和权限控制)。
  • 信息隐藏是设计原则(指导思想)。

更准确的理解是

封装不仅仅是为了隐藏数据,更是为了隐藏"设计决策"

比如,你设计了一个 CacheManager 类,内部可能用 std::list + std::unordered_map 实现 LRU 算法。通过封装,外界调用者只知道 get(key)put(key, value),而完全不知道你用了什么数据结构。当你发现新算法更快,想换成 boost::intrusive::list 时,调用方代码零改动。这就是封装带来的最大价值。

封装在 LvglDisplay 类中的体现

基于 LVGL 的图形显示控制类,它把 LCD 预览、拍照、亮度调节、UI 刷新线程都包进一个类里:

  • public 暴露接口:start()stop()push_frame()take_photo()set_brightness()
  • private 隐藏实现:lvgl_loop()build_ui()update_preview_image() 和内部成员变量

这就是很标准的封装:

外部只管"怎么用",内部自己管理线程、缓冲区、UI 控件。

1. 成员变量全部私有(private)------ 封装的第一原则
c++ 复制代码
private:
    int screen_width_;
    int screen_height_;
    int preview_width_;
    int preview_height_;
    std::thread lvgl_thread_;
    std::atomic<bool> running_;
    // ... 所有成员变量都在 private 区域
  • 所有成员变量都是 private,外部无法直接访问或修改它们。
2. 通过公有(public)接口控制行为 ------ 封装的核心

外部只能通过以下公开方法与该类交互:

c++ 复制代码
public:
    bool start();                        // 启动
    void stop();                         // 停止
    void push_frame(Channel, const cv::Mat&);  // 推帧
    bool take_photo(Channel, const std::string&); // 拍照
    void set_brightness(int);            // 设置亮度
    int get_brightness() const;          // 获取亮度
    void set_photo_callback(PhotoCallback); // 设置回调
  • 这些公有方法就是对外暴露的接口
  • 外部调用者不需要知道 内部是如何实现的(LVGL 如何渲染、线程如何同步、背光如何调节),只需要知道"调用 push_frame 就能显示图像"即可。

封装的体现 :接口稳定,实现可变更。

将来如果你想更换 GUI 库(从 LVGL 换成 Qt 或 MiniGUI),只要保持这些公有接口不变,所有调用方代码零修改 ,只需要重新实现类内部的 private 函数即可。

3. 信息隐藏 ------ 隐藏设计决策

外部看不到的 private 成员和函数,隐藏了哪些"设计决策"?

隐藏的设计决策 代码中的体现
线程模型 外部不知道 start() 内部创建了 lvgl_thread_ 线程,也不知道 lvgl_loop() 是如何运行的。
图像缓冲机制 外部不知道 push_frame 内部是用 frame_mutex_ + latest_frames_ + has_new_frames_ 来实现线程安全的缓冲。
UI 布局细节 外部不知道 preview_panels_preview_imgs_waiting_labels_ 这些 LVGL 控件是如何组织和排列的。
背光调节方式 外部不知道 set_brightness 是通过 /sys/class/backlight/ 的 sysfs 写入来实现的。
拍照实现 外部不知道 take_photo 是将当前 preview_buffers_ 编码成 JPEG 并写入文件。

封装的精髓 :调用者只用接口,不需要关心这些实现细节。当硬件平台变化时(比如背光路径改变),只需要改 set_brightness 内部代码,接口不变。

4. 控制访问粒度 ------ getter/setter 的精细控制
c++ 复制代码
int get_brightness() const { return current_brightness_.load(); }
void set_brightness(int brightness);   // setter 有额外逻辑(写 sysfs)
  • 只有 getter,没有 setter 的成员 :比如 screen_width_screen_height_ 没有提供任何 setter,构造函数初始化后就只读,外部无法修改。这保证了分辨率在对象生命周期内不可变。
  • 既有 getter 又有 setterbrightness 提供了 getter 和 setter,但 setter 内部做了额外工作(调节硬件背光),不是简单的赋值。
  • 完全没有 getter/setter :比如 frame_mutex_preview_dscs_ 这些内部辅助成员,完全不暴露给外部。

这就是精确控制访问粒度------有些数据完全隐藏,有些只读,有些可读写且带校验或副作用。

5. 删除拷贝构造和赋值运算符 ------ 防止对象被意外复制
c++ 复制代码
LvglDisplay(const LvglDisplay&) = delete;
LvglDisplay& operator=(const LvglDisplay&) = delete;
  • 这个类管理了线程、互斥锁、LVGL 控件等不可复制的资源。
  • 如果允许拷贝,两个对象会持有同一份资源,析构时会造成重复释放或未定义行为。
  • 通过 = delete 显式禁止拷贝,从语法层面杜绝了这种错误。这是封装中"保护对象完整性"的极端体现。
6. 用 enum class 限定作用域 ------ 避免命名污染
C++ 复制代码
enum class Channel {
    Usb = 0,
    Mipi = 1
};
  • 使用 enum class(强类型枚举)而不是普通 enum
  • 外部调用时必须写 Channel::Usb,不能只写 Usb。这有效避免了全局命名空间污染,也防止了隐式转换为整数带来的误用。
  • 这虽然不是典型的"封装数据",但属于接口设计的封装思想------把相关的常量名封闭在类的作用域内。
7. 回调机制的解耦
c++ 复制代码
using PhotoCallback = std::function<void(const std::string&)>;
void set_photo_callback(PhotoCallback cb) { photo_callback_ = std::move(cb); }
  • 这个类允许外部设置一个回调函数,当拍照完成时触发。
  • 不关心外部具体怎么处理拍好的照片(是上传服务器、存储到本地还是发送邮件),只负责在合适的时机调用这个回调。
  • 这是封装 + 依赖倒置的体现:类本身不依赖具体业务逻辑,通过回调把决定权交给使用者。
封装原则 LvglDisplay 的实现
数据隐藏 全部成员变量放在 private
接口隔离 公有接口只有 6~7 个方法,职责清晰
实现独立 内部线程、LVGL、背光调节的细节完全不可见
防御性设计 删除拷贝构造,防止误复制;用原子变量和互斥锁保护共享数据
可扩展性 通过回调机制让外部注入业务逻辑,类本身保持通用

3. 继承(Inheritance)

  • 含义:允许一个类(子类/派生类)获得另一个类(父类/基类)的属性和方法。
  • 目的 :实现代码复用 和建立层次关系 (如:继承自动物)。
  • 注意 :继承方式(publicprotectedprivate)决定了父类成员在子类中的访问权限。实际开发中,public继承最常用,表示"is-a"(是一个)关系。

抽象基类 CommInterface

c++ 复制代码
// K210 通信接口基类
class CommInterface {
public:
    virtual ~CommInterface() = default;
    virtual bool open() = 0;
    virtual void close() = 0;
    virtual bool send(const std::vector<uint8_t>& data) = 0;
    virtual bool receive(std::vector<uint8_t>& data, size_t max_len, int timeout_ms) = 0;
    virtual bool isOpen() const = 0;
};

class 子类 : public 基类

子类 UARTComm : public CommInterface

C++ 复制代码
// UART 通信实现
class UARTComm : public CommInterface {
public:
    struct Config {
        std::string device;     // 串口设备,如 "/dev/ttyS1"
        int baudrate;           // 波特率,默认 115200
        int databits;           // 数据位,默认 8
        int stopbits;           // 停止位,默认 1
        char parity;            // 校验位,默认 'N'

        Config() 
            : device("/dev/ttyS1"), baudrate(115200), databits(8), stopbits(1), parity('N') {}
    };

    explicit UARTComm(const Config& config);
    ~UARTComm() override;

    bool open() override;
    void close() override;
    bool send(const std::vector<uint8_t>& data) override;
    bool receive(std::vector<uint8_t>& data, size_t max_len, int timeout_ms) override;
    bool isOpen() const override;

private:
    Config config_;
    int fd_;
    std::mutex mtx_;
};

4. 多态

  • 多态 :字面意思是"多种形态"。指父类指针/引用 指向子类对象时,调用同一个函数名,实际执行的是子类的版本。
  • 虚函数 :实现多态的机制。在父类函数前加 virtual,子类重写它。
  • 底层原理(关键) :每个含有虚函数的类都有一个虚函数表(vtable) ,里面存放着虚函数的地址。对象中有个虚指针(vptr) 指向这个表。当调用虚函数时,程序会通过vptr找到正确的表,再找到正确的函数地址执行。这就是运行时多态(动态绑定)。

1. 基类与派生类的继承体系(多态的前提)

K210Detector 里保存的是:

std::unique_ptr<CommInterface> comm_;

  • CommInterface 是一个抽象基类 (通常包含纯虚函数,如 virtual bool open() = 0;)。
  • UARTCommSPIComm公有继承CommInterface,并重写了 open() 等虚函数。

这就建立了多态的基础:父类指针/引用可以指向子类对象

C++ 复制代码
class K210Detector {
public:
    
    bool start();
    void stop();

private:

    Config config_;
    DetectionCallback callback_;
    std::unique_ptr<CommInterface> comm_;
    std::mutex comm_mutex_;      // 保护 comm_ 并发访问(L1 恢复与 read)

    
};

2. 运行时决定具体类型(动态绑定)

启动时根据配置决定实际创建哪个子类:

UARTCommSPIComm: // 创建通信接口

后面统一按基类接口调用:

  • comm_->open():// 打开通信接口
  • comm_->receive(...)
C++ 复制代码
bool K210Detector::start() {
    if (running_) {
        std::cout << "[K210] Already running" << std::endl;
        return true;
    }

    // 创建通信接口
    if (config_.comm_type == Config::UART) {
        comm_ = std::make_unique<UARTComm>(config_.uart_config);
    } else {
        comm_ = std::make_unique<SPIComm>(config_.spi_config);
    }

    // 打开通信接口
    if (!comm_->open()) {
        std::cerr << "[K210] Failed to open communication" << std::endl;
        return false;
    }

    running_ = true;
    comm_thread_ = std::thread(&K210Detector::commThreadLoop, this);

    std::cout << "[K210] Detector started" << std::endl;
    return true;
}


{
     std::lock_guard<std::mutex> lock(comm_mutex_);
     if (comm_ && comm_->isOpen()) {
         got_data = comm_->receive(data, 256, config_.poll_interval_ms);
    }
}
        
  • 编译时期,编译器不知道 comm_ 会指向 UARTComm 还是 SPIComm
  • 这取决于运行时的配置config_.comm_type 的值)。
  • std::make_unique<UARTComm> 创建了一个 UARTComm 对象,然后通过 std::unique_ptr 的移动语义赋值给 comm_

这里的多态体现comm_ 的静态类型是 std::unique_ptr<CommInterface>(编译期类型),但动态类型(运行时实际指向的对象)是 UARTCommSPIComm

3. 通过基类指针调用派生类方法(多态的威力)

  • comm_->open() 调用的不是 CommInterface::open()(如果它是纯虚函数,甚至都没有实现)。
  • 因为 comm_ 实际指向的是 UARTCommSPIComm 对象,所以调用的是对应的派生类的 open() 版本

这就是多态的核心

同一个接口(open()),不同的实现(UART的打开方式 vs SPI的打开方式)。 同一个基类指针,运行时可以指向不同子类对象。

4. 多态三要素(多态的前提条件)

多态要素 代码中的体现
继承 UARTCommSPIComm 继承自 CommInterface
虚函数重写 派生类重写了 open()write()read() 等虚函数
父类指针指向子类对象/基类指针指向派生类对象 std::unique_ptr<CommInterface> 指向 UARTComm / SPIComm 对象

三个条件都满足时,comm_->open() 就是多态的调用。

为什么不是其他代码实现多态

你可能会问:难道 comm_ = std::make_unique<UARTComm>(...) 不是多态吗?

不是! 那行代码是对象创建指针赋值 ,它是为多态做准备,但本身不是多态。

代码行 作用 是否是多态
comm_ = std::make_unique<UARTComm>(...) 创建派生类对象,赋值给基类指针 准备阶段
if (!comm_->open()) 通过基类指针调用虚函数 多态发生
comm_->write(data, len) 通过基类指针调用另一个虚函数 多态发生
comm_.reset() 销毁对象 ❌ 资源管理

只要是 comm_-> 加上一个虚函数,那就是多态的调用。

问题1:std::unique_ptr<CommInterface> comm_; 是啥?

这是一个智能指针 ,它指向一个 CommInterface 类型的对象,并且独占这个对象的所有权。

部分 含义
std::unique_ptr C++标准库中的独占所有权智能指针 。同一时刻只能有一个 unique_ptr 指向同一个对象。
<CommInterface> 这个智能指针指向的类型CommInterface(通信接口基类)。
comm_ 这个智能指针变量的名字。

为什么不用裸指针?

C++ 复制代码
// ❌ 裸指针(危险)
CommInterface* comm_ = new UARTComm(...);
// ... 中间可能抛出异常或提前 return
delete comm_;  // 容易忘记!导致内存泄漏
C++ 复制代码
// ✅ unique_ptr(安全)
std::unique_ptr<CommInterface> comm_;
comm_ = std::make_unique<UARTComm>(...);
// 无论何时离开作用域,comm_ 的析构函数都会自动 delete

核心好处RAII(资源获取即初始化) ------资源(堆内存)的生命周期与对象(comm_)的生命周期绑定,永不泄漏。

问题2:comm_ = std::make_unique<UARTComm>(config_.uart_config); 是啥?

在堆上创建一个 UARTComm 对象,并用 config_.uart_config 初始化它,然后把这个对象交给 comm_ 管理。

部分 含义
std::make_unique<UARTComm> 堆内存 上创建一个 UARTComm 类型的对象,并返回一个指向它的 unique_ptr。这是最安全的创建方式(异常安全)。
(config_.uart_config) 传给 UARTComm 构造函数的参数 。相当于调用 UARTComm(config_.uart_config)
comm_ = ... 把新创建的 unique_ptr 移动赋值comm_。注意!这里是移动 ,不是拷贝,因为 unique_ptr 不允许拷贝。

完整执行流程

C++ 复制代码
1. std::make_unique<UARTComm>(config_.uart_config)
   ↓
2. 在堆上分配一块内存,大小为 sizeof(UARTComm)
   ↓
3. 调用 UARTComm 的构造函数,传入 config_.uart_config
   ↓
4. 返回一个临时的 std::unique_ptr<UARTComm> 对象
   ↓
5. comm_ 的移动赋值运算符接管这个临时对象(comm_ 原本指向的对象被释放)
   ↓
6. 现在 comm_ 指向了一个 UARTComm 对象

如果不用 make_unique(老式写法):

C++ 复制代码
// ❌ 老式写法(不推荐)
comm_ = std::unique_ptr<UARTComm>(new UARTComm(config_.uart_config));

为什么 make_unique 更好?

  1. 异常安全:如果构造函数抛出异常,不会发生内存泄漏。
  2. 代码简洁 :不用写 new 和重复类型名。
  3. 更高效 :理论上可以避免一些中间临时对象的开销(虽然对 unique_ptr 影响不大)。

内存布局示意图

C++ 复制代码
栈(Stack)                    堆(Heap)
┌─────────────────────┐      ┌──────────────────────┐
│   comm_             │      │   UARTComm 对象      │
│  ┌───────────────┐  │      │  ┌────────────────┐  │
│  │ 原始指针: 0x1000│──┼──────┼─→│ vptr (虚表指针) │  │
│  └───────────────┘  │      │  │ m_fd = 3       │  │
│  (智能指针本身)     │      │  │ m_baud = 115200│  │
└─────────────────────┘      │  │ ...            │  │
                              │  └────────────────┘  │
                              └──────────────────────┘
  • comm_ 本身在上(8字节,一个原始指针)。
  • 它指向的 UARTComm 对象在上(可能有几十字节)。
  • comm_ 析构时(离开作用域),堆上的 UARTComm 对象被自动 delete

底层原理:虚函数表(vtable)

通过虚函数表(vtable)和虚指针(vptr)实现动态绑定。

c++ 复制代码
comm_ (智能指针)
    ↓ 解引用
[原始指针] → 指向堆上的 UARTComm 对象
                ↓
           对象内存布局:
           ┌─────────────────┐
           │ vptr (虚指针)   │ → 指向 UARTComm 的虚函数表
           ├─────────────────┤
           │ config_         │
           ├─────────────────┤
           │ ...             │
           └─────────────────┘
                ↓
           UARTComm 的虚函数表:
           ┌─────────────────────────┐
           │ &UARTComm::~UARTComm    │
           ├─────────────────────────┤
           │ &UARTComm::open()  ←─── │ 调用这个!
           ├─────────────────────────┤
           │ &UARTComm::write()      │
           ├─────────────────────────┤
           │ &UARTComm::read()       │
           └─────────────────────────┘

关键点 :编译器在编译时无法知道 comm_ 指向哪个派生类,但运行时 通过 vptr 找到正确的虚函数表,从而调用正确的 open()

5. 虚函数

  • 多态 :字面意思是"多种形态"。指父类指针/引用 指向子类对象时,调用同一个函数名,实际执行的是子类的版本。
  • 虚函数 :实现多态的机制。在父类函数前加 virtual,子类重写它。
  • 虚函数(Virtual Function) 是一个允许在派生类中被重写 的成员函数,它的核心作用是实现运行时多态 ------当通过基类指针/引用调用虚函数时,实际执行的是对象真实类型对应的版本。
  • 底层原理(关键) :每个含有虚函数的类都有一个虚函数表(vtable) ,里面存放着虚函数的地址。对象中有个虚指针(vptr) 指向这个表。当调用虚函数时,程序会通过vptr找到正确的表,再找到正确的函数地址执行。这就是运行时多态(动态绑定)。
C++ 复制代码
class CommInterface {
public:
    virtual ~CommInterface() = default;  //虚析构函数
    virtual bool open() = 0;  //纯虚函数
    virtual void close() = 0;
    virtual bool send(const std::vector<uint8_t>& data) = 0;
    virtual bool receive(std::vector<uint8_t>& data, size_t max_len, int timeout_ms) = 0;
    virtual bool isOpen() const = 0;
};

子类里用 override 实现:

C++ 复制代码
 explicit UARTComm(const Config& config);
    ~UARTComm() override;

    bool open() override;
    void close() override;
    bool send(const std::vector<uint8_t>& data) override;
    bool receive(std::vector<uint8_t>& data, size_t max_len, int timeout_ms) override;
    bool isOpen() const override;

这部分就是很标准的"接口类 + 虚函数"。

当一个类有虚函数时,编译器会为它做两件事:

1. 生成虚函数表(vtable)

  • 每个有虚函数的类都有一个虚函数表(在程序的只读数据段)。
  • 表中存储着所有虚函数的地址(函数指针)。

2. 在对象中插入虚指针(vptr)

  • 每个对象的前 8 个字节(64位系统)存储一个 vptr(虚指针)。
  • vptr 指向该类对应的 vtable。
C++ 复制代码
// Animal 类(有虚函数)
class Animal {
    virtual void speak() { ... }
    virtual void eat() { ... }
    int age;
};

// 内存布局
Animal 对象 (sizeof = 16字节,64位系统)
┌────────────────────────────────────┐
│ vptr (8字节) → 指向 Animal 的 vtable │
├────────────────────────────────────┤
│ age (4字节)                        │
├────────────────────────────────────┤
│ 内存对齐填充 (4字节)               │
└────────────────────────────────────┘

Animal 的 vtable(只读数据段)
┌─────────────────────────┐
│ &Animal::speak()        │  ← 第0个虚函数
├─────────────────────────┤
│ &Animal::eat()          │  ← 第1个虚函数
└─────────────────────────┘

6. 构造函数(Constructor)与析构函数(Destructor)

  • 构造函数 :创建对象时自动调用 ,用于初始化成员变量。可以重载。初始化列表:后)是推荐给成员赋初值的方式,特别是对于const和引用成员。
  • 析构函数 :对象销毁时自动调用(如离开作用域、delete),用于释放资源(如关闭文件、释放堆内存)。
  • 黄金法则 :如果类中管理了动态内存(new),必须 显式提供析构函数、拷贝构造函数和拷贝赋值运算符(三/五法则)。更重要 :基类的析构函数必须是 virtual ,否则当delete父类指针指向子类对象时,只会调用父类析构,子类资源泄露。
C++ 复制代码
LvglDisplay(int width = 1024, int height = 600,
                 int preview_width = 0, int preview_height = 0);

 ~LvglDisplay();
3. 重载(Overloading)与重写/覆盖(Overriding)
  • 重载(编译时多态) :在同一个作用域 内,函数名字相同但参数列表不同 (个数、类型或顺序)。编译器根据传入的参数决定调用哪个版本。与返回值类型无关
  • 重写(运行时多态的一部分) :在子类 中重新定义父类中带有 virtual 关键字的函数。要求函数名、参数列表、返回值完全相同(协变返回除外)。

第二部分:STL 容器(Standard Template Library)

STL容器是"数据结构的现成工具箱"。分为几大类:

1. 顺序容器(Sequence Containers)

元素有先后顺序,插入位置与值无关。

  • vector :动态数组。随机访问快 O(1),尾部插入快,中间/头部插入删除慢。首选容器
  • deque:双端队列。头尾插入删除快,随机访问也快(略逊于vector)。
  • list :双向链表。任何位置插入删除快 ,但不支持随机访问(只能迭代器移动)。
  • forward_list:单向链表。更节省内存,只能向前遍历。
2. 关联容器(Associative Containers)

元素自动按键(Key)排序 ,底层通常是红黑树 (平衡二叉树)。查找复杂度 O(log n)

  • set / multiset :键即值。set键唯一,multiset允许重复。
  • map / multimap :键-值对。map键唯一,multimap键可重复。通过键查找值。
3. 无序关联容器(Unordered Associative Containers)

元素按哈希表存储,不排序 。查找、插入、删除平均复杂度 O(1),最坏 O(n)

  • unordered_set / unordered_multiset
  • unordered_map / unordered_multimap
  • 关键 :如果自定义类型作为键,必须提供哈希函数(hash相等比较函数(equal_to
4. 容器适配器(Container Adapters)

不是完整的容器,是对底层容器(如dequelist)的接口进行限制的包装。

  • stack (栈):后进先出(LIFO)。底层默认deque
  • queue (队列):先进先出(FIFO)。底层默认deque
  • priority_queue (优先队列):最大/最小堆。底层默认vector
5. 分配器(Allocator)与迭代器(Iterator)
  • 迭代器泛型指针 ,用于遍历容器中的所有元素。所有STL算法(如sortfind)都通过迭代器操作容器,不关心容器具体类型。分为输入、输出、前向、双向、随机访问五类,vector支持最强的随机访问。
  • 分配器 :内存分配策略。为容器提供底层内存的申请和释放机制。默认是 std::allocator,一般开发者很少自定义,除非做内存池优化。

第三部分:Lambda 表达式

本质 :一个可调用的匿名函数对象 (编译器会将其生成为一个匿名的仿函数类)。

语法[捕获列表](参数列表) -> 返回类型 { 函数体 }

  • 捕获列表(核心)

    • [ ]:不捕获外部变量。
    • [=]:值捕获(复制一份进来,只读,不可修改外部变量)。
    • [&]:引用捕获(可以修改外部变量,注意生命周期)。
    • [this]:捕获当前类对象的指针。
    • [x, &y]:显式指定x值捕获,y引用捕获。
  • 用途 :常用于STL算法(如std::find_if)的回调函数,或定义简短的本地逻辑,极大简化代码。


第四部分:智能指针(Smart Pointers)

智能指针是RAII(资源获取即初始化) 的典范,用于自动管理堆内存,防止内存泄漏 。定义在 <memory>

  • unique_ptr(独占所有权) :同一时刻只能有一个指针指向对象。不能拷贝 ,只能移动std::move)。是C++中最推荐的默认选择,开销极小(与裸指针相当)。
  • shared_ptr(共享所有权) :多个指针共同拥有一个对象。内部使用引用计数 ,当计数归零时自动销毁对象。注意循环引用 问题(两个shared_ptr互相指向对方,导致计数永远不为0)。
  • weak_ptr(弱引用) :配合shared_ptr使用,不增加引用计数。用于打破循环引用 。它不能直接访问对象,必须通过lock()方法升级为临时的shared_ptr才能使用。

第五部分:多线程与并发

并发(Concurrency)指任务交替执行,并行(Parallelism)指任务同时执行(多核)。

1. 线程同步与互斥(Synchronization & Mutex)
  • 互斥:防止多个线程同时访问共享资源,导致数据不一致。
  • std::mutex:互斥锁。只允许一个线程持有锁。
  • RAII锁管理std::lock_guard (简单,构造上锁,析构解锁)、std::unique_lock (更灵活,支持延迟加锁、条件变量)。强烈推荐使用lock_guard/unique_lock,绝不手动lock/unlock,以防异常导致死锁。
2. 线程死锁(Deadlock)
  • 含义:线程A持有锁1等待锁2,线程B持有锁2等待锁1,双方无限等待。

  • 常见解法

    • 固定加锁顺序:所有线程按相同顺序获取锁。
    • 使用std::lock函数:一次性锁定多个锁,内部采用死锁避免算法。
    • 使用try_lock尝试获取。
3. STL中的 <future>

用于异步任务 (获取线程返回结果),是比直接操作std::thread更高级的工具。

  • std::async :异步启动一个任务,返回std::future对象。
  • std::future :存储异步任务的返回结果。通过get()获取结果(会阻塞等待),wait()只等待不获取。
  • std::promise :允许在一个线程中设置值,在另一个线程的future中获取,用于线程间传递数据。
  • std::packaged_task:将普通函数包装成可异步调用的任务。
4. 线程池(Thread Pool)
  • 含义:预先创建一组固定数量的工作线程,任务队列不断接收新任务,空闲线程从队列取任务执行。
  • 目的:避免频繁创建/销毁线程的开销(开销巨大),同时控制并发数量,防止系统过载。
  • 实现思路 :一个线程安全的任务队列std::queue + mutex + condition_variable),多个std::thread循环从队列中取任务执行。
相关推荐
Escalating_xu2 小时前
【C++ STL简介】从六大组件到容器、迭代器与算法协作
java·c++·算法
Brilliantwxx2 小时前
【Linux】 进程(3)深度解析:从查看进程到进程状态
linux·服务器·网络·c++
啊啊啊啊啊!!!!2 小时前
【c++】二叉搜索树
开发语言·c++
ShineWinsu2 小时前
对于 C++:C++20中Concept(概念) 与 Coroutine(协程)的解析
linux·开发语言·网络·c++·c++20·epoll
liulilittle3 小时前
llmx 学习手册 06 —— CPU 指令集优化(AVX-512 三层演进)
c++·学习·算法·ai·llm
skr爱码士3 小时前
05_Qt 核心模块概览——Qt Core、Gui、Widgets、Quick 的职责划分
c++·qt·系统架构·客户端
郝学胜-神的一滴4 小时前
Horse3D 游戏引擎研发笔记(七):Clydesdale——从流式日志到多输出订阅
c++·qt·unity·游戏引擎·图形渲染·unreal engine·opengl
码匠许师傅12 小时前
【C++ 面试真题】26. 聊聊 C++ 的智能指针
java·c++·面试
夜不会漫长17 小时前
C++入门(1)
开发语言·c++·算法