ROS2 C++开发系列10-写成模块化+可维护代码的ROS2项目代码,C++面向对象编程入门

在机器人系统开发中,代码的可维护性、模块化以及清晰的架构设计至关重要。C++ 作为 ROS2 的核心语言之一,其强大的面向对象编程(OOP)特性能够很好地支持这些需求。本教程将从基础类定义出发,深入探讨头文件组织、访问控制、静态成员、构造函数与析构函数,以及函数重载等核心概念,帮助你构建结构严谨的 C++ 机器人应用代码。

封装与类的初步构建

面向对象编程的第一原则是封装。通过隐藏内部实现细节,仅暴露必要的接口,我们可以保护数据的完整性并降低模块间的耦合度。

距离传感器类示例

为了演示这一概念,我们创建一个模拟机器人距离传感器的类 DistanceSensor。该类包含一个私有数据成员和一个公共方法。

#include <iostream>

class DistanceSensor {
private:
    // range 是私有成员,类外部无法直接访问
    // 这种封装保护了数据的完整性
    double range;

public:
    // 公共构造函数,使用初始化列表直接设置 range
    // 当创建新对象时自动调用
    DistanceSensor(double r) : range(r) {}

    // 公共方法,演示对象如何通过函数具备行为
    void displayRange() {
        std::cout << &#34;Current range: &#34; << range << &#34; meters&#34; << std::endl;
    }
};

int main() {
    // 创建对象并传入初始值 1.5
    DistanceSensor sensor(1.5);
    sensor.displayRange(); // 输出: Current range: 1.5 meters
    return 0;
}

在这个例子中,range 被声明为 private,这意味着任何外部代码都不能直接修改它,必须通过公共接口(如构造函数或未来的 setter 方法)来交互。这种设计使得未来如果需要改变存储距离的方式(例如从米改为厘米),只需修改类内部逻辑,而不影响使用该类的外部代码。

小结:封装是 OOP 的基石。通过将数据标记为 private,你强制所有交互通过受控的方法进行,从而防止意外修改并提高代码的可维护性。

模块化代码组织:头文件与分离编译

随着项目规模扩大,将所有代码放在一个 .cpp 文件中会导致编译缓慢且难以管理。C++ 的标准做法是将类的声明放在头文件(.h.hpp)中,将实现放在源文件(.cpp)中。

包含守卫(Include Guards)

为了防止同一个头文件被多次包含导致编译错误,必须使用包含守卫。

// Robot.hpp
#ifndef ROBOT_HPP_
#define ROBOT_HPP_

class Robot {
public:
    // 只声明方法,不指定具体实现
    void greet();
};

#endif // ROBOT_HPP_

#ifndef ROBOT_HPP_:如果未定义宏 ROBOT_HPP_,则继续执行。

#define ROBOT_HPP_:定义该宏,标记此头文件已被处理。

#endif:结束条件编译块。

如果在大型项目中,多个 .cpp 文件都包含了 Robot.hpp,编译器会先检查当前目录,再检查系统路径(如 /usr/include)。尖括号 <> 表示只在系统路径查找,而双引号 &#34;&#34; 表示先在当前目录查找。

声明与实现的分离

robot.cpp 中,我们需要包含头文件并实现具体的逻辑。注意使用作用域解析运算符 :: 来表明方法属于哪个类。

// robot.cpp
#include &#34;robot.hpp&#34; // 双引号查找当前目录下的头文件
#include <iostream>

// 实现 greet 方法,使用 Robot::greet 语法表明归属
void Robot::greet() {
    std::cout << &#34;Hello, I am a robot.&#34; << std::endl;
}

int main() {
    Robot myRobot;
    myRobot.greet(); // 输出: Hello, I am a robot.
    return 0;
}

这种分离不仅提高了编译效率(修改 .cpp 无需重新编译整个项目),还清晰地定义了模块间的契约:头文件告诉使用者“我能做什么”,而 .cpp 文件隐藏了“我是怎么做的”。

易错点:忘记添加包含守卫会导致重复定义错误;在 .cpp 中实现成员函数时,务必加上类名前缀(如 Robot::),否则会被视为全局函数。

访问修饰符:控制可见性

C++ 提供了三种访问修饰符,用于精确控制类成员的可见性,这对于构建安全的机器人系统至关重要。

修饰符 可见范围 适用场景
private 仅类内部 敏感数据(如电池电量、内部状态标志)
protected 类内部及派生类 供子类继承和复用的数据或方法
public 任何地方 对外提供的 API 接口

以下示例展示了这三种修饰符的实际应用:

#include <iostream>
#include <string>

class Robot {
private:
    int batteryLevel; // 私有:仅限类内访问,防止外部直接篡改

protected:
    int maxSpeed;     // 受保护:类及其子类可访问,便于继承复用

public:
    // 公有:任何地方均可访问
    void setBattery(int level) {
        if (level >= 0 && level <= 100) {
            batteryLevel = level;
        }
    }

    void printStatus() {
        std::cout << &#34;Battery: &#34; << batteryLevel 
                  << &#34;, Max Speed: &#34; << maxSpeed << std::endl;
    }
    
    // 构造函数初始化列表
    Robot(int b, int s) : batteryLevel(b), maxSpeed(s) {}
};

