3. C++编程核心(上):面向对象编程、智能指针与STL容器

各位同学,欢迎来到《机器人系统开发核心技能精讲》的第三章节。今天我们要啃的这块骨头,可以说是整个机器人软件开发的基石——C++编程核心的上半部分。

说实话,我见过太多做机器人开发的工程师,代码能跑,但一遇到复杂系统就崩。为什么?说白了,就是对C++的这几个核心特性理解不够深。你想想看,一个机器人系统里,传感器数据要管理、电机控制要调度、路径规划要计算,没有扎实的面向对象功底和高效的数据结构,代码很快就会变成一团乱麻。

今天我们就来把这几个硬骨头啃下来:面向对象编程(类、继承、多态)、智能指针(shared_ptr、unique_ptr)、STL容器(vector、map、list)。

C++编程核心知识体系 C++编程核心(上) 面向对象编程 智能指针 STL容器 类与对象 继承 多态 shared_ptr unique_ptr 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 {
        // 步进电机制动逻辑
    }
};
注意:我曾经在项目里见过有人把继承层级搞到七八层深,结果改一个基类,整个系统都要重新编译。记住:继承不是万能的,组合优先于继承,这是《Effective C++》里说的,也是我踩坑踩出来的经验。

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);  // 可以移动
我的建议:能用 unique_ptr 就别用 shared_ptr。unique_ptr 没有引用计数的开销,性能更好。我在项目中,90%的场景都用 unique_ptr。

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 都销毁时,资源才释放
避坑指南:我曾经在项目里遇到过循环引用的问题——两个对象互相持有 shared_ptr,导致内存永远无法释放。解决方案是用 weak_ptr 打破循环。记住:shared_ptr 不是银弹,用之前想清楚所有权关系。

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"));
注意:list 的每个元素都是独立分配的内存,所以内存碎片化比较严重。在嵌入式系统里,如果内存紧张,慎用 list。我一般用 vector 加索引模拟链表,或者用静态数组实现的内存池。

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存储轨迹点。只有动手写过,才能真正理解这些概念。

记住:代码是写给人看的,顺便能在机器上跑。好的代码,结构清晰,内存安全,性能高效。这就是我们追求的目标。