ROS2 C++ 基础语法保姆级教程

目录

摘要

[一、为什么要单独补充 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 最需要记住的几组写法)

(1)创建节点对象

[(2)定义 ROS2 节点类](#(2)定义 ROS2 节点类)

(3)构造函数设置节点名

(4)通过当前节点创建发布者

(5)普通对象用点

(6)指针对象用箭头

(7)绑定回调函数

[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

这些内容主要解决的是:

  1. 如何看懂一个最基础的 ROS2 C++ 节点?
  2. 如何创建节点?
  3. 如何创建发布者?
  4. 如何创建订阅者?
  5. 如何创建定时器?
  6. 如何让节点持续运行?

第一篇 ROS2 C++ 常见 rclcpp 写法 链接如下:

ROS2 C++ 常见 rclcpp 写法保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/162202727?sharetype=blogdetail&sharerId=162202727&sharerefer=PC&sharesource=m0_58954356&spm=1011.2480.3001.8118


为了让整个专栏形成清晰的学习顺序,可以按照下面 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++ 代码中最常见、最容易卡住初学者的语法进行整理。

学完本篇之后,至少要能看懂下面这些问题:

  1. class MyNode : public rclcpp::Node 是什么意思?
  2. public 和 private 有什么区别?
  3. 构造函数什么时候执行?
  4. this-> 表示什么?
  5. msg->linear.x 和 vel_cmd.linear.x 为什么写法不一样?
  6. SharedPtr 为什么到处都有?
  7. auto 为什么能自动推导类型?
  8. std::make_shared 是怎么创建节点对象的?
  9. std::bind 为什么能绑定回调函数?
  10. 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

也就是说:

  1. ROS2 决定这个节点能做什么;
  2. 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 功能,这段代码并不复杂:

  1. 创建一个 ROS2 节点
  2. 订阅 /cmd_vel 话题
  3. 收到消息后进入回调函数
  4. 打印 linear.x

但是初学者真正不理解的往往是这些内容:

  1. class 是什么?
  2. public 是什么?
  3. private 是什么?
  4. : public rclcpp::Node 是什么?
  5. LimoTopicSub() : Node("limo_topic_sub") 是什么?
  6. this-> 是什么?
  7. <geometry_msgs::msg::Twist> 是什么?
  8. ::SharedPtr 是什么?
  9. std::bind 是什么?
  10. 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 可以理解成:

  1. 把别人写好的代码接口引入进来
  2. 后面才能使用里面的类和函数
  3. 相当于告诉编译器:我要用这些功能

比如:

复制代码
#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>

  1. 通常用于 C++ 标准库头文件
  2. 例如 memory、functional、chrono、string

(2)#include "xxx"

  1. 通常用于项目头文件或 ROS2 功能包头文件
  2. 例如 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 可以理解成:

  1. msg 这个指针本身不希望在函数里被随意修改
  2. 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()

就是构造函数

它的特点是:

  1. 名字和类名一样
  2. 没有返回值
  3. 创建对象时自动执行

当执行:

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

就会创建一个 MyNode 对象,并自动执行 MyNode() 构造函数。


5.5 初始化列表是什么?

这行代码:

复制代码
MyNode() : Node("my_node")

中间的:

复制代码
: Node("my_node")

初始化列表

在 ROS2 C++ 中,它的作用是:

  1. 调用父类 rclcpp::Node 的构造函数
  2. 并设置当前节点名字为 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_
└── 是定时器成员变量

为什么要把发布者、订阅者、定时器写成成员变量?

  1. 因为这些对象需要在节点运行期间一直存在。
  2. 如果只写成构造函数里的局部变量,函数结束后对象可能被释放,节点就无法继续正常发布、订阅或定时触发。

所以 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++ 中非常常见
  1. ROS2 里,发布者、订阅者、定时器、服务端、客户端等对象经常使用 SharedPtr 保存。
  2. 因为这些对象通常需要在节点运行期间一直存在。

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 底盘控制代码,就会轻松很多。