我们确实学过一种"比较直白的老写法",但要注意------那种写法主要适用于成员变量;这里的 Node("number_publisher") 是父类初始化,不能简单搬到 {} 里面。
先把这个区别重新捋顺。
一、之前学过的两种初始化写法
假设:
class Motor
{
public:
Motor(int motor_id)
{
id = motor_id;
}
private:
int id;
};
这里是你最开始学的写法:
Motor(int motor_id)
{
id = motor_id;
}
执行逻辑可以粗略理解成:
先创建 id
↓
进入构造函数
↓
再执行
id = motor_id
比如:
Motor motor(3);
就是:
motor_id = 3
然后:
id = 3
二、后来学了"初始化列表"
更标准的写法:
class Motor
{
public:
Motor(int motor_id)
: id(motor_id)
{
}
private:
int id;
};
这里:
: id(motor_id)
就是:
用
motor_id直接初始化 成员变量id。
所以:
Motor motor(3);
可以想成:
创建 motor
↓
直接让 id 初始就是 3
↓
再进入构造函数 {}
三、这两个简单例子的结果差不多,但过程不完全一样
第一种:
Motor(int motor_id)
{
id = motor_id;
}
更像:
先有
id,然后再给它赋值。
第二种:
Motor(int motor_id)
: id(motor_id)
{
}
更像:
id出生的时候直接就是这个值。
所以现代 C++ 更常用第二种。
记:
构造函数 {} 里面
→ 赋值
构造函数 : 后面
→ 初始化
四、那为什么 ROS2 是:
NumberPublisher()
: Node("number_publisher")
{
}
这里比刚才更重要。
因为:
class NumberPublisher : public rclcpp::Node
说明:
NumberPublisher
↓
继承了
↓
rclcpp::Node
因此一个 NumberPublisher 对象里面,可以粗略想成:
NumberPublisher 对象
┌──────────────────────────┐
│ 父类 Node 部分 │
│ │
│ NumberPublisher自己的部分 │
└──────────────────────────┘
创建子类的时候:
父类 Node 必须先构造。
所以:
: Node("number_publisher")
就是:
调用父类
Node的构造函数,用"number_publisher"初始化父类。
五、不能简单写成"老版本"
你可能想到:
NumberPublisher()
{
Node("number_publisher");
}
这个不等价。
为什么?
因为等进入:
{
}
的时候:
当前对象里面真正的父类
Node部分已经应该初始化完成了。
所以:
Node("number_publisher");
放在里面,更像:
临时另外创建一个 Node。
而不是初始化:
当前
NumberPublisher自己继承来的那个 Node。
所以父类构造这种情况,必须在初始化列表里做:
NumberPublisher()
: Node("number_publisher")
{
}
六、用机械装配再理解一次
假设:
Node
=
机械臂的标准底座
而:
NumberPublisher
=
在这个底座上安装了发布器的整机
创建整机:
NumberPublisher()
: Node("number_publisher")
{
}
相当于:
1. 先装真正属于这台机器的底座 Node
并给它编号 number_publisher
2. 底座装好
3. 再进入 {}
安装我们自己的 Publisher 等东西
你不能先把整机装完,再在旁边:
Node("number_publisher");
临时造个新底座。
那不是同一个东西。
七、所以你现在这样记最合适
普通成员变量
初学版:
Motor(int id)
{
this->id = id;
}
更推荐:
Motor(int id)
: id(id)
{
}
父类
class MyNode : public rclcpp::Node
构造时:
MyNode()
: Node("my_node")
{
}
这里不是单纯为了"写得高级"。
而是:
父类必须先初始化。
上一课答案
第 1 题
class NumberPublisher : public rclcpp::Node
意思:
定义一个自己的
NumberPublisher类,并让它继承 ROS2 的rclcpp::Node,因此拥有 ROS2 节点的基础能力。
第 2 题
: Node("number_publisher")
设置的是:
这个 ROS2 节点的节点名称。
这里名字是:
number_publisher
第 3 题
create_publisher<std_msgs::msg::Int32>(
"number",
10
);
其中:
std_msgs::msg::Int32
表示:
Publisher 发布的消息类型是整数消息。
而:
"number"
表示:
Topic 名称。
也就是往 number 频道发送。
第 4 题
std_msgs::msg::Int32 msg;
创建一个 ROS2 整数消息盒子。
msg.data = 100;
把 100 放进去。
publisher_->publish(msg);
使用 Publisher 把消息发出去。
整体:
创建盒子
↓
装100
↓
发送
第 5 题
因为:
publisher_
是一个智能指针。
所以:
对象本体 .
对象指针 ->
因此:
publisher_->publish(msg);
使用 ->。
第 31C 课:Subscriber 到底是什么
这一课先不写完整 ROS2 程序。
Publisher 是:
发消息。
Subscriber 是:
收消息。
还是用微信群类比。
假设:
VLA节点
│
│ 发布动作
▼
/vla_action
│
▼
机器人控制节点
那么:
VLA节点
=
Publisher
/vla_action
=
Topic
机器人控制节点
=
Subscriber
1. Subscriber 不断"监听" Topic
假设有:
/number
这个 Topic。
Publisher 发:
100
Subscriber 订阅:
/number
于是:
Publisher
│
│ 100
▼
/number
│
▼
Subscriber
但 Subscriber 收到以后:
到底要干什么?
这就需要:
callback
2. Subscriber + callback
流程:
Subscriber开始监听 /number
↓
平时没消息
↓
有消息到达
↓
ROS2自动执行 callback
↓
callback读取消息
所以以前我们学 callback 是有原因的。
3. 先做一个完全假的版本
暂时不看 ROS2 真语法。
假设可以这样写:
subscribe("number", callback);
意思:
订阅
numberTopic;有消息的时候执行callback。
然后:
void callback(int data)
{
std::cout << data;
}
如果 Publisher 发:
100
ROS2 可以想象成自动做:
callback(100);
注意:
不是我们自己主动写
callback(100)。
而是消息到达以后,ROS2 帮我们调用。
4. 真实 ROS2 不直接给你一个 int
Publisher 之前不是发:
int x = 100;
而是发:
std_msgs::msg::Int32 msg;
msg.data = 100;
也就是一个消息对象。
因此 Subscriber 收到的也是:
一个
Int32消息对象。
5. 最核心的 callback 长这样
先只看:
void callback(
const std_msgs::msg::Int32::SharedPtr msg)
{
std::cout << msg->data << std::endl;
}
这一句确实比较长。
我们分层。
6. 先看变量名
msg
就是:
收到的消息。
这是程序员自己起的变量名。
也可以叫:
message
只是大家经常写:
msg
7. 再看 SharedPtr
std_msgs::msg::Int32::SharedPtr
不要全部看。
核心就是:
Int32
↓
一个整数消息
SharedPtr
↓
这个消息通过智能指针交给你
所以:
msg
不是消息对象本身,而是:
指向/管理收到的消息对象的智能指针。
可以画成:
msg
│
▼
┌────────────────┐
│ Int32消息 │
│ data = 100 │
└────────────────┘
8. 所以为什么:
msg->data
因为:
msg
是智能指针。
因此:
msg
↓
顺着指针找到消息
↓
访问消息里的 data
于是:
msg->data
如果收到的是 100:
结果 = 100
9. const 现在怎么理解?
完整:
const std_msgs::msg::Int32::SharedPtr msg
现在先简单理解成:
这个回调主要负责读取收到的消息,不希望随便修改它。
严格的 const/智能指针细节我们之后再区分,现在不用钻。
这里先建立工程直觉:
ROS2 把消息交给 callback,我们读取它。
10. 整个 callback 翻译
void callback(
const std_msgs::msg::Int32::SharedPtr msg)
{
std::cout << msg->data << std::endl;
}
翻译成人话:
定义一个回调函数。ROS2 收到一条
Int32消息后,把这个消息通过智能指针msg传进来,然后我们读取其中的data并打印。
11. 整个通信链终于可以画出来了
Publisher:
msg.data = 100;
publisher_->publish(msg);
然后:
Publisher
│
│ Int32:
│ data=100
▼
/number Topic
│
▼
Subscriber
│
│ 消息到达
▼
callback(msg)
│
▼
msg->data
│
▼
100
这就是 ROS2 Topic 通信最核心的逻辑。
12. 和 VLA 完全一样
以后不再是:
Int32
data = 100
而可能是:
VLA Action
├── x
├── y
├── z
├── gripper
└── ...
Subscriber 收到后依然:
消息来了
↓
callback
↓
msg->某个字段
↓
取出VLA动作
↓
给机械臂执行
所以你现在真正需要学的不是死记 API,而是先建立:
Publisher 发 → Topic 传 → Subscriber 收 → callback 处理
这条脑回路。
本课小练习
1
Subscriber 最核心的作用是什么?
2
callback 是我们在主程序里不停主动调用的吗?
3
msg->data
为什么用 ->?
4
如果:
Publisher发送 data=50
Subscriber callback 中:
std::cout << msg->data;
会打印什么?
5
目前用一句话描述完整 ROS2 Topic 通信:
Publisher → ? → Subscriber → ?