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 << "Current range: " << range << " meters" << 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)。尖括号 <> 表示只在系统路径查找,而双引号 "" 表示先在当前目录查找。
声明与实现的分离
在 robot.cpp 中,我们需要包含头文件并实现具体的逻辑。注意使用作用域解析运算符 :: 来表明方法属于哪个类。
// robot.cpp
#include "robot.hpp" // 双引号查找当前目录下的头文件
#include <iostream>
// 实现 greet 方法,使用 Robot::greet 语法表明归属
void Robot::greet() {
std::cout << "Hello, I am a robot." << 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 << "Battery: " << batteryLevel
<< ", Max Speed: " << 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 << " ";
}
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 << "System Reset" << 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 << "Created " << name_
<< " at (" << x_ << ", " << y_ << ")" << std::endl;
}
};
int main() {
RobotPos r1("Alpha", 1.0, 2.0);
RobotPos r2("Beta", 5.0, 7.0);
return 0;
}
析构函数
析构函数在对象销毁时自动调用(如超出作用域或被 delete),用于清理资源。它以波浪号 ~ 开头。
class RobotController {
public:
RobotController() {
std::cout << "Controller Initialized" << std::endl;
}
~RobotController() {
std::cout << "Controller Shutting Down" << std::endl;
// 实际场景中:关闭串口、释放内存、断开网络等
}
void controlRobot() {
std::cout << "Controlling Robot..." << std::endl;
}
};
int main() {
{
RobotController controller;
controller.controlRobot();
} // controller 在此处超出作用域,析构函数自动调用
return 0;
}
输出顺序为:初始化 -> 控制 -> 关闭。这确保了即使发生异常退出,资源也能得到适当清理,对于管理硬件接口(如相机、激光雷达)尤为重要。
注意:析构函数不应抛出异常,因为它可能在栈展开过程中被调用,异常可能导致程序终止。
函数重载:灵活的接口设计
函数重载允许定义多个同名但参数列表不同的函数。编译器根据参数的数量、类型和顺序来决定调用哪个版本。
#include <iostream>
void moveRobot(int distance) {
std::cout << "Moving forward by " << distance << " units" << std::endl;
}
void moveRobot(int x, int y) {
std::cout << "Moving to position (" << x << ", " << y << ")" << 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 易用性。

查看7道真题和解析