C++ 第 40 章:ROS2 Publisher + Timer + Subscriber + Callback + Executor 完整闭环

这一章把 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 → 数据流

串成了一条完整逻辑链。

相关推荐
400分1 小时前
具身智能系列之从零到「机械臂会放试管」:π0.5 VLA 真机部署完全指南【第一节】
算法
专注仿真1 小时前
决策树的算法
开发语言·高性能算法
Evand J1 小时前
【电机滤波例程2】负载转矩增广扩展卡尔曼滤波(EKF)原理与MATLAB例程:五维PMSM状态与负载阶跃估计。订阅专栏后可查看完整代码
开发语言·matlab·电机·ekf·卡尔曼滤波
小宋10211 小时前
一套服务托管多个 LoRA:适配器加载、租户隔离与热切换实战
大数据·人工智能·算法
清水白石0081 小时前
Python 对象模型深度解析:从“一切皆对象”到 id、type、isinstance 底层机制与小整数缓存原理
开发语言·python·缓存
燕落南枝1 小时前
c语言 洛谷P1765手机题解
c语言·开发语言
Rosanci1 小时前
Codex 下载与本地部署实战:从安装到运行全流程指南
开发语言·前端·算法·chatgpt·codex
zhangzeyuaaa1 小时前
深入 Ruby:Block、Proc、Lambda 核心区别与最佳实践
开发语言·前端·ruby
蜗牛互联网2 小时前
Java 17调用Embeddings API:余弦相似度与FAQ拒答阈值
java·开发语言·人工智能