C++ 第 32 课:main()、rclcpp::init()、spin()、shutdown()

这里之前的问题是我突然把 "ROS2 节点"这个新概念 当成你已经认识的东西直接用了,确实会造成跳跃。

以后遇到这种情况,我会明确告诉你:

这是旧知识,还是新概念;如果是新概念,它和我们已经学过的 C++ 知识是什么关系。

先把这里彻底捋顺。

一、你目前理解的这句话基本正确

代码:

复制代码
class MyNode : public rclcpp::Node
{
public:
    MyNode()
        : Node("my_node")
    {
    }
};

你现在理解成:

MyNode 是子类,继承父类 Node;然后在 MyNode 的构造函数中调用父类 Node 的构造函数,并传入 "my_node" 这个名字。

完全正确。

如果暂时不管 ROS2,它就是普通的 C++ 继承与构造:

复制代码
父类:
Node

       ↑ 继承

子类:
MyNode

创建:

复制代码
MyNode node;

时:

复制代码
先构造父类 Node
↓
Node("my_node")
↓
再构造 MyNode 自己的部分

这就是你已经学过的 C++ 知识。


二、"ROS2 节点"是我们之前学过的东西吗?

不是。

这是一个刚加入的 ROS2 新概念。

我们以前学的是:

复制代码
变量
函数
class
对象
继承
构造函数
指针

这些都是 C++ 概念。

而:

ROS2 Node / ROS2 节点

是 ROS2 提出来的一种软件概念。

所以现在要把两个世界连接起来:

复制代码
C++世界                 ROS2世界

class
对象             ←→     Node节点

继承
构造函数

三、什么叫"ROS2 节点"?

先不看代码。

假设一台机器人里面同时运行很多程序:

复制代码
程序A:读取摄像头

程序B:跑VLA

程序C:控制机械臂

程序D:读取关节状态

ROS2 不把它们简单称为:

程序 A、程序 B......

而经常把这种独立参与 ROS2 通信的程序单元叫:

Node,节点

例如:

复制代码
camera_node
负责相机

vla_node
负责VLA推理

robot_controller
负责机械臂控制

所以现在先把 Node 理解成:

ROS2 系统中一个独立工作的程序单元。

它能够:

复制代码
发消息
收消息
提供服务
接收请求
运行定时任务

四、那 ROS2 Node 是不是"对象"?

这里要分成两个层次。

从 ROS2 概念上讲

Node 是:

ROS2 系统中的一个运行节点。

它是一种软件角色/运行实体。

从我们现在使用的 C++ 实现上讲

ROS2 的 C++ 库提供了:

复制代码
rclcpp::Node

这个 C++ 类。

我们创建出来的对象,例如:

复制代码
MyNode node;

就是一个:

C++ 对象

同时,因为 MyNode 继承了:

复制代码
rclcpp::Node

它也具有:

ROS2 Node 的身份和能力。

所以你可以先这样记:

复制代码
ROS2里的"节点"
= 一个概念

rclcpp::Node
= ROS2为了在C++里实现这个概念而提供的类

MyNode node;
= 我们真正创建出来的C++对象
  同时也是一个ROS2节点

这个区分很重要。


五、用机械设计类比

用机械概念比较容易理解。

假设有一个概念:

"汽车"

这是一个现实概念。

C++ 里面定义:

复制代码
class Car
{
};

这是:

用代码描述汽车。

然后:

复制代码
Car my_car;

这是:

真正创建出来的一辆具体"汽车对象"。

对应 ROS2:

复制代码
ROS2 Node
↓
"节点"这个概念

rclcpp::Node
↓
C++里描述Node的类

MyNode node;
↓
实际创建出来的Node对象

所以我以后如果说:

"创建一个 ROS2 节点"

更严格地说,就是:

创建一个继承 rclcpp::Node 的 C++ 对象,让它作为 ROS2 Node 运行。


六、再看你的代码

复制代码
class MyNode : public rclcpp::Node

【旧知识:class + 继承】

表示:

定义 MyNode 类,它继承 rclcpp::Node。

其中:

复制代码
MyNode

是我们自己定义的子类。

复制代码
rclcpp::Node

是 ROS2 提供的父类。


然后:

