1.1a. 封装 (Encapsulation) 概念
封装是面向对象编程的第一大支柱,它通过class将C语言中松散的数据和函数,整合成一个高内聚、低耦合的逻辑单元。将数据(状态)和方法(行为)捆绑在一起。使用 public定义稳定接口,使用 private隐藏实现细节,保护内部状态。通过构造函数保证对象的有效初始化,通过析构函数保证资源的自动释放(RAII在嵌入式开发中,封装的意义尤为重大。它让我们能够创建出像GpioPinI2CDeviceTim
在我们的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);
这种模式虽然有效,但在复杂系统中暴露了两个根本性的弱点:
-
缺乏数据保护: 任何能够接触到
gpio_pin_t
结构体实例的代码,都可以直接修改其内部成员(如p_port
),绕过所有预设的函数,可能导致硬件进入不一致或危险的状态。 -
松散的耦合:
gpio_pin_t
和操作它的函数之间没有强制的关联。它们是分离的实体,需要程序员通过文档和约定来保证它们的正确组合使用。
C++的 封装 (Encapsulation) 正是为了解决这些问题而生的。它是一种将 数据 (Data) 和操作这些数据的 方法 (Methods) 捆绑在一起的机制,并将它们“封装”在一个名为 类 (Class) 的“黑盒”中,同时对外部世界隐藏其内部实现的复杂性。
1. class
与 struct
:从数据聚合到行为抽象
在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++中 class
与 struct
的区别
这是一个常见的面试题,但答案非常简单:
-
唯一的语法区别:
-
class
的成员默认访问权限是private
(私有的)。 -
struct
的成员默认访问权限是public
(公共的)。
-
-
语义上的约定(更重要):
-
使用
struct
来表示那些只有数据、几乎没有行为的 普通旧数据 (Plain Old Data, POD) 集合。它类似于C语言的struct
,其内部状态通常是对外完全开放的。 -
使用
class
来表示那些具有复杂行为、需要维护内部 不变量 (Invariants)、并希望对外部隐藏实现细节的 对象 (Objects)。
-
2. 访问控制:public
, private
, protected
访问控制是封装实现 信息隐藏 (Information Hiding) 的关键机制。它定义了类的哪些部分可以被外部代码访问。
-
public
(公共的):-
定义了类的 公共接口 (Public Interface)。
-
任何外部代码都可以访问
public
成员。 -
这是类的使用者唯一应该关心的部分。
-
-
private
(私有的):-
定义了类的 内部实现 (Internal Implementation)。
-
只有该类的 成员函数 可以访问
private
成员。 -
核心作用: 保护对象的内部状态不被外部直接、非法地修改,从而保证对象状态的一致性和有效性(即维护“不变量”)。在
GpioPin
的例子中,将p_port
和pin
设为private
,就杜绝了任何外部代码绕过init
函数直接篡改硬件地址的可能性。
-
-
protected
(受保护的):-
介于
public
和private
之间。 -
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),这些抽象层接口清晰、使用安全、内部实现与上层应用完全隔离。这正是构建大型、复杂、可维护嵌入式固件的基石,并且得益于 零成本抽象 原则,我们无需为此付出性能代价。
更多推荐
所有评论(0)