目录
[一、为什么要单独补充 C++ 基础语法?](#一、为什么要单独补充 C++ 基础语法?)
[1.1 ROS2 C++ 代码不是只学 ROS2 就够了](#1.1 ROS2 C++ 代码不是只学 ROS2 就够了)
[1.2 初学者最容易卡住的不是 ROS2,而是 C++ 写法](#1.2 初学者最容易卡住的不是 ROS2,而是 C++ 写法)
[1.3 本篇和前两篇的关系](#1.3 本篇和前两篇的关系)
[二、C++ 文件结构与头文件写法](#二、C++ 文件结构与头文件写法)
[2.1 #include 是什么?](#include 是什么?)
[2.2 尖括号和双引号 include 的区别](#2.2 尖括号和双引号 include 的区别)
[2.3 using namespace 是什么?](#2.3 using namespace 是什么?)
[2.4 main() 函数是什么?](#2.4 main() 函数是什么?)
[三、变量、类型、auto 与 const](#三、变量、类型、auto 与 const)
[3.1 变量是什么?](#3.1 变量是什么?)
[3.2 常见基础数据类型](#3.2 常见基础数据类型)
[3.3 auto 是什么?](#3.3 auto 是什么?)
[3.4 const 是什么?](#3.4 const 是什么?)
[3.5 变量命名习惯](#3.5 变量命名习惯)
[4.1 函数是什么?](#4.1 函数是什么?)
[4.2 函数参数是什么?](#4.2 函数参数是什么?)
[4.3 返回值是什么?](#4.3 返回值是什么?)
[4.4 作用域解析符 :: 是什么?](#4.4 作用域解析符 :: 是什么?)
[4.5 尖括号 <> 是什么?](#4.5 尖括号 <> 是什么?)
[五、class 类、构造函数与成员变量](#五、class 类、构造函数与成员变量)
[5.1 class 是什么?](#5.1 class 是什么?)
[5.2 继承是什么意思?](#5.2 继承是什么意思?)
[5.3 public 和 private 是什么?](#5.3 public 和 private 是什么?)
[5.4 构造函数是什么?](#5.4 构造函数是什么?)
[5.5 初始化列表是什么?](#5.5 初始化列表是什么?)
[5.6 成员变量是什么?](#5.6 成员变量是什么?)
[六、指针、智能指针、this、. 和 ->](#六、指针、智能指针、this、. 和 ->)
[6.1 普通对象和指针对象的区别](#6.1 普通对象和指针对象的区别)
[(1)普通对象用 .](#(1)普通对象用 .)
[(2)指针对象用 ->](#(2)指针对象用 ->)
[6.2 SharedPtr 是什么?](#6.2 SharedPtr 是什么?)
[6.3 std::make_shared 是什么?](#6.3 std::make_shared 是什么?)
[6.4 this 是什么?](#6.4 this 是什么?)
[6.5 std::bind 是什么?](#6.5 std::bind 是什么?)
[6.6 lambda 表达式是什么?](#6.6 lambda 表达式是什么?)
[7.1 本篇文章主要讲了什么?](#7.1 本篇文章主要讲了什么?)
[7.2 最需要记住的几组写法](#7.2 最需要记住的几组写法)
[(2)定义 ROS2 节点类](#(2)定义 ROS2 节点类)
[7.3 后续学习建议](#7.3 后续学习建议)

摘要
摘要
上一篇文章已经整理了 ROS2 C++ 中最基础、最常见的 rclcpp 写法,例如:
cpp
rclcpp::init(argc, argv);
rclcpp::spin(node);
rclcpp::shutdown();
rclcpp::Node
rclcpp::Publisher
rclcpp::Subscription
rclcpp::TimerBase
这些内容主要解决的是:
- 如何看懂一个最基础的 ROS2 C++ 节点?
- 如何创建节点?
- 如何创建发布者?
- 如何创建订阅者?
- 如何创建定时器?
- 如何让节点持续运行?
第一篇 ROS2 C++ 常见 rclcpp 写法 链接如下:
为了让整个专栏形成清晰的学习顺序,可以按照下面 4 篇文章来阅读:
第一篇:ROS2 C++ 常见 rclcpp 写法
第二篇:ROS2 C++ 基础语法
第三篇:ROS2 C++ 进阶语法
第四篇:ROS2 C++ 底盘运动控制算法写法
本篇作为第二篇,重点补充 ROS2 C++ 代码背后的 C++ 基础语法。
因为 ROS2 决定节点能做什么,而 C++ 决定代码怎么组织、怎么调用、怎么运行。
很多初学者看 ROS2 C++ 代码时,真正卡住的往往不是 Topic、Service、Action 这些概念,而是下面这些 C++ 写法:
C++ 基础语法
├── #include 头文件
├── using namespace
├── main() 函数
├── 变量与数据类型
├── auto 自动类型推导
├── const 常量
├── 函数与参数
├── 返回值
├── class 类
├── public / private
├── 构造函数
├── 成员变量
├── 成员函数
├── this 指针
├── . 和 -> 的区别
├── std::shared_ptr
├── std::make_shared
├── std::bind
└── lambda 表达式
本篇文章的目标不是一次性讲完 C++ 所有语法,而是围绕 ROS2 C++ 代码中最常见、最容易卡住初学者的语法进行整理。
学完本篇之后,至少要能看懂下面这些问题:
- class MyNode : public rclcpp::Node 是什么意思?
- public 和 private 有什么区别?
- 构造函数什么时候执行?
- this-> 表示什么?
- msg->linear.x 和 vel_cmd.linear.x 为什么写法不一样?
- SharedPtr 为什么到处都有?
- auto 为什么能自动推导类型?
- std::make_shared 是怎么创建节点对象的?
- std::bind 为什么能绑定回调函数?
- lambda 表达式为什么也能写回调?
后续第三篇会继续整理 ROS2 C++ 进阶语法和工程常见模块,例如 Service、Action、Parameter、QoS、Executor、CallbackGroup、Lifecycle、Component、tf2、message_filters 等。链接如下:
ROS2 C++ 进阶语法保姆级教程-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/162213658?spm=1001.2014.3001.5502第四篇会进一步进入 AGV 底盘运动控制算法写法,结合 PID、运动学模型、RobotState、ChassisParam、dt 计算、速度限幅和里程计更新,说明这些 C++ 写法在真实底盘控制代码中到底怎么用。链接如下:
ROS2 C++ 运动控制算法保姆级教程-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/162246437?spm=1001.2014.3001.5502所以,本篇是连接 rclcpp 基础写法和 ROS2 C++ 工程代码的重要过渡篇。
先把这些 C++ 基础语法看懂,再继续学习 ROS2 C++ 进阶写法和底盘运动控制算法,就不会只停留在"代码能跑",而是能真正理解代码为什么这样写。
一、为什么要单独补充 C++ 基础语法?
1.1 ROS2 C++ 代码不是只学 ROS2 就够了
ROS2 是机器人中间件,C++ 是编程语言。
使用 C++ 写 ROS2 节点时,本质上是在用 C++ 调用 ROS2 提供的接口。
可以这样理解:
ROS2 C++ 代码
├── ROS2 部分
│ ├── rclcpp::Node
│ ├── Publisher
│ ├── Subscription
│ ├── Service
│ ├── Action
│ ├── Parameter
│ ├── QoS
│ └── tf2
│
└── C++ 部分
├── class
├── public / private
├── 构造函数
├── 指针
├── 智能指针
├── auto
├── const
├── std::bind
└── lambda
也就是说:
- ROS2 决定这个节点能做什么;
- C++ 决定这段代码怎么组织、怎么调用、怎么运行。
1.2 初学者最容易卡住的不是 ROS2,而是 C++ 写法
比如下面这段代码:
cpp
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, std::placeholders::_1)
);
}
private:
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "linear.x = %.2f", msg->linear.x);
}
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
};
如果只看 ROS2 功能,这段代码并不复杂:
- 创建一个 ROS2 节点
- 订阅 /cmd_vel 话题
- 收到消息后进入回调函数
- 打印 linear.x
但是初学者真正不理解的往往是这些内容:
- class 是什么?
- public 是什么?
- private 是什么?
- : public rclcpp::Node 是什么?
- LimoTopicSub() : Node("limo_topic_sub") 是什么?
- this-> 是什么?
- <geometry_msgs::msg::Twist> 是什么?
- ::SharedPtr 是什么?
- std::bind 是什么?
- msg->linear.x 为什么用 ->?
所以,这篇文章就是专门把这些 C++ 基础写法拆开讲。
1.3 本篇和前两篇的关系
可以这样理解整个学习路线:
第一篇:ROS2 C++ 常见 rclcpp 写法
├── 先看懂最基础的 ROS2 节点
├── Node
├── Publisher
├── Subscription
└── Timer
第二篇:ROS2 C++ 进阶写法
├── 再建立完整 ROS2 C++ 模块框架
├── Service
├── Action
├── Parameter
├── QoS
├── Executor
├── CallbackGroup
├── tf2
└── message_filters
第三篇:ROS2 C++ 基础语法
├── 最后补齐这些代码背后的 C++ 语法
├── class
├── auto
├── this
├── 指针
├── SharedPtr
├── std::bind
└── lambda
这三篇串起来之后,就能从"看不懂代码"逐渐过渡到"能理解 ROS2 C++ 工程结构"。
二、C++ 文件结构与头文件写法
2.1 #include 是什么?
在 C++ 中,#include 用来引入头文件。
例如 ROS2 C++ 节点中经常会看到:
cpp
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp"
#include 可以理解成:
- 把别人写好的代码接口引入进来
- 后面才能使用里面的类和函数
- 相当于告诉编译器:我要用这些功能
比如:
#include "rclcpp/rclcpp.hpp"
表示引入 ROS2 C++ 的核心功能。
有了它之后,代码里才能使用:
rclcpp::Node
rclcpp::init()
rclcpp::spin()
rclcpp::Publisher
rclcpp::Subscription
如果要使用 Twist 消息类型,需要写:
#include "geometry_msgs/msg/twist.hpp"
有了它之后,才能使用:
geometry_msgs::msg::Twist
2.2 尖括号和双引号 include 的区别
C++ 中常见两种头文件写法:
cpp
#include <memory>
#include "rclcpp/rclcpp.hpp"
简单理解:
(1)#include <xxx>
- 通常用于 C++ 标准库头文件
- 例如 memory、functional、chrono、string
(2)#include "xxx"
- 通常用于项目头文件或 ROS2 功能包头文件
- 例如 rclcpp/rclcpp.hpp、geometry_msgs/msg/twist.hpp
例如:
#include <memory>
表示引入智能指针相关功能。
后面才能使用:
std::shared_ptr
std::make_shared
再比如:
#include <functional>
表示引入函数绑定相关功能。
后面才能使用:
std::bind
std::placeholders::_1
2.3 using namespace 是什么?
ROS2 C++ 代码里经常会看到:
using namespace std::chrono_literals;
这行代码的作用是:
允许直接写 500ms、1s、2s 这种时间单位
比如定时器:
cpp
timer_ = this->create_wall_timer(
500ms,
std::bind(&MyNode::timer_callback, this)
);
如果没有:
cpp
using namespace std::chrono_literals;
那么 500ms 这种写法可能无法识别。
可以这样理解:
using namespace std::chrono_literals
├── 引入 chrono 时间字面量命名空间
├── 让 500ms、1s 这种写法可以直接使用
└── ROS2 定时器中很常见
2.4 main() 函数是什么?
C++ 程序一般从 main() 函数开始执行。
ROS2 C++ 节点中常见结构如下:
cpp
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
可以这样理解:
main()
├── 是 C++ 程序入口
├── 程序从这里开始执行
└── 程序最终也从这里结束
其中:
int main(int argc, char * argv[])
可以简单理解成:
int
├── 表示 main 函数最终返回一个整数
argc / argv
├── 用来接收命令行参数
├── ROS2 会用它解析一些启动参数
所以 ROS2 C++ 程序中一般会把它传给:
rclcpp::init(argc, argv);
三、变量、类型、auto 与 const
3.1 变量是什么?
变量可以理解成:
用来保存数据的名字
例如:
double speed = 0.2;
int count = 0;
bool success = true;
这里:
speed
├── 保存一个小数
count
├── 保存一个整数
success
└── 保存 true 或 false
在 ROS2 小车控制中,经常会看到:
cpp
double linear_x = 0.2;
double angular_z = 0.0;
表示:
linear_x:线速度
angular_z:角速度
3.2 常见基础数据类型
C++ 中常见基础类型如下:
int
├── 整数
double
├── 双精度小数
float
├── 单精度小数
bool
├── true / false
std::string
└── 字符串
例如:
cpp
int count = 0;
double speed = 0.2;
bool is_running = true;
std::string frame_id = "base_link";
在 ROS2 中,这些类型经常用于:
int
├── 计数器、状态码
double / float
├── 速度、位置、角度、时间
bool
├── 是否成功、是否开启、是否急停
std::string
└── 节点名、话题名、坐标系名
3.3 auto 是什么?
auto 表示让编译器自动推导变量类型。
例如:
auto node = std::make_shared<MyNode>();
等价于:
std::shared_ptr<MyNode> node = std::make_shared<MyNode>();
也就是说,auto 可以让代码更简洁。
常见写法:
cpp
auto request = std::make_shared<limo_msgs::srv::LimoSrv::Request>();
auto future = client_->async_send_request(request);
auto topic_names = this->get_topic_names_and_types();
可以这样理解:
auto
├── 自动推导变量类型
├── 右边是什么类型,左边就推导成什么类型
└── 常用于类型很长的对象
在 ROS2 C++ 中,类型经常特别长,例如:
std::shared_ptr<limo_msgs::srv::LimoSrv::Request>
所以用 auto 可以减少代码长度。
3.4 const 是什么?
const 表示这个变量不能被修改。
例如:
const double max_speed = 1.0;
表示:
max_speed 是一个常量,后面不能再修改它的值
如果写:
max_speed = 2.0;
就会报错。
在 ROS2 回调函数中,也经常看到:
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
这里的 const 可以理解成:
- msg 这个指针本身不希望在函数里被随意修改
- msg 这个智能指针变量本身不能被重新赋值、不能被 reset、不能指向别的消息对象。
例如:
cpp
msg = std::make_shared<geometry_msgs::msg::Twist>(); // 不允许
msg.reset(); // 不允许
再比如:
cpp
void print_speed(const double speed)
{
RCLCPP_INFO(this->get_logger(), "speed = %.2f", speed);
}
表示:
speed 只是拿来读取,不应该在函数内部修改
3.5 变量命名习惯
ROS2 C++ 代码中,经常会看到成员变量后面带一个下划线:
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_vel_;
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
rclcpp::TimerBase::SharedPtr timer_;
这里的:
pub_vel_
cmd_sub_
timer_
都是变量名。
后面的 _ 不是语法要求,而是一种常见命名习惯。
它通常表示:
这是一个类的成员变量
这样可以和普通局部变量区分开。
四、函数、参数、返回值与作用域
4.1 函数是什么?
函数可以理解成:
把一段代码封装起来,取一个名字,需要时调用
例如:
cpp
void timer_callback()
{
RCLCPP_INFO(this->get_logger(), "timer callback");
}
这里:
timer_callback
├── 是函数名
void
├── 表示这个函数没有返回值
{}
└── 里面是函数执行的代码
在 ROS2 中,经常会看到这些函数:
cpp
void timer_callback()
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
int main(int argc, char * argv[])
4.2 函数参数是什么?
函数参数就是调用函数时传进去的数据。
例如:
cpp
void print_speed(double speed)
{
RCLCPP_INFO(this->get_logger(), "speed = %.2f", speed);
}
调用时:
print_speed(0.2);
这里:
0.2
├── 传给 speed
speed
└── 在函数内部使用
ROS2 订阅者回调函数中,参数通常是收到的消息:
cpp
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "linear.x = %.2f", msg->linear.x);
}
这里的:
msg
表示 ROS2 收到的消息。
4.3 返回值是什么?
返回值就是函数执行完之后返回给外部的结果。
例如:
cpp
double add(double a, double b)
{
return a + b;
}
调用:
cpp
double result = add(1.0, 2.0);
此时:
result = 3.0
如果函数前面写的是 void,表示没有返回值:
cpp
void timer_callback()
{
// 没有 return 结果
}
ROS2 回调函数大多数都是 void,因为它们主要是被触发后执行一段逻辑,不需要返回结果给调用者。
4.4 作用域解析符 :: 是什么?
在 ROS2 C++ 中,经常看到:
cpp
rclcpp::Node
std::bind
geometry_msgs::msg::Twist
这里的 :: 叫作用域解析符。
简单理解:
:: 表示从某个命名空间或类里面找东西
例如:
rclcpp::Node
意思是:
从 rclcpp 里面找到 Node
再比如:
geometry_msgs::msg::Twist
意思是:
从 geometry_msgs 里面找到 msg,再从 msg 里面找到 Twist
再比如:
std::bind
意思是:
从 std 标准库命名空间里面找到 bind
4.5 尖括号 <> 是什么?
在 ROS2 C++ 中,经常看到:
cpp
rclcpp::Publisher<geometry_msgs::msg::Twist>
rclcpp::Subscription<geometry_msgs::msg::Twist>
this->create_publisher<geometry_msgs::msg::Twist>()
这里的**<> 表示模板参数**。
简单理解:
<> 用来告诉这个类或函数:我要处理哪种类型的数据
例如:
rclcpp::Publisher<geometry_msgs::msg::Twist>
表示:
这是一个发布 Twist 消息的发布者
如果换成:
rclcpp::Publisher<std_msgs::msg::String>
表示:
这是一个发布 String 消息的发布者
所以可以简单记住:
Publisher<Twist>
├── 发布 Twist 类型消息
Subscription<Twist>
└── 订阅 Twist 类型消息
五、class 类、构造函数与成员变量
5.1 class 是什么?
class 是 C++ 中非常重要的语法。
可以理解成:
class = 自己定义一种类型
在 ROS2 C++ 中,我们经常用**class 定义一个节点类**。
例如:
cpp
class MyNode : public rclcpp::Node
{
public:
MyNode() : Node("my_node")
{
}
};
这里表示:
定义一个类
类名叫 MyNode
它继承自 rclcpp::Node
所以 MyNode 是一个 ROS2 节点类
5.2 继承是什么意思?
这行代码:
cpp
class MyNode : public rclcpp::Node
可以拆开理解:
class MyNode
├── 定义一个自己的类,名字叫 MyNode
: public rclcpp::Node
├── 继承 ROS2 官方提供的节点类 rclcpp::Node
└── 让 MyNode 具备 ROS2 节点能力
因为继承了 rclcpp::Node,所以 MyNode 可以使用:
this->create_publisher()
this->create_subscription()
this->create_wall_timer()
this->get_logger()
this->now()
这也是为什么 ROS2 C++ 节点类经常要写成:
class XxxNode : public rclcpp::Node
5.3 public 和 private 是什么?
在类里面,经常会看到:
public:
private:
它们表示访问权限。
可以简单理解:
public
├── 外部可以访问
└── 一般放构造函数、对外接口
private
├── 外部不能直接访问
└── 一般放回调函数、成员变量、内部函数
例如:
cpp
class MyNode : public rclcpp::Node
{
public:
MyNode() : Node("my_node")
{
}
private:
void timer_callback()
{
}
rclcpp::TimerBase::SharedPtr timer_;
};
这里:
MyNode()
├── 是 public
├── 外部创建对象时需要调用
timer_callback()
├── 是 private
├── 只在类内部使用
timer_
└── 是 private
用来保存定时器对象
5.4 构造函数是什么?
构造函数是在创建对象时自动执行的函数。
例如:
cpp
class MyNode : public rclcpp::Node
{
public:
MyNode() : Node("my_node")
{
RCLCPP_INFO(this->get_logger(), "node has started.");
}
};
其中:
MyNode()
就是构造函数。
它的特点是:
- 名字和类名一样
- 没有返回值
- 创建对象时自动执行
当执行:
auto node = std::make_shared<MyNode>();
就会创建一个 MyNode 对象,并自动执行 MyNode() 构造函数。
5.5 初始化列表是什么?
这行代码:
MyNode() : Node("my_node")
中间的:
: Node("my_node")
叫初始化列表。
在 ROS2 C++ 中,它的作用是:
- 调用父类 rclcpp::Node 的构造函数
- 并设置当前节点名字为 my_node
所以:
MyNode() : Node("my_node")
可以理解成:
创建 MyNode 对象时
先创建一个 ROS2 节点
节点名字叫 my_node
启动后可以通过命令查看:
ros2 node list
正常情况下会看到:
/my_node
5.6 成员变量是什么?
成员变量就是定义在类里面的变量。
例如:
cpp
class MyNode : public rclcpp::Node
{
private:
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
这里:
pub_
├── 是发布者成员变量
timer_
└── 是定时器成员变量
为什么要把发布者、订阅者、定时器写成成员变量?
- 因为这些对象需要在节点运行期间一直存在。
- 如果只写成构造函数里的局部变量,函数结束后对象可能被释放,节点就无法继续正常发布、订阅或定时触发。
所以 ROS2 C++ 中经常写成:
private:
rclcpp::Publisher<MsgT>::SharedPtr pub_;
rclcpp::Subscription<MsgT>::SharedPtr sub_;
rclcpp::TimerBase::SharedPtr timer_;
六、指针、智能指针、this、. 和 ->
6.1 普通对象和指针对象的区别
在 C++ 中,对象访问成员时有两种常见写法:
cpp
vel_cmd.linear.x
msg->linear.x
这两个看起来很像,但含义不同。
(1)普通对象用 .
例如:
cpp
geometry_msgs::msg::Twist vel_cmd;
vel_cmd.linear.x = 0.1;
vel_cmd.angular.z = 0.0;
这里 vel_cmd 是一个普通对象,所以访问成员变量时用:
.
也就是:
vel_cmd.linear.x
(2)指针对象用 ->
例如:
cpp
void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "%.2f", msg->linear.x);
}
这里 msg 是一个智能指针,所以访问里面的数据时用:
->
也就是:
msg->linear.x
可以简单记住:
普通对象
├── 用 .
指针对象
└── 用 ->
6.2 SharedPtr 是什么?
ROS2 C++ 中经常看到:
::SharedPtr
例如:
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_;
rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr sub_;
rclcpp::TimerBase::SharedPtr timer_;
SharedPtr 表示共享智能指针。
简单理解:
SharedPtr
├── 是一种智能指针
├── 可以自动管理对象生命周期
├── 不需要手动 delete
└── ROS2 C++ 中非常常见
- ROS2 里,发布者、订阅者、定时器、服务端、客户端等对象经常使用
SharedPtr保存。- 因为这些对象通常需要在节点运行期间一直存在。
6.3 std::make_shared 是什么?
创建智能指针对象时,经常会看到:
auto node = std::make_shared<MyNode>();
可以拆开理解:
std::make_shared<MyNode>()
├── 创建一个 MyNode 对象
├── 并用 shared_ptr 智能指针管理它
auto node
└── 自动推导 node 的类型
完整含义是:
创建一个 MyNode 节点对象
并把它交给智能指针 node 管理
这句代码等价于:
std::shared_ptr<MyNode> node = std::make_shared<MyNode>();
只是 auto 让写法更简洁。
6.4 this 是什么?
在类的成员函数中,经常会看到:
this->get_logger()
this->create_publisher()
this->create_subscription()
this->now()
这里的 this 表示:
当前对象自己
因为我们的类继承了 rclcpp::Node,所以当前对象本身就是一个 ROS2 节点对象。
例如:
this->get_logger()
意思是:
通过当前节点对象获取日志器
再比如:
this->create_publisher<geometry_msgs::msg::Twist>("/cmd_vel", 10)
意思是:
通过当前节点对象创建一个发布者
可以这样理解:
this
├── 当前类对象自己
this->
├── 通过当前对象访问成员函数或成员变量
6.5 std::bind 是什么?
ROS2 订阅者和定时器里经常会看到:
std::bind(&MyNode::cmd_callback, this, std::placeholders::_1)
std::bind 的作用是:
把一个函数绑定成可以被 ROS2 调用的回调函数
比如订阅者:
cpp
sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel",
10,
std::bind(&MyNode::cmd_callback, this, std::placeholders::_1)
);
其中:
&MyNode::cmd_callback
├── 表示 MyNode 类里面的 cmd_callback 函数
this
├── 表示当前这个 MyNode 对象
std::placeholders::_1
└── 表示预留第一个参数位置
这个参数就是 ROS2 收到的消息 msg
完整意思是:
当 /cmd_vel 收到消息时
ROS2 会把收到的消息作为第一个参数
传给当前对象的 cmd_callback 函数
6.6 lambda 表达式是什么?
除了 std::bind,ROS2 C++ 中也可以使用 lambda 表达式写回调。
例如:
cpp
sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
"/cmd_vel",
10,
[this](const geometry_msgs::msg::Twist::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "linear.x = %.2f", msg->linear.x);
}
);
这里的:
cpp
[this](const geometry_msgs::msg::Twist::SharedPtr msg)
{
...
}
就是lambda 表达式。
可以简单理解成:
lambda
├── 是一种匿名函数
├── 可以直接写在创建订阅者的位置
├── 不一定需要单独定义 callback 函数
└── 适合简单回调逻辑
其中:
[this]
表示在 lambda 里面可以使用当前对象的 this。
比如:
this->get_logger()
就需要 [this] 把当前对象捕获进去。
七、总结
本篇文章作为 ROS2 C++ 系列的第二篇,主要补充了 ROS2 C++ 代码背后的 C++ 基础语法。
上一篇我们已经知道了 ROS2 C++ 节点中最常见的 rclcpp 写法:
第一篇:ROS2 C++ 常见 rclcpp 写法
├── rclcpp::init()
├── rclcpp::spin()
├── rclcpp::shutdown()
├── rclcpp::Node
├── Publisher
├── Subscription
└── Timer
而本篇重点解释这些代码为什么要这样组织、这样调用、这样访问对象。
本篇最核心的内容可以总结为:
#include
├── 引入头文件
main()
├── C++ 程序入口
auto
├── 自动推导变量类型
const
├── 表示变量不希望被修改
class
├── 定义一个类
public / private
├── 控制访问权限
构造函数
├── 创建对象时自动执行
成员变量
├── 保存对象运行期间需要一直存在的数据
this
├── 表示当前对象
.
├── 普通对象访问成员
->
├── 指针对象访问成员
SharedPtr
├── 共享智能指针
std::make_shared
├── 创建智能指针对象
std::bind
├── 绑定回调函数
lambda
└── 匿名函数写法
对于 ROS2 C++ 初学者来说,下面几组写法一定要反复看懂:
auto node = std::make_shared<MyNode>();
意思是:
创建一个 MyNode 对象
并用智能指针 node 管理
class MyNode : public rclcpp::Node
意思是:
定义一个自己的节点类
继承 ROS2 的 rclcpp::Node
让它具备 ROS2 节点能力
MyNode() : Node("my_node")
意思是:
创建节点对象时
设置 ROS2 节点名为 my_node
vel_cmd.linear.x = 0.1;
意思是:
vel_cmd 是普通对象
所以用 . 访问成员
msg->linear.x
意思是:
msg 是智能指针
所以用 -> 访问成员
std::bind(&MyNode::cmd_callback, this, std::placeholders::_1)
意思是:
把当前对象的 cmd_callback 函数
绑定成 ROS2 订阅者回调函数
收到消息后自动调用
学习 ROS2 C++,不要把 ROS2 和 C++ 完全割裂开。
更好的学习顺序是:
第一步:先看懂 rclcpp 基础写法
↓
第二步:补齐 C++ 基础语法
↓
第三步:理解 ROS2 C++ 进阶模块
↓
第四步:进入 AGV 底盘运动控制算法实战
下一篇将继续进入:
第三篇:ROS2 C++ 进阶语法
├── Service
├── Action
├── Parameter
├── Logging
├── Time
├── QoS
├── Executor
├── CallbackGroup
├── LifecycleNode
├── Component
├── tf2
└── message_filters
也就是说,本篇解决的是"ROS2 C++ 代码为什么这样写",下一篇会继续解决"完整 ROS2 C++ 工程中还会遇到哪些常见模块"。
把前两篇内容理解清楚之后,再看后面的 Service、Action、QoS、Executor、tf2 和 AGV 底盘控制代码,就会轻松很多。