复制代码
MyNode()

【旧知识:构造函数】

这是 MyNode 的构造函数。


然后:

复制代码
: Node("my_node")

【旧知识:父类构造 + 初始化列表】

表示:

构造 MyNode 对象时,先调用父类 Node 的构造函数。

传入:

复制代码
"my_node"

七、那 "my_node" 到底给谁?

严格说:

它传给的是父类 Node 的构造函数。

也就是说:

复制代码
Node("my_node")

负责初始化当前 MyNode 对象内部继承来的 Node 部分。

但这个 Node 部分就是:

当前整个对象作为 ROS2 节点所依赖的基础部分。

所以 ROS2 最后会把这个节点识别为:

复制代码
my_node

八、你可以这样理解整个对象

创建:

复制代码
MyNode node;

内部粗略画成:

复制代码
node 这个C++对象

┌──────────────────────────────┐
│ rclcpp::Node 父类部分       │
│                              │
│ ROS2节点名 = "my_node"       │
│ ROS2通信能力                 │
│ Publisher / Subscriber能力  │
├──────────────────────────────┤
│ MyNode 自己新增的部分       │
│                              │
│ 我们自己的变量              │
│ 我们自己的函数              │
└──────────────────────────────┘

它不是:

复制代码
一个父类Node对象
+
另一个MyNode对象

而是:

一个完整的 MyNode 对象,里面包含父类 Node 部分。


九、所以你的那句话我稍微修改一下就非常准确

你说:

"这里的子类 MyNode 继承了父类 Node,之后在构造函数里面为我们的 Node 赋予名字。"

更严谨地改成:

MyNode 继承了父类 rclcpp::Node;创建 MyNode 对象时,通过初始化列表调用父类 Node 的构造函数,把 "my_node" 作为这个 ROS2 节点的名字传进去。

这就是当前最准确、也最适合你的理解。


十、以后我会加这种提示

例如以后出现:

复制代码
rclcpp::spin(node);

我会先告诉你:

复制代码
【旧知识】
node 是一个对象/智能指针

【ROS2新知识】
spin 是 ROS2 的事件循环机制

而不是直接说:

"让节点 spin。"

这样你就不会突然碰到没定义过的概念。

第 32 课:main()、rclcpp::init()、spin()、shutdown()

这一课我们正式解决:

一个 ROS2 节点到底怎么"跑起来"?

先看最小骨架:

复制代码
int main(int argc, char** argv)
{
    rclcpp::init(argc, argv);

    auto node =
        std::make_shared<MyNode>();

    rclcpp::spin(node);

    rclcpp::shutdown();

    return 0;
}

先不要怕,我们只拆四步。


1. main() ------【旧知识】

你已经学过:

复制代码
int main()

这是:

C++ 程序入口。

程序从这里开始执行。

现在 ROS2 常写:

复制代码
int main(int argc, char** argv)

这里多出来的:

复制代码
int argc
char** argv

是:

命令行参数。

暂时不用深入。

你只需要知道:

ROS2 可能需要读取启动程序时附带的参数。


2. rclcpp::init(argc, argv) ------【ROS2新知识】

复制代码
rclcpp::init(argc, argv);

先把:

复制代码
rclcpp::

理解成:

ROS2 C++ 库里面的。

所以:

复制代码
rclcpp::init(...)

就是:

调用 ROS2 C++ 库的初始化功能。

可以理解成:

复制代码
C++程序刚启动
↓
初始化ROS2环境
↓
之后才能正常使用Node等ROS2功能

3. 创建 Node 对象 ------【旧知识】

复制代码
auto node =
    std::make_shared<MyNode>();

这个我们已经学过。

拆开:

复制代码
MyNode

我们的类。

复制代码
std::make_shared<MyNode>()

创建一个 MyNode 对象,并交给 shared_ptr 管理。

复制代码
auto node

让编译器自动判断类型。

所以:

复制代码
node
↓
智能指针
↓
指向/管理 MyNode 对象

而这个 MyNode 对象:

同时也是我们的 ROS2 节点对象。


4. spin(node) ------【ROS2新知识,最重要】

复制代码
rclcpp::spin(node);

先形象理解成:

让 ROS2 开始值班。

