这一章把 C++ 对象模型放进真实 ROS2 数据流。
先分清三条线:
对象关系
谁指向谁
控制流
哪个函数调用哪个函数
数据流
数据到底从哪里变到哪里
以后看复杂代码都按这三条线读。
40.1 Publisher 完整代码
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"
class NumberPublisher : public rclcpp::Node
{
public:
NumberPublisher()
: Node("number_publisher"),
count_(0)
{
publisher_ =
this->create_publisher<std_msgs::msg::Int32>(
"number",
10
);
timer_ =
this->create_wall_timer(
std::chrono::milliseconds(1000),
[this]()
{
publishNumber();
}
);
}
private:
void publishNumber()
{
std_msgs::msg::Int32 msg;
msg.data = count_;
count_++;
publisher_->publish(msg);
}
rclcpp::Publisher<
std_msgs::msg::Int32
>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
int count_;
};
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
auto node =
std::make_shared<NumberPublisher>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
ROS 2 的 C++ 节点确实常采用"Node 派生类 + Publisher/Subscription 智能指针成员 + callback"的组织方式;官方 Humble 代码中也能看到 create_publisher、create_subscription、timer 和成员 callback 的组合。ROS Documentation
40.2 Publisher:对象关系
首先:
auto node =
std::make_shared<NumberPublisher>();
创建:
node
↓
NumberPublisher对象
构造函数执行:
publisher_ =
this->create_publisher<...>();
又创建 Publisher 对象:
NumberPublisher
│
publisher_
↓
Publisher对象
然后:
timer_ =
this->create_wall_timer(...);
创建:
NumberPublisher
│
timer_
↓
Timer对象
所以:
node
↓
NumberPublisher
├─ publisher_ → Publisher
├─ timer_ → Timer
└─ count_
40.3 Publisher:控制流
程序第一次启动:
main()
↓
rclcpp::init()
↓
make_shared<NumberPublisher>()
↓
NumberPublisher构造函数
↓
Node父类构造
↓
count_=0
↓
create_publisher()
↓
create_wall_timer()
↓
构造完成
↓
spin(node)
然后等待。
1000ms 到:
Timer事件ready
↓
Executor调度Timer callback
↓
执行lambda
↓
[this]
让lambda找到当前NumberPublisher
↓
publishNumber()
然后:
publisher_->publish(msg);
40.4 Publisher:数据流
第一次:
count_ = 0
↓
创建msg对象
↓
msg.data = count_
↓
msg.data = 0
↓
count_++
↓
count_ = 1
↓
publish(msg)
第二次:
count_ = 1
↓
msg.data = 1
↓
count_ = 2
↓
发布1
最终 Topic:
0
1
2
3
...
40.5 注意:msg 这里是对象
std_msgs::msg::Int32 msg;
所以:
msg.data
用 .。
因为:
msg
= Int32对象本体
40.6 Subscriber 加进来
现在写接收方:
#include <iostream>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"
class NumberSubscriber : public rclcpp::Node
{
public:
NumberSubscriber()
: Node("number_subscriber")
{
subscription_ =
this->create_subscription<std_msgs::msg::Int32>(
"number",
10,
[this](
std_msgs::msg::Int32::ConstSharedPtr msg)
{
receiveNumber(msg);
}
);
}
private:
void receiveNumber(
std_msgs::msg::Int32::ConstSharedPtr msg)
{
std::cout
<< "收到数字:"
<< msg->data
<< std::endl;
}
rclcpp::Subscription<
std_msgs::msg::Int32
>::SharedPtr subscription_;
};
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
auto node =
std::make_shared<NumberSubscriber>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
Humble 代码中确实广泛使用消息类型的 ConstSharedPtr 作为只读回调消息指针,并把 Subscription 本身保存在 rclcpp::Subscription<...>::SharedPtr 成员中。ROS Documentation
40.7 Subscriber 对象关系
auto node =
std::make_shared<NumberSubscriber>();
产生:
node
↓
NumberSubscriber对象
构造函数执行:
subscription_ =
this->create_subscription<...>();
于是:
NumberSubscriber对象
│
subscription_
↓
Subscription对象
而 Subscription 内部登记:
Topic = "number"
callback =
[this](ConstSharedPtr msg)
{
receiveNumber(msg);
}
所以大概:
NumberSubscriber
│
├─ subscription_
│ ↓
│ Subscription
│ │
│ ├─监听 number
│ └─保存callback
│
└─ receiveNumber()
40.8 Subscriber callback 里的 msg 为什么突然又是指针?
Publisher 端:
std_msgs::msg::Int32 msg;
这里:
msg = 对象
所以:
msg.data
Subscriber 端:
std_msgs::msg::Int32::ConstSharedPtr msg
这里:
msg = 智能指针
↓
收到的Int32消息对象
所以:
msg->data
必须用:
->
这两个 msg 只是碰巧变量名相同。
类型完全不同。
40.9 Subscriber 内部消息流
Publisher 发:
Int32消息
data = 5
ROS2 通信系统将这条消息交给接收端。
接收端:
Subscription发现有新消息
↓
对应callback ready
↓
Executor执行callback
执行:
[this](
std_msgs::msg::Int32::ConstSharedPtr msg)
{
receiveNumber(msg);
}
此时:
msg
│
▼
收到的Int32消息对象
┌──────────┐
│ data = 5 │
└──────────┘
然后:
receiveNumber(msg);
注意这里:
传进去的不是
5。
传进去的是:
指向整个消息对象的智能指针
msg。
40.10 receiveNumber(msg) 又发生什么?
函数:
void receiveNumber(
std_msgs::msg::Int32::ConstSharedPtr msg)
得到这个智能指针。
于是:
msg->data
表示:
msg
↓
找到消息对象
↓
访问data
↓
5
打印:
收到数字:5
40.11 [this] 和 msg 完全不是一个东西
这是目前最容易混淆的地方。
Lambda:
[this](ConstSharedPtr msg)
左边:
[this]
是:
捕获外部环境。
让 lambda 找得到:
NumberSubscriber对象
右边:
(ConstSharedPtr msg)
是:
函数参数。
当新消息到达时,ROS2 把:
消息指针
传进来。
所以:
[this]
→ 我是谁?
msg
→ 我收到什么?
这一句非常值得记。
40.12 为什么 Subscriber lambda 需要 [this]?
因为里面:
receiveNumber(msg);
是:
NumberSubscriber的成员函数。
lambda 想调用:
当前对象.receiveNumber(...)
就必须知道:
当前对象是谁。
所以:
[this]
记住当前对象地址。
于是:
lambda
│
├─ this → NumberSubscriber对象
│
└─ msg → 收到的消息对象
这张图特别重要。
40.13 this 和 msg 是两条不同的指针链
this
↓
NumberSubscriber对象
↓
receiveNumber()
而:
msg
↓
Int32消息对象
↓
data
所以执行:
receiveNumber(msg);
本质是:
this
找到"谁来处理"
msg
告诉它"处理什么"
这个就是 callback 最本质的结构。
40.14 Publisher + Subscriber 完整对象图
进程A
──────────────────────────────
node
↓
NumberPublisher
├─ timer_
│ ↓
│ Timer
│ └─ lambda
│ └─ this ─────┐
│ │
├─ publisher_ │
│ ↓ │
│ Publisher │
│ │
└─ count_ │
▲ │
└───────────────┘
│
Topic
/number
│
进程B
──────────────────────────────
node
↓
NumberSubscriber
├─ subscription_
│ ↓
│ Subscription
│ └─ lambda
│ ├─ this → NumberSubscriber
│ └─ msg → 收到的消息
│
└─ receiveNumber()
40.15 完整控制流
现在把整个系统一次走完。
Publisher程序main
↓
创建NumberPublisher对象
↓
创建Publisher
↓
创建Timer
↓
spin
同时:
Subscriber程序main
↓
创建NumberSubscriber对象
↓
创建Subscription
↓
登记callback
↓
spin
然后:
Publisher Timer到1000ms
↓
Timer callback ready
↓
Executor执行lambda
↓
[this]找到NumberPublisher
↓
publishNumber()
↓
创建msg
↓
msg.data=count_
↓
publisher_->publish(msg)
↓
ROS2通信系统
↓
/number
↓
Subscriber收到消息
↓
Subscriber callback ready
↓
Subscriber Executor执行lambda
↓
[this]找到NumberSubscriber
↓
msg指向收到的消息
↓
receiveNumber(msg)
↓
msg->data
↓
打印
↓
继续spin
ROS 2 的示例程序本身也是这种"Publisher 周期发送、Subscriber callback 接收"的模式。ROS Documentation
40.16 这里面真正有几种"流"?
这是以后阅读所有 C++ / ROS2 代码的方法。
第一种:对象关系流
node
→ Node对象
publisher_
→ Publisher对象
timer_
→ Timer对象
subscription_
→ Subscription对象
this
→ 当前类对象
msg
→ 消息对象
回答:
谁能找到谁?
第二种:控制流
main
↓
constructor
↓
spin
↓
callback
↓
member function
↓
publish
回答:
哪个函数接着执行哪个函数?
第三种:数据流
count_
↓
msg.data
↓
Topic
↓
Subscriber收到的msg
↓
msg->data
↓
receiveNumber
回答:
真正的数据从哪里到哪里?
40.17 指针不等于数据流
例如:
publisher_
→ Publisher
是对象关系。
而:
count_
→ msg.data
→ Topic
才是数据流。
不要再把:
指针
理解成:
信息在里面流动的管子。
指针更像:
地址/遥控器/句柄------让我能够找到另一个对象。
40.18 Lambda 到底处于什么位置?
Timer lambda:
Timer事件
↓
lambda
↓
this找到Publisher节点对象
↓
publishNumber
Subscriber lambda:
新消息事件
↓
lambda
├─ this → 找到Subscriber节点对象
└─ msg → 得到消息对象
↓
receiveNumber(msg)
所以 lambda 的主要意义不是:
"传消息"。
而是:
把未来某个事件发生时需要执行的行为保存下来。
40.19 Executor / spin 在这里的位置
不是:
Publisher
↓
spin
↓
Topic
而是:
事件产生
↓
callback ready
↓
Executor
↓
执行callback
例如 Publisher 端:
Timer到期
↓
Timer callback ready
↓
Executor
↓
lambda
Subscriber 端:
消息到达
↓
Subscriber callback ready
↓
Executor
↓
lambda
而:
rclcpp::spin(node);
就是让对应执行机制持续工作。
40.20 现在把 VLA 换进去
未来你的程序可能:
Python VLANode
↓
Publisher
↓
/vla_action
↓
C++ RobotController
↓
Subscription
↓
callback
Subscriber lambda 可能同时拥有:
[this]
→ 找到RobotController对象
msg
→ 找到VLA Action消息
然后:
[this](ActionMsg::ConstSharedPtr msg)
{
target_ = msg->target;
}
这里就是:
msg->target
↓
读取VLA消息数据
this->target_
↓
修改当前RobotController对象内部状态
于是完整数据转移:
VLA消息对象
↓
msg->target
↓
当前RobotController对象
↓
target_
这就是以后你真正会写的东西。
39/40 两章最终总图
现在你应该把整个阶段压成:
C++ class
↓
创建对象
↓
constructor初始化
↓
┌────────────┴────────────┐
│ │
this 成员
│ │
│ publisher_
│ timer_
│ subscription_
│
▼
当前对象
进入 ROS2:
Timer
↓
callback
↓
this找到当前Publisher节点对象
↓
创建msg
↓
Publisher.publish(msg)
↓
Topic
↓
Subscription
↓
callback
├─ this找到Subscriber节点对象
└─ msg找到消息对象
↓
成员函数处理数据
再加上:
事件ready
↓
Executor调度
↓
callback执行
至此,第 39/40 章才算真正把:
类 → 对象 → 构造 → 继承 → this → 指针 → 智能指针 → lambda → Publisher → Timer → Subscriber → callback → spin/Executor → 数据流
串成了一条完整逻辑链。