在我们的C语言教程中,我们遵循一种标准的模块化范式:使用 struct 来聚合数据,并编写一系列独立的全局函数 (_init, _set, _get 等) 来操作这些数据。这种“数据与操作分离”的模式是面向过程编程的核心。

// C语言的范式:数据与操作分离
typedef struct {
    volatile uint32_t* p_port;
    uint8_t pin;
} gpio_pin_t;

void gpio_init(gpio_pin_t* pin, void* port, uint8_t pin_num);
void gpio_set(gpio_pin_t* pin);
void gpio_clear(gpio_pin_t* pin);

这种模式虽然有效,但在复杂系统中暴露了两个根本性的弱点:

  1. 缺乏数据保护: 任何能够接触到 gpio_pin_t 结构体实例的代码,都可以直接修改其内部成员(如 p_port),绕过所有预设的函数,可能导致硬件进入不一致或危险的状态。

  2. 松散的耦合: gpio_pin_t 和操作它的函数之间没有强制的关联。它们是分离的实体,需要程序员通过文档和约定来保证它们的正确组合使用。

C++的 封装 (Encapsulation) 正是为了解决这些问题而生的。它是一种将 数据 (Data) 和操作这些数据的 方法 (Methods) 捆绑在一起的机制,并将它们“封装”在一个名为 类 (Class) 的“黑盒”中,同时对外部世界隐藏其内部实现的复杂性。

1. classstruct:从数据聚合到行为抽象

在C++中,class 是实现封装的核心关键字。它将C语言中分离的数据和函数,统一成一个高内聚的逻辑单元。

  • 数据成员 (Data Members): 对应于C语言 struct 的成员,用于表示对象的状态。

  • 成员函数 (Member Functions / Methods): 封装在类内部的、专门用于操作该类数据成员的函数。

// C++的范式:数据与行为封装在类中
class GpioPin {
public:
    // 公共接口 (Public Interface)
    void init(volatile uint32_t* port, uint8_t pin_num);
    void set();
    void clear();
    bool read();

private:
    // 私有数据成员 (Private Data Members)
    volatile uint32_t* p_port;
    uint8_t pin;
};

通过这种方式,GpioPin 不再仅仅是一个被动的数据容器,而是一个主动的、自洽的“对象”,它知道如何管理自己的状态。

C++中 classstruct 的区别

这是一个常见的面试题,但答案非常简单:

  • 唯一的语法区别:

    • class 的成员默认访问权限是 private (私有的)。

    • struct 的成员默认访问权限是 public (公共的)。

  • 语义上的约定(更重要):

    • 使用 struct 来表示那些只有数据、几乎没有行为的 普通旧数据 (Plain Old Data, POD) 集合。它类似于C语言的 struct,其内部状态通常是对外完全开放的。

    • 使用 class 来表示那些具有复杂行为、需要维护内部 不变量 (Invariants)、并希望对外部隐藏实现细节的 对象 (Objects)

2. 访问控制:public, private, protected

访问控制是封装实现 信息隐藏 (Information Hiding) 的关键机制。它定义了类的哪些部分可以被外部代码访问。

  1. public (公共的):

    • 定义了类的 公共接口 (Public Interface)

    • 任何外部代码都可以访问 public 成员。

    • 这是类的使用者唯一应该关心的部分。

  2. private (私有的):

    • 定义了类的 内部实现 (Internal Implementation)

    • 只有该类的 成员函数 可以访问 private 成员。

    • 核心作用: 保护对象的内部状态不被外部直接、非法地修改,从而保证对象状态的一致性和有效性(即维护“不变量”)。在 GpioPin 的例子中,将 p_portpin 设为 private,就杜绝了任何外部代码绕过 init 函数直接篡改硬件地址的可能性。

  3. protected (受保护的):

    • 介于 publicprivate 之间。

    • protected 成员可以被该类及其 子类 (Derived Classes) 的成员函数访问。

    • 我们将在 1.2a 继承与多态 中详细探讨它的作用。

3. 对象生命周期管理:构造函数与析构函数

封装不仅是捆绑数据和行为,更是管理一个对象的完整 生命周期 (Lifetime)。C++通过两种特殊的成员函数来自动化这个过程,这也是 RAII原则 的基础。

3.1 构造函数 (Constructor)

  • 是什么? 一个与类同名、没有返回值的特殊成员函数。它在对象被 创建自动调用

  • 核心作用: 保证对象在诞生时就处于一个有效的、一致的初始状态。 这彻底消除了C语言中“先创建结构体,再调用 _init() 函数”这种可能被遗忘的两步操作。

class GpioPin {
public:
    // 构造函数
    GpioPin(volatile uint32_t* port, uint8_t pin_num) {
        // 在构造函数中完成初始化
        p_port = port;
        pin = pin_num;
        // 可以在这里直接进行硬件的初始化配置
        // *p_port_config_reg = ...;
        std::cout << "GpioPin for pin " << (int)pin << " constructed and initialized." << std::endl;
    }

    // ... 其他成员函数 ...
private:
    volatile uint32_t* p_port;
    uint8_t pin;
};

// 使用
void setup() {
    // 当 led_pin 对象被创建时,其构造函数被自动调用
    GpioPin led_pin(GPIOA_BASE, 5); // 一步完成创建和初始化
}

3.2 析构函数 (Destructor)

  • 是什么? 一个以 ~ 符号开头、与类同名的特殊成员函数。它在对象 生命周期结束(例如,离开作用域、被 delete)时 自动调用

  • 核心作用: 保证对象在销毁前能够正确地释放其占有的资源。 这是实现RAII原则的关键。

class UartDriver {
public:
    UartDriver(int port_num) {
        // 构造函数:打开串口,分配缓冲区等
        std::cout << "UART port " << port_num << " opened." << std::endl;
    }
    
    // 析构函数
    ~UartDriver() {
        // 在对象销毁时,自动关闭串口,释放资源
        std::cout << "UART port closed and resources freed." << std::endl;
    }
    
    // ...
};

void communicate() {
    // uart_com 对象在栈上创建,构造函数被调用
    UartDriver uart_com(1);
    
    // ... 使用 uart_com 进行通信 ...

} // 当函数 communicate() 结束,uart_com 离开作用域,
  // 其析构函数 ~UartDriver() 被自动调用。

4. 总结

封装是面向对象编程的第一大支柱,它通过 class 将C语言中松散的数据和函数,整合成一个高内聚、低耦合的逻辑单元。

  • 捆绑:数据(状态)方法(行为) 捆绑在一起。

  • 隐藏: 使用 public 定义稳定接口,使用 private 隐藏实现细节,保护内部状态。

  • 生命周期管理: 通过 构造函数 保证对象的有效初始化,通过 析构函数 保证资源的自动释放(RAII)。

在嵌入式开发中,封装的意义尤为重大。它让我们能够创建出像 GpioPin, I2CDevice, Timer 这样的硬件抽象层(HAL),这些抽象层接口清晰、使用安全、内部实现与上层应用完全隔离。这正是构建大型、复杂、可维护嵌入式固件的基石,并且得益于 零成本抽象 原则,我们无需为此付出性能代价。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