int main() {
    Robot r(75, 10);
    r.printStatus(); // 输出: Battery: 75, Max Speed: 10
    
    // r.batteryLevel = 0; // 错误!private 成员不可直接访问
    return 0;
}

通过 private 保护 batteryLevel,我们确保电量只能通过 setBattery 方法进行验证后的更新,避免了非法值的注入。protected 则允许子类在不破坏封装的前提下扩展功能。

小结:合理选择访问修饰符是编写健壮代码的关键。默认情况下,类成员是 private 的,养成显式声明修饰符的习惯有助于代码清晰。

静态关键字(static):共享状态与行为

static 关键字在 C++ 中有两种主要用途:一是让变量在函数调用间保持值,二是让成员属于类本身而非特定实例。

静态局部变量

静态局部变量仅在第一次执行时初始化,之后保留其值。

void incrementCounter() {
    static int count = 0; // 仅初始化一次,后续调用保留旧值
    count++;
    std::cout << count << &#34; &#34;;
}

int main() {
    for (int i = 0; i < 5; ++i) {
        incrementCounter(); // 输出: 1 2 3 4 5
    }
    return 0;
}

在机器人应用中,这可用于记录总行驶里程或抓取次数,无需将这些计数器作为类的成员变量传递。

静态成员变量与函数

静态成员属于类,所有实例共享同一份数据。

class RobotConfig {
public:
    static const double MAX_SPEED; // 声明静态常量
    static void resetSystem() {
        std::cout << &#34;System Reset&#34; << std::endl;
    }
};

const double RobotConfig::MAX_SPEED = 10.0; // 定义静态常量

int main() {
    // 直接通过类名访问,无需创建对象
    std::cout << RobotConfig::MAX_SPEED << std::endl;
    RobotConfig::resetSystem();
    return 0;
}

这在机器人系统中非常有用,例如定义所有机器人实例共享的最大速度限制,或提供全局配置读取函数。

关键点:静态成员不属于任何对象,因此在访问前必须确保已正确定义(特别是非 const 静态成员)。

生命周期管理:构造与析构

对象的创建和销毁过程由构造函数和析构函数自动控制,这是资源管理的核心。

构造函数

构造函数在对象创建时自动调用,用于初始化数据成员。可以使用初始化列表提高效率。

class RobotPos {
private:
    std::string name_; // 后缀下划线表示私有变量
    double x_, y_;

public:
    // 构造函数接收参数并初始化成员
    RobotPos(std::string n, double x, double y) 
        : name_(n), x_(x), y_(y) {
        std::cout << &#34;Created &#34; << name_ 
                  << &#34; at (&#34; << x_ << &#34;, &#34; << y_ << &#34;)&#34; << std::endl;
    }
};

int main() {
    RobotPos r1(&#34;Alpha&#34;, 1.0, 2.0);
    RobotPos r2(&#34;Beta&#34;, 5.0, 7.0);
    return 0;
}

析构函数

析构函数在对象销毁时自动调用(如超出作用域或被 delete),用于清理资源。它以波浪号 ~ 开头。

class RobotController {
public:
    RobotController() {
        std::cout << &#34;Controller Initialized&#34; << std::endl;
    }

    ~RobotController() {
        std::cout << &#34;Controller Shutting Down&#34; << std::endl;
        // 实际场景中:关闭串口、释放内存、断开网络等
    }

    void controlRobot() {
        std::cout << &#34;Controlling Robot...&#34; << std::endl;
    }
};

int main() {
    {
        RobotController controller;
        controller.controlRobot();
    } // controller 在此处超出作用域,析构函数自动调用
    return 0;
}

输出顺序为:初始化 -> 控制 -> 关闭。这确保了即使发生异常退出,资源也能得到适当清理,对于管理硬件接口(如相机、激光雷达)尤为重要。

注意:析构函数不应抛出异常,因为它可能在栈展开过程中被调用,异常可能导致程序终止。

函数重载:灵活的接口设计

函数重载允许定义多个同名但参数列表不同的函数。编译器根据参数的数量、类型和顺序来决定调用哪个版本。

#include <iostream>

void moveRobot(int distance) {
    std::cout << &#34;Moving forward by &#34; << distance << &#34; units&#34; << std::endl;
}

void moveRobot(int x, int y) {
    std::cout << &#34;Moving to position (&#34; << x << &#34;, &#34; << y << &#34;)&#34; << std::endl;
}

int main() {
    moveRobot(10);      // 调用第一个版本
    moveRobot(5, 7);    // 调用第二个版本
    return 0;
}

在机器人控制中,重载非常实用。你可以提供一个简单的 moveRobot(distance) 用于相对移动,同时提供 moveRobot(x, y) 用于绝对坐标定位,从而提供更直观的用户体验。

技巧:重载函数的签名(参数列表)必须不同。仅返回类型不同不足以构成重载。

速查表

封装:使用 private 隐藏数据,通过 public 方法暴露接口,保护数据完整性。

头文件:使用 #ifndef/#define/#endif 包含守卫防止重复包含;声明放 .hpp,实现放 .cpp

访问修饰符private(仅类内)、protected(类及子类)、public(全局)。

Static:静态局部变量跨调用保留值;静态成员属于类而非实例,需单独定义。

构造/析构:构造函数初始化对象,析构函数(~Class)清理资源,遵循 RAII 原则。

重载:同名函数通过不同参数列表区分,提供多种调用方式,提升 API 易用性。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务