为什么?

假设节点有 Subscriber:

复制代码
等待 /number 消息

如果程序创建完 Node 后马上结束:

复制代码
创建Node
↓
程序结束

它根本来不及等待任何消息。

所以我们需要:

复制代码
rclcpp::spin(node);

告诉 ROS2:

"这个 node 继续运行,不要退出;有事件来了就处理。"


5. spin 可以理解成事件值班室

假设 Node 是值班人员。

执行:

复制代码
rclcpp::spin(node);

以后:

复制代码
Node开始值班
│
├── 没消息 → 等
│
├── 消息来了 → 调callback
│
├── Timer到时间 → 调timer callback
│
├── Service请求 → 处理
│
└── 继续等

所以:

spin() 是为什么 Subscriber 能"一直等消息"的核心原因之一。


6. callback 为什么自动执行?

这就连起来了。

前面我们说:

ROS2 消息到达后自动调用 callback。

但是谁一直在那里等?

答案就是运行中的:

复制代码
rclcpp::spin(node);

可以粗略画:

复制代码
spin(node)
    │
    │ 一直等待事件
    ▼
新消息到达
    │
    ▼
找到对应Subscriber
    │
    ▼
执行callback
    │
    ▼
处理完成
    │
    └────────→继续spin

7. shutdown() ------【ROS2新知识】

复制代码
rclcpp::shutdown();

意思:

关闭 ROS2 运行环境。

正常情况下:

复制代码
init
↓
运行
↓
spin
↓
准备退出
↓
shutdown

8. 完整执行顺序

复制代码
int main(int argc, char** argv)
{
    rclcpp::init(argc, argv);

    auto node =
        std::make_shared<MyNode>();

    rclcpp::spin(node);

    rclcpp::shutdown();

    return 0;
}

脑子里翻译成:

复制代码
程序启动
↓
初始化ROS2
↓
创建MyNode对象
↓
让这个Node持续运行、等待事件
↓
退出时关闭ROS2
↓
程序结束

9. 和 VLA 场景联系

以后你的控制节点可能:

复制代码
启动
↓
创建 VLAController Node
↓
spin
↓
等待 /vla_action
↓
VLA Action来了
↓
callback执行
↓
读取动作
↓
发送给机械臂
↓
继续等待下一条Action

所以:

复制代码
rclcpp::spin(node);

不是一个无关紧要的语法。

它对应:

机器人程序为什么能够一直在线等待数据并处理事件。


本课小练习

  1. MyNode 是 C++ 类还是 ROS2 运行时对象?

复制代码
auto node = std::make_shared<MyNode>();

执行以后,node 是什么?

复制代码
rclcpp::init(argc, argv);

大概负责什么?

复制代码
rclcpp::spin(node);

为什么不能简单理解成"什么也不干地卡住"?

  1. 如果没有 spin(),一个 Subscriber 节点可能出现什么问题?
相关推荐
汉克老师2 小时前
GESP2026年9月认证C++一级( 第三部分编程题(1、新龟兔赛跑))精讲
c++·gesp·小学生·学c++编程
旋生万物2 小时前
螺旋角分布的功率谱分析:多窗谱 + FDR 校正
开发语言·python
沫璃染墨2 小时前
《从零入门Linux系统篇(五十七):线程篇·十——生产者消费者模型进阶:从环形缓冲区到POSIX信号量》
linux·运维·服务器·开发语言·c++·系统架构·信号处理
时间的拾荒人2 小时前
Qt 窗口系统详解:从菜单栏到对话框的完整指南
开发语言·qt·面试
wuminyu2 小时前
LockStack在虚拟线程Mount和Unmount拷贝过程剖析
java·linux·c语言·jvm·c++
wuyk5553 小时前
Python 网络爬虫入门到实战 第 04 章:请求头、UA 伪装、超时、异常处理、基础反爬绕过
开发语言·爬虫·python
流形填表3 小时前
题库去重实战:基于题干指纹的重复题清理
开发语言·c#
\光辉岁月/3 小时前
3.java运算符
java·开发语言
知识分享小能手3 小时前
C++ 学习教程,从入门到精通,string类和标准模板库 — 完整知识点详解(16)
开发语言·c++·学习