C 里你大概写过这种代码:
1 2 3 4 5 6 7 8 9
| typedef struct { uint8_t *buf; uint32_t baud; uint8_t initialized; } UART_Handle;
void UART_Init(UART_Handle *h, uint32_t baud); void UART_Send(UART_Handle *h, const uint8_t *data, uint16_t len); void UART_Deinit(UART_Handle *h);
|
数据和操作是分开的——UART_Handle 只是一堆字段,函数靠第一个参数 *h 关联到数据。这没有任何问题,C 大型项目都是这么做的。
C++ 的类做的事情完全一样,只是把数据和函数打包在一起,编译器帮你传那个隐式的 *h:
1 2 3 4 5 6 7 8 9 10
| class UartDriver { public: void init(uint32_t baud); void send(const uint8_t *data, uint16_t len); void deinit(); private: uint8_t *buf_; uint32_t baud_; bool initialized_; };
|
就这么简单。类不是魔法,是组织代码的方式。
一、访问控制:public / private / protected
1 2 3 4 5 6 7 8 9 10 11 12
| class SensorDriver { public: void init(); float read();
private: float calibrate(float raw); uint8_t reg_addr_; float offset_; };
|
public:任何地方都能访问
private:只有本类的成员函数能访问
protected:本类和子类能访问(继承时才有意义,后面讲)
struct 和 class 唯一的区别:struct 默认 public,class 默认 private。
为什么要 private?
不是为了藏秘密,是为了划清边界——调用方只能通过 public 接口操作对象,内部实现随时可以改,不影响外部代码。reg_addr_ 是硬件细节,外部不应该直接改它。
二、构造函数
构造函数在对象创建时自动调用,负责初始化。名字和类名相同,没有返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class UartDriver { public: UartDriver(uint32_t baud) { baud_ = baud; initialized_ = false; buf_ = nullptr; }
private: uint32_t baud_; bool initialized_; uint8_t *buf_; };
UartDriver uart(115200);
|
默认构造函数
没有参数的构造函数:
1 2 3 4 5 6 7 8 9 10
| class GpioPin { public: GpioPin() { state_ = false; } private: bool state_; };
GpioPin pin;
|
如果你一个构造函数都不写,编译器会生成一个什么都不做的默认构造函数。一旦你写了任何构造函数,编译器就不再自动生成默认构造函数。
构造函数重载
1 2 3 4 5 6 7 8 9 10 11
| class Timer { public: Timer() : period_ms_(1000) {} Timer(uint32_t ms) : period_ms_(ms) {}
private: uint32_t period_ms_; };
Timer t1; Timer t2(500);
|
三、初始化列表:比赋值更正确
上面构造函数里写了 : period_ms_(ms),这是成员初始化列表,不是在构造函数体内赋值。
1 2 3 4 5 6 7 8 9
| UartDriver(uint32_t baud) : baud_(baud), initialized_(false), buf_(nullptr) {}
UartDriver(uint32_t baud) { baud_ = baud; initialized_ = false; buf_ = nullptr; }
|
两种方式对 int、bool 这类基本类型效果一样。但有三种情况只能用初始化列表:
1. const 成员
1 2 3 4 5 6 7
| class Config { public: Config(uint32_t id) : id_(id) {} private: const uint32_t id_; };
|
2. 引用成员
1 2 3 4 5 6
| class Logger { public: Logger(UartDriver &uart) : uart_(uart) {} private: UartDriver &uart_; };
|
3. 没有默认构造函数的成员对象
1 2 3 4 5 6
| class System { public: System() : uart_(115200) {} private: UartDriver uart_; };
|
初始化列表的执行顺序是成员声明顺序,不是列表里写的顺序。所以列表的顺序最好和成员声明顺序一致,避免混淆。
四、析构函数
析构函数在对象销毁时自动调用,负责清理资源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class UartDriver { public: UartDriver(uint32_t baud) : baud_(baud), buf_(nullptr) { buf_ = new uint8_t[256]; }
~UartDriver() { delete[] buf_; buf_ = nullptr; }
private: uint32_t baud_; uint8_t *buf_; };
|
对象离开作用域时析构函数自动调用:
1 2 3 4
| void task() { UartDriver uart(115200); uart.send(data, len); }
|
这就是 C++ 里最重要的设计模式 RAII 的基础——资源在构造时获取,在析构时释放,不需要手动管理。RAII 会在后面专门讲。
五、this 指针
每个成员函数都有一个隐式的 this 指针,指向当前对象。大部分时候不需要显式写 this,但有时候需要:
1. 成员名和参数名冲突
1 2 3 4 5 6 7 8
| class GpioPin { public: void set_mode(uint8_t mode) { this->mode_ = mode; } private: uint8_t mode_; };
|
命名规范上用下划线后缀(mode_)就能避免这个问题,不需要 this。
2. 返回自身引用(链式调用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Builder { public: Builder& set_baud(uint32_t baud) { baud_ = baud; return *this; } Builder& set_parity(bool parity) { parity_ = parity; return *this; } private: uint32_t baud_; bool parity_; };
Builder b; b.set_baud(115200).set_parity(false);
|
六、const 成员函数
如果一个成员函数不修改对象的状态,应该声明为 const:
1 2 3 4 5 6 7 8 9 10 11 12
| class SensorDriver { public: float read() const { return last_value_; } void calibrate(float offset) { offset_ = offset; } private: float last_value_; float offset_; };
|
const 对象只能调用 const 成员函数:
1 2 3
| const SensorDriver sensor; sensor.read(); sensor.calibrate(1.0f);
|
养成习惯:不修改成员的函数一律加 const,调用方一眼能看出哪些函数有副作用。
七、一个完整的嵌入式例子
把上面所有知识点放在一起,写一个简单的 GPIO 驱动类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| class GpioPin { public: enum class Mode { Input, Output, Analog };
GpioPin(uint32_t pin, Mode mode) : pin_(pin), mode_(mode), state_(false) { hw_init(pin_, mode_); }
~GpioPin() { hw_deinit(pin_); }
void set(bool value) { state_ = value; hw_write(pin_, value); }
bool get() const { return hw_read(pin_); }
void toggle() { set(!get()); }
uint32_t pin() const { return pin_; }
private: uint32_t pin_; Mode mode_; bool state_;
void hw_init(uint32_t pin, Mode mode); void hw_deinit(uint32_t pin); void hw_write(uint32_t pin, bool value); bool hw_read(uint32_t pin) const; };
|
使用:
1 2 3 4 5 6 7 8
| void blink_task() { GpioPin led(GPIO_PIN_13, GpioPin::Mode::Output);
while (true) { led.toggle(); delay_ms(500); } }
|
和 C 版本相比:
- 不需要显式传
handle,对象自己携带状态
- 不需要手动调用
GPIO_Init/GPIO_DeInit,构造析构自动处理
Mode 用枚举类限定了合法值,不会传错参数
八、对象的生命周期
1 2 3 4 5 6 7 8 9 10 11 12
| void func() { UartDriver uart(115200); }
static UartDriver uart(115200);
UartDriver *p = new UartDriver(115200); delete p;
|
嵌入式裸机里推荐优先用栈上对象或静态对象,配合 RAII 让编译器管理生命周期,少用 new/delete。
总结
public/private:划清接口和实现的边界
- 构造函数:对象创建时自动初始化
- 初始化列表:比构造函数体内赋值更正确,
const/引用成员必须用
- 析构函数:对象销毁时自动清理,RAII 的基础
const 成员函数:声明函数不修改对象状态
this 指针:指向当前对象,大多数时候隐式使用