3. C++编程核心(上):面向对象编程、智能指针与STL容器
各位同学,欢迎来到《机器人系统开发核心技能精讲》的第三章节。今天我们要啃的这块骨头,可以说是整个机器人软件开发的基石——C++编程核心的上半部分。
说实话,我见过太多做机器人开发的工程师,代码能跑,但一遇到复杂系统就崩。为什么?说白了,就是对C++的这几个核心特性理解不够深。你想想看,一个机器人系统里,传感器数据要管理、电机控制要调度、路径规划要计算,没有扎实的面向对象功底和高效的数据结构,代码很快就会变成一团乱麻。
今天我们就来把这几个硬骨头啃下来:面向对象编程(类、继承、多态)、智能指针(shared_ptr、unique_ptr)、STL容器(vector、map、list)。
3.1 面向对象编程:类、继承、多态
面向对象编程,说白了就是把现实世界的东西抽象成代码里的「类」。机器人系统里到处都是对象——电机、传感器、控制器,每个都是一个对象。
3.1.1 类与对象
类就是蓝图,对象就是根据蓝图造出来的实物。我习惯把类比作「模具」,对象就是「零件」。在机器人系统里,我们经常这样定义类:
class Motor {
private:
std::string name_;
int pwm_pin_;
int encoder_pin_;
float current_speed_;
public:
Motor(const std::string& name, int pwm, int encoder)
: name_(name), pwm_pin_(pwm), encoder_pin_(encoder), current_speed_(0.0f) {}
void setSpeed(float speed) {
current_speed_ = speed;
// 实际硬件控制代码
}
float getSpeed() const {
return current_speed_;
}
};
name_。这样一眼就能看出哪些是成员变量,哪些是局部变量。这个习惯跟了十几年了,推荐你也试试。
3.1.2 继承
继承解决的是「is-a」关系。比如我们有各种类型的电机:直流电机、步进电机、伺服电机。它们都有共同的行为——转,但实现方式不同。
class BaseMotor {
public:
virtual void start() = 0; // 纯虚函数
virtual void stop() = 0;
virtual ~BaseMotor() = default;
};
class DCMotor : public BaseMotor {
public:
void start() override {
// 直流电机启动逻辑
}
void stop() override {
// 直流电机制动逻辑
}
};
class StepperMotor : public BaseMotor {
public:
void start() override {
// 步进电机启动逻辑
}
void stop() override {
// 步进电机制动逻辑
}
};
3.1.3 多态
多态是面向对象的精髓。它让我们可以用统一的接口操作不同的对象。在机器人系统里,这太重要了——你想想看,一个运动控制器,要同时控制不同类型的电机,没有多态怎么办?
void controlAllMotors(std::vector<std::unique_ptr<BaseMotor>>& motors) {
for (auto& motor : motors) {
motor->start(); // 多态!每个电机用自己的方式启动
}
}
为什么会这样?因为虚函数表(vtable)在背后起作用。每个有虚函数的类都有一个虚函数表,调用虚函数时,程序会通过vtable找到正确的函数地址。嗯,这里要注意,虚函数调用比普通函数调用多一次间接寻址,性能上会有一点损耗。但在机器人系统里,这点损耗通常可以忽略不计。
3.2 智能指针:shared_ptr 与 unique_ptr
说到内存管理,我真是有一肚子话要说。早期做机器人开发,C++里全是裸指针,new和delete满天飞。结果呢?内存泄漏、野指针、悬空指针,这些问题能把人逼疯。
智能指针的出现,简直是救星。它遵循RAII(资源获取即初始化)原则,说白了就是:资源在构造函数里获取,在析构函数里释放。你只管用,不用管释放。
3.2.1 unique_ptr:独占所有权
unique_ptr 是独占所有权的智能指针。一个资源只能被一个 unique_ptr 拥有,不能拷贝,只能移动。这在机器人系统里非常有用——比如一个传感器对象,只能被一个控制器拥有。
std::unique_ptr<Sensor> sensor1 = std::make_unique<Sensor>("lidar");
// std::unique_ptr<Sensor> sensor2 = sensor1; // 编译错误!不能拷贝
std::unique_ptr<Sensor> sensor2 = std::move(sensor1); // 可以移动
3.2.2 shared_ptr:共享所有权
shared_ptr 使用引用计数来管理资源。当最后一个 shared_ptr 被销毁时,资源才被释放。这在需要共享资源的场景下很有用——比如多个控制器共享同一个传感器数据。
std::shared_ptr<SensorData> data = std::make_shared<SensorData>();
std::shared_ptr<SensorData> data2 = data; // 引用计数+1
// 当 data 和 data2 都销毁时,资源才释放
3.3 STL容器:vector、map、list
STL容器是C++标准库的精华。在机器人系统里,数据管理是家常便饭——传感器数据流、配置参数、任务队列,都离不开容器。
3.3.1 vector:动态数组
vector 是我用得最多的容器。它在内存中是连续存储的,支持随机访问,尾部插入删除效率高。适合存储传感器数据流、点云数据等。
std::vector<float> lidar_data;
lidar_data.reserve(1000); // 预分配空间,避免频繁扩容
for (int i = 0; i < 1000; ++i) {
lidar_data.push_back(scan_data[i]);
}
// 随机访问
float center_point = lidar_data[500];
reserve() 预分配空间。否则 vector 会频繁扩容,每次扩容都要拷贝所有元素,性能损耗很大。我在处理激光雷达数据时,这个技巧帮我省了不少时间。
3.3.2 map:键值对映射
map 是基于红黑树的有序键值对容器。适合存储配置参数、设备ID到设备对象的映射等。
std::map<std::string, int> motor_config;
motor_config["left_wheel"] = 1;
motor_config["right_wheel"] = 2;
motor_config["arm_joint"] = 3;
// 查找
auto it = motor_config.find("left_wheel");
if (it != motor_config.end()) {
int pin = it->second;
}
unordered_map,它的查找速度是O(1),比 map 的O(log n)快。但 unordered_map 占用的内存更多。在机器人系统里,如果数据量不大,我一般用 map,代码更清晰。
3.3.3 list:双向链表
list 是双向链表,支持常数时间的插入和删除,但不支持随机访问。适合需要频繁在中间插入删除的场景——比如任务队列、消息队列。
std::list<Task> task_queue;
task_queue.push_back(Task("move_forward"));
task_queue.push_back(Task("turn_left"));
// 在中间插入
auto it = task_queue.begin();
std::advance(it, 1);
task_queue.insert(it, Task("stop"));
3.4 容器选择指南
为了帮你快速决策,我整理了一个表格:
| 容器 | 底层结构 | 随机访问 | 中间插入/删除 | 尾部插入/删除 | 适用场景 |
|---|---|---|---|---|---|
| vector | 动态数组 | O(1) | O(n) | O(1) | 传感器数据、点云、序列数据 |
| map | 红黑树 | O(log n) | O(log n) | O(log n) | 配置参数、设备映射 |
| list | 双向链表 | O(n) | O(1) | O(1) | 任务队列、消息队列 |
好了,这一章的内容就到这里。面向对象编程、智能指针、STL容器,这三个东西是C++编程的三大支柱。你想想看,一个机器人系统里,哪个地方不用到它们?
我个人建议,学完这一章后,找个实际项目练练手。比如写一个简单的机器人运动控制程序,用类封装电机和传感器,用智能指针管理资源,用vector存储轨迹点。只有动手写过,才能真正理解这些概念。
记住:代码是写给人看的,顺便能在机器上跑。好的代码,结构清晰,内存安全,性能高效。这就是我们追求的目标。