目录
前言
摘要
一、完整代码示例:/cmd_vel 订阅节点
二、LimoTopicSub 类本身详解
2.1 class LimoTopicSub : public rclcpp::Node 是什么意思?
2.2 rclcpp::Node 是什么?
2.3 继承后 LimoTopicSub 就能当 ROS2 节点用吗?
2.4 LimoTopicSub 和 rclcpp::Node 是完全一样的吗?
三、public 公有成员详解
3.1 构造函数 LimoTopicSub()
3.2 构造函数里面做了什么?
3.3 Node("limo_topic_sub") 是什么意思?
3.4 this->create_subscription() 是什么意思?
3.5 RCLCPP_INFO(this->get_logger(), ...) 是什么意思?
四、private 私有成员详解
4.1 成员函数:cmd_callback()
4.2 为什么说 cmd_callback() 是回调函数?
4.3 const geometry_msgs::msg::Twist::SharedPtr msg 是什么?
4.4 为什么是 msg->linear.x?
4.5 成员变量:cmd_sub_
4.6 为什么 cmd_sub_ 要写成成员变量?
五、ROS2 C++ 常见写法归纳
5.1 geometry_msgs::msg::Twist
5.2 std::bind
5.3 std::placeholders::_1
5.4 SharedPtr
5.5 std::make_shared
5.6 this
5.7 :: 作用域解析符
六、哪些是 LimoTopicSub 自己的成员?
七、总结
前言
在之前的专栏已经详细讲解了话题 Topic 消息 msg、服务 Service 消息 srv 和 行为 Action 消息 action 通信,详细内容见以下链接:
(1)ROS2 C++ 小车控制完整实战(一):官方标准 msg 消息发布与订阅保姆级教程:
ROS2 C++ 小车控制完整实战(一):官方标准 msg 消息发布与订阅保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161829804?spm=1001.2014.3001.5502(2)ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程:
ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161834874?spm=1001.2014.3001.5502(3)ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信保姆级教程:
ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161834909?spm=1001.2014.3001.5502(4)ROS2 C++ 小车控制完整实战(四):自定义 Action 通信保姆级教程
ROS2 C++ 小车控制完整实战(四):自定义 Action 通信保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/162141534?spm=1001.2014.3001.5502
除此之外,还有 ROS2 与 C++ 相关保姆级教程:
(1)ROS2 C++ 回调函数 保姆级教程:
ROS2 C++ 回调函数 保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/162178534?spm=1001.2014.3001.5502
摘要
在 ROS2 C++ 编程中,我们经常会看到下面这种代码:
class LimoTopicSub : public rclcpp::Node { public: LimoTopicSub() : Node("limo_topic_sub") { cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", 10, std::bind(&LimoTopicSub::cmd_callback, this, _1) ); RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started."); } private: void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg) { RCLCPP_INFO(this->get_logger(), "收到速度指令:linear.x = %.2f, angular.z = %.2f", msg->linear.x, msg->angular.z); } rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_; };这段代码看起来不长,但里面包含了ROS2 C++ 节点开发中非常核心的内容:
- 类继承
- 构造函数
- ROS2 节点基类
- 订阅者对象
- 回调函数
- 智能指针
- std::bind
- 占位符 _1
- 消息类型 geometry_msgs::msg::Twist
很多初学者看到rclcpp::Node、rclcpp::Subscription、geometry_msgs::msg::Twist::SharedPtr、std::bind这些写法时,会感觉非常绕。
本篇文章就以/cmd_vel订阅节点为例,详细讲解:
- LimoTopicSub 这个类到底由哪些成员组成?
- rclcpp::Node 是什么?
- 为什么继承它之后就能当 ROS2 节点使用?
- cmd_callback() 为什么是回调函数?
- cmd_sub_ 为什么要写成成员变量?
一、完整代码示例:/cmd_vel 订阅节点
首先来看完整代码。
这个节点的作用是:
- 订阅 /cmd_vel 话题
- 接收 geometry_msgs::msg::Twist 速度消息
- 收到消息后自动触发回调函数
- 打印 linear.x 和 angular.z
完整代码如下:
#include <memory> #include <functional> #include "rclcpp/rclcpp.hpp" #include "geometry_msgs/msg/twist.hpp" using std::placeholders::_1; class LimoTopicSub : public rclcpp::Node { public: LimoTopicSub() : Node("limo_topic_sub") { cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", 10, std::bind(&LimoTopicSub::cmd_callback, this, _1) ); RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started."); } private: void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg) { RCLCPP_INFO(this->get_logger(), "收到速度指令:linear.x = %.2f, angular.z = %.2f", msg->linear.x, msg->angular.z); } rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_; }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); auto node = std::make_shared<LimoTopicSub>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }这段代码中最核心的类是:
class LimoTopicSub : public rclcpp::Node我们可以把这个类里面的内容分成三部分来看:
1. 类本身
2. public 公有成员
3. private 私有成员
二、LimoTopicSub 类本身详解
2.1class LimoTopicSub : public rclcpp::Node是什么意思?
代码如下:
class LimoTopicSub : public rclcpp::Node这句话可以拆成三部分理解:
class LimoTopicSub表示定义一个类,类名叫LimoTopicSub。
public表示公开继承。
rclcpp::Node表示ROS2 C++ 提供的节点基类。
所以整句话的意思是:
定义一个名为 LimoTopicSub 的类,它公开继承自 rclcpp::Node。
也就是说:
LimoTopicSub 继承了 rclcpp::Node,所以它拥有 ROS2 节点的基本能力。
2.2rclcpp::Node是什么?
rclcpp::Node是 ROS2 C++ 提供的节点基类。
可以把它理解成:
ROS2 官方已经写好的“节点模板类”。
它里面封装了一个 ROS2 节点常用的基础功能,例如:
- 节点名称
- 日志功能
- 话题发布功能
- 话题订阅功能
- 服务端功能
- 客户端功能
- 定时器功能
- 参数功能
- 时间功能
- 节点运行相关接口
所以我们自己的类继承了rclcpp::Node之后,才能直接使用这些函数:
this->create_subscription(...) this->create_publisher(...) this->create_wall_timer(...) this->create_service(...) this->create_client(...) this->get_logger() this->now()这些函数并不是我们自己定义的,而是从rclcpp::Node继承来的。
2.3 继承后 LimoTopicSub 就能当 ROS2 节点用吗?
可以这样理解。
因为LimoTopicSub继承了rclcpp::Node,所以它本质上就是一个带有 ROS2 节点能力的类。
在main()函数中,我们可以这样创建它:
auto node = std::make_shared<LimoTopicSub>();然后交给 ROS2 运行:
rclcpp::spin(node);这里spin()能运行node,是因为LimoTopicSub继承了rclcpp::Node。
所以可以总结成一句话:
- rclcpp::Node 提供 ROS2 节点基础能力;
- LimoTopicSub 继承它之后,就可以作为一个 ROS2 节点运行。
2.4 LimoTopicSub 和 rclcpp::Node 是完全一样的吗?
不是完全一样。
更准确地说:
LimoTopicSub = rclcpp::Node 的基础功能 + 自己新增的订阅功能
rclcpp::Node本身提供基础节点能力。- 而
LimoTopicSub在这个基础上,自己又添加了:
- 构造函数 LimoTopicSub()
- 订阅者对象 cmd_sub_
- 订阅回调函数 cmd_callback()
所以它不是一个普通节点,而是:
一个专门用来订阅 /cmd_vel 话题的自定义 ROS2 节点类。可以这样理解:
1. rclcpp::Node:
ROS2 官方提供的基础节点类。2. LimoTopicSub:
继承 rclcpp::Node,并添加 /cmd_vel 订阅逻辑后的自定义节点类。
三、public 公有成员详解
在这段代码中,public里面主要有一个成员:
LimoTopicSub() : Node("limo_topic_sub") { cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", 10, std::bind(&LimoTopicSub::cmd_callback, this, _1) ); RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started."); }这个成员就是构造函数。
3.1 构造函数LimoTopicSub()
LimoTopicSub() : Node("limo_topic_sub")这是类的构造函数。
构造函数的特点是:
- 函数名和类名相同 ✔✔✔
- 没有返回值
- 创建对象时自动调用 ✔✔✔也就是说,当
main()函数执行:
它的作用是:
创建 LimoTopicSub 对象时,自动执行这里面的代码
也就是说,当 main 函数里执行:
auto node = std::make_shared<LimoTopicSub>();创建一个 LimoTopicSub 对象,并用 std::shared_ptr 智能指针管理它。
node 是一个智能指针,它指向一个 LimoTopicSub 节点对象。
就会自动进入这个构造函数。
LimoTopicSub()这个构造函数。
3.2 构造函数里面做了什么?
构造函数里面主要做了两件事情。
第一件事:创建/cmd_vel订阅者。
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", 10, std::bind(&LimoTopicSub::cmd_callback, this, _1) );第二件事:打印节点启动日志。
RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started.");所以构造函数的作用可以总结为:
- 创建 LimoTopicSub 节点对象时,
- 自动初始化节点,
- 创建 /cmd_vel 订阅者,
- 并打印节点启动日志。
3.3Node("limo_topic_sub")是什么意思?
这一行:
LimoTopicSub() : Node("limo_topic_sub")其中的:
Node("limo_topic_sub")表示调用父类rclcpp::Node的构造函数。
它的作用是:
初始化 ROS2 节点部分,并把节点名称设置为 limo_topic_sub。
所以节点启动后,可以通过命令查看:
ros2 node list正常情况下会看到:
/limo_topic_sub注意:
Node("limo_topic_sub")不是普通成员变量,也不是普通函数调用。它是在初始化父类rclcpp::Node。
也就是说,创建LimoTopicSub对象时,先要把它继承来的rclcpp::Node那部分初始化好,并给这个节点起名叫:
limo_topic_sub3.4this->create_subscription()是什么意思?
代码如下:
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", 10, std::bind(&LimoTopicSub::cmd_callback, this, _1) );这里的:
this->create_subscription表示调用当前节点对象的create_subscription()函数。
这个函数是从rclcpp::Node继承来的。
它的作用是:
创建一个订阅者。
完整参数可以这样理解:
this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", // 订阅的话题名 10, // 队列深度 std::bind(&LimoTopicSub::cmd_callback, this, _1) // 回调函数 );也就是说:
- 创建一个订阅者,
- 订阅 /cmd_vel 话题,
- 消息类型是 geometry_msgs::msg::Twist,
- 当收到消息时,自动调用 cmd_callback()。
3.5RCLCPP_INFO(this->get_logger(), ...)是什么意思?
代码如下:
RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started.");这里可以拆成两部分看。
第一部分:
this->get_logger()表示获取当前节点的日志器。
第二部分:
RCLCPP_INFO(...)表示以 INFO 级别打印日志。
所以整体意思是:
使用当前节点的日志系统,打印一条 INFO 级别的日志。
终端输出类似:
[INFO] [limo_topic_sub]: limo_topic_sub node has started.四、private 私有成员详解
在这段代码中,private里面有两个成员:
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg) { ... } rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;分别是:
cmd_callback():成员函数 / 回调函数 cmd_sub_:成员变量 / 订阅者对象4.1 成员函数:cmd_callback()
代码如下:
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg) { RCLCPP_INFO(this->get_logger(), "收到速度指令:linear.x = %.2f, angular.z = %.2f", msg->linear.x, msg->angular.z); }这是一个私有成员函数。
它的作用是:
当 /cmd_vel 话题收到 Twist 消息时,自动执行这个函数。
也就是说,它是一个订阅回调函数。
4.2 为什么说 cmd_callback() 是回调函数?
因为它不是我们主动调用的。
代码中没有写:
cmd_callback();它是在创建订阅者时,交给了 ROS2:
std::bind(&LimoTopicSub::cmd_callback, this, _1)意思是:
以后 /cmd_vel 收到消息时, ROS2 自动调用 LimoTopicSub 类中的 cmd_callback() 函数。所以它的执行流程是:
发布端发布 /cmd_vel 消息 ↓ 订阅端收到 Twist 消息 ↓ ROS2 自动触发 cmd_callback() ↓ cmd_callback() 读取 msg 数据并打印4.3const geometry_msgs::msg::Twist::SharedPtr msg是什么?
回调函数参数是:
const geometry_msgs::msg::Twist::SharedPtr msg可以拆开理解:
geometry_msgs::msg::Twist表示消息类型是Twist。
SharedPtr表示智能指针。
msg表示变量名。
所以这一整句的意思是:
接收一个 Twist 类型的消息指针, 这个指针变量名叫 msg。当/cmd_vel话题收到消息后,ROS2 会把收到的消息传给msg。
所以在回调函数里面可以通过:
msg->linear.x msg->angular.z访问速度数据。
4.4 为什么是msg->linear.x?
因为msg是一个指针。
在 C++ 中:
普通对象访问成员,用 . 指针访问成员,用 ->比如普通对象:
vel_cmd.linear.x指针对象:
msg->linear.x所以:
msg->linear.x表示:
访问 msg 指向的 Twist 消息中的 linear.x 字段。同理:
msg->angular.z表示:
访问 msg 指向的 Twist 消息中的 angular.z 字段。4.5 成员变量:cmd_sub_
代码如下:
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;这是一个私有成员变量。
它的作用是:
保存 /cmd_vel 订阅者对象。完整拆开看:
rclcpp::Subscription<geometry_msgs::msg::Twist>表示这是一个ROS2 订阅者类型,订阅的消息类型是geometry_msgs::msg::Twist。
SharedPtr表示智能指针。
cmd_sub_是变量名,表示 command velocity subscriber,也就是速度指令订阅者。
所以整体意思是:
定义一个订阅者对象指针 cmd_sub_, 它用于保存 /cmd_vel 话题的订阅者。4.6 为什么 cmd_sub_ 要写成成员变量?
因为订阅者对象必须在节点运行期间一直存在。
如果订阅者对象只是一个普通局部变量,那么构造函数执行结束后,局部变量可能被释放,订阅关系也可能失效。
例如不推荐这样写:
auto cmd_sub = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", 10, std::bind(&LimoTopicSub::cmd_callback, this, _1) );这里的cmd_sub是局部变量。
构造函数执行结束后,它的生命周期就结束了。
所以更稳妥的写法是:
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;然后在构造函数中赋值:
cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>( "/cmd_vel", 10, std::bind(&LimoTopicSub::cmd_callback, this, _1) );这样cmd_sub_就会随着LimoTopicSub节点对象一起存在。
节点不销毁,订阅者对象就一直存在。
五、ROS2 C++ 常见写法归纳
5.1geometry_msgs::msg::Twist
geometry_msgs::msg::Twist是ROS2 官方提供的一种消息类型,常用于表示速度。
它里面主要有两个成员:
linear angular其中:
linear.x linear.y linear.z表示线速度。
angular.x angular.y angular.z表示角速度。
小车控制中最常用的是:
linear.x angular.z含义是:
linear.x:小车前进 / 后退速度 angular.z:小车绕 z 轴旋转速度例如:
vel_cmd.linear.x = 0.1; vel_cmd.angular.z = 0.0;表示:
小车以 0.1 m/s 向前运动,不旋转。5.2std::bind
std::bind(&LimoTopicSub::cmd_callback, this, _1)std::bind是 C++ 标准库提供的函数绑定工具。
它的作用是:
把类里面的成员函数绑定成一个可以被 ROS2 调用的回调函数。
因为cmd_callback()是类成员函数,不能像普通函数一样直接传给 ROS2。
所以需要写成:
std::bind(&LimoTopicSub::cmd_callback, this, _1)可以理解为:
- 请 ROS2 将来使用当前这个 LimoTopicSub 对象,
- 去调用它里面的 cmd_callback() 函数,
- 并把收到的消息作为第一个参数传进去。
5.3std::placeholders::_1
using std::placeholders::_1;这句话表示引入占位符_1。
_1的作用是:
占住第一个参数的位置。在订阅回调中:
std::bind(&LimoTopicSub::cmd_callback, this, _1)其中_1对应的是收到的消息msg。
也就是说:
当 /cmd_vel 收到消息时,ROS2 会把这条消息作为第一个参数传给 cmd_callback()。
如果是 Service 回调,通常会看到:
std::bind(&LimoSrvServer::srv_callback, this, _1, _2)其中:
_1 对应 request _2 对应 response5.4SharedPtr
ROS2 C++ 中经常看到:
SharedPtr例如:
geometry_msgs::msg::Twist::SharedPtr msg或者:
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;SharedPtr本质上是智能指针。
可以简单理解为:
SharedPtr 用来安全管理对象生命周期,避免手动 new 和 delete。
在 ROS2 中,很多对象都用SharedPtr管理,例如:
- 节点对象
- 发布者对象
- 订阅者对象
- 定时器对象
- 消息对象
- 服务对象
- 客户端对象
5.5std::make_shared
主函数中有这样一行:
auto node = std::make_shared<LimoTopicSub>();意思是:
创建一个 LimoTopicSub 类型的智能指针对象。
它等价于:
创建一个 LimoTopicSub 节点对象,并交给智能指针管理。
为什么用智能指针?
因为 ROS2 中节点通常需要在运行期间一直存在,使用智能指针可以更安全地管理节点生命周期。
5.6this
代码中经常出现:
this->create_subscription(...) this->get_logger()这里的this表示:当前对象自己。
在本例中,this指向当前的LimoTopicSub节点对象。
所以:
this->create_subscription(...)意思是:
当前这个节点对象创建一个订阅者。
this->get_logger()意思是:
获取当前这个节点对象的日志器。5.7::作用域解析符
ROS2 C++ 代码中经常有很多::,例如:
rclcpp::Node geometry_msgs::msg::Twist std::placeholders::_1这里的::是 C++ 的作用域解析符。
可以简单理解为:
到某个命名空间或类里面去找东西。
例如(1)
rclcpp::Node表示:
到 rclcpp 命名空间中找 Node。
例如(2)
geometry_msgs::msg::Twist表示:
到 geometry_msgs 命名空间中,再到 msg 命名空间中,找到 Twist 类型。
例如(3)
std::bind表示:
到 std 标准库命名空间中找 bind。六、哪些是 LimoTopicSub 自己的成员?
表格总结:
| 代码 | 类型 | 是否属于 LimoTopicSub 自己定义的成员 |
LimoTopicSub() | 构造函数 | 是 |
cmd_callback() | 成员函数 / 回调函数 | 是 |
cmd_sub_ | 成员变量 / 订阅者对象 | 是 |
Node("limo_topic_sub") | 父类构造函数调用 | 不是普通成员 |
this->create_subscription() | 从rclcpp::Node继承来的函数 | 不是自己定义的成员 |
this->get_logger() | 从rclcpp::Node继承来的函数 | 不是自己定义的成员 |
_1 | 占位符 | 不是成员 |
msg | 回调函数参数 | 不是类成员 |
geometry_msgs::msg::Twist | 消息类型 | 不是类成员 |
rclcpp::Subscription | 订阅者类型 | 不是类成员 |
所以LimoTopicSub里面真正自己定义的核心成员主要有三个:
LimoTopicSub()构造函数:创建节点对象时自动执行,用来初始化节点并创建订阅者。
cmd_callback()成员函数:收到/cmd_vel消息时自动触发,用来处理接收到的速度消息。
cmd_sub_成员变量:保存/cmd_vel订阅者对象,让订阅者在节点运行期间一直存在
七、总结
本篇文章主要围绕LimoTopicSub订阅节点类,详细拆解了 ROS2 C++ 中类成员、继承、回调函数和订阅者对象的关系。
核心结论如下:
rclcpp::Node 是 ROS2 C++ 提供的节点基类, 里面封装了节点名称、日志、话题、服务、定时器、参数等基础功能。LimoTopicSub 继承 rclcpp::Node 后, 就拥有了 ROS2 节点的基本能力, 可以调用 create_subscription() 创建订阅者, 也可以调用 get_logger() 打印日志。LimoTopicSub() 是构造函数, 创建节点对象时自动执行, 主要负责初始化节点和创建 /cmd_vel 订阅者。cmd_sub_ 是成员变量, 用于保存 /cmd_vel 订阅者对象, 保证订阅者在节点运行期间一直存在。cmd_callback() 是订阅回调函数, 当 /cmd_vel 收到 Twist 消息时, ROS2 会自动调用它, 并通过 msg->linear.x 和 msg->angular.z 读取速度信息。最后用一句话总结:
- LimoTopicSub 是一个继承自 rclcpp::Node 的自定义 ROS2 订阅节点类;
- 它通过构造函数创建订阅者,通过 cmd_sub_ 保存订阅对象,
- 通过 cmd_callback() 处理收到的 /cmd_vel 速度消息。