尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ROS 2 C++ 第一个节点:从工作空间到运行完整工程指南

发布时间:2026/9/17 16:59:04

资讯中心
01
ARTICLE

ROS 2 C++ 第一个节点:从工作空间到运行完整工程指南

ROS 2 C++ 第一个节点:从工作空间到运行完整工程指南
很多人第一次碰 ROS 2都会在节点这个词上卡一下。文档里说节点是计算图的基本单元教程里又说第一个程序就是写节点可真当你打开编辑器准备敲代码时会发现拦路的不是概念而是一堆环境变量、编译配置和奇怪的报错。我带的几个新人里几乎没有人是被代码难住的——二十行 C 就能跑起来的东西能有多难——反倒是ros2 run报找不到包、colcon build提示找不到rclcpp这类问题能耗掉一整个晚上。所以这篇我想换个角度不把编写你的第一个节点当成一段孤立代码来讲而是把它当成一个完整的、从零到跑通的小工程来拆。ROS 2、C、节点这三个关键词背后其实是一条理解执行单元—搭好功能包—写代码—配编译—跑起来—排错的完整链路我会按这条链路一段段讲清楚同时把那些教程里一带而过、但实操中一定会踩的坑挖出来。适合刚装上 ROS 2、会一点 C 语法、想真正把第一个节点跑通的朋友。1. 别急着敲代码ROS 2 里的节点到底是个什么东西节点Node在 ROS 2 里被官方定义为计算图中的基本执行单元这句话很多资料都抄来抄去但它其实没有回答一个更实在的问题一个节点在机器上到底对应着什么我更喜欢用一个工厂的类比。整个机器人系统就像一个工厂工厂里有很多工位每个工位负责一件具体的事——有的负责读激光雷达、有的负责算路径、有的负责驱动电机。一个节点就是这样一个工位。它不是一条消息也不是一个函数而是一个能独立运转、能跟别的工位交换信息的活体。在操作系统层面它最终就是一个进程或者进程里的一段执行逻辑有自己的名字、自己订阅和发布的内容。理解到这一层你就会明白为什么 ROS 2 要把节点作为入门第一课因为后面所有的话题、服务、动作、参数都是挂在节点身上的。节点是主语其他都是谓语和宾语。你写不出一个节点后面所有的通信机制都无从谈起。1.1 节点、可执行文件、进程这三个概念很多人混着用这是新手最容易糊在一起的地方我单独拎出来说。可执行文件是编译产物是磁盘上的一个二进制进程是它被加载进内存、正在运行的那个实例节点是 ROS 2 语义层面对这个运行实例的称呼。三者默认情况下是一一对应的编译出一个可执行文件ros2 run跑起来变成一个进程这个进程在 ROS 2 里注册成一个节点。但它们又不是严格绑定rclcpp 支持在一个进程里创建多个节点也就是所谓的多节点单进程这在资源紧张或者需要共享数据的场景下很有用反过来同一个可执行文件可以被启动多次变成多个同名的进程和节点后者需要用命名空间或参数区分。搞清楚这层关系后面遇到节点名重复一个进程里好几个节点这类问题时你就不至于懵。1.2 第一个示例为什么建议用 C 而不是 Python 写ROS 2 同时支持 C 和 Python很多教程会先教 Python理由是上手快、不用编译。这话没错但我还是建议第一个节点用 C 写原因很实在Python 把这些编译配置、依赖声明、运行时环境统统藏起来了你跑通了也不知道背后发生了什么而 C 会把整条链路——依赖声明、CMake 配置、编译、安装、环境加载——全暴露在你面前。跑通一个 C 节点的过程本质上是一次对 ROS 2 工程结构的完整体验。等你后面自己建包、加依赖、发布包这些知识一个都少不了。而且机器人开发里对实时性和性能敏感的部分比如控制循环、点云处理绝大多数最终还是要落到 C 上。早一点熟悉这套东西后面少走弯路。2. 动手之前工作空间、功能包与一个节点的出生地代码还没写先得给节点准备一个家。ROS 2 里这个家叫工作空间Workspace节点住在里面的**功能包Package**里。很多新手觉得目录结构是小事随手建个文件夹就开干结果后面编译、source、运行各种乱。我建议一开始就把结构建对因为这是后面所有操作的基准。2.1 工作空间不是随便建的一个文件夹它有一套约定一个标准的工作空间长这样根目录下有一个src目录你自己写的所有功能包源码都放在src里面编译之后colcon会在根目录生成build、install、log三个目录分别放编译中间产物、最终安装结果和日志。关键点是源码只放src编译产物全在install。你运行节点时用的source install/setup.bash加载的就是install里的东西。为什么要这么分因为 ROS 2 支持叠加overlay你可以在一个已经 source 过的底层环境之上叠加自己的install环境。这样你的包和系统里自带的包可以共存同名时你的优先级更高。理解 overlay 机制后面遇到为什么我改的代码没生效为什么两个地方有同一个包就不会慌。建工作空间就两行mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src2.2 用 pkg create 一键生成带骨架的包比起手搓目录和文件我更推荐用官方命令生成骨架它能帮你把package.xml和CMakeLists.txt的框架自动写好少踩很多格式坑ros2 pkg create --build-type ament_cmake \ --node-name my_first_node \ --dependencies rclcpp \ my_first_pkg这条命令里几个参数都值得说一下。--build-type ament_cmake是指定用 CMake 构建体系C 包必须选这个Python 包选 ament_python--node-name my_first_node会顺手帮你生成一个名为my_first_node.cpp的源文件里面就是节点的骨架--dependencies rclcpp会自动在你的package.xml和CMakeLists.txt里写上对rclcpp的依赖声明省得你手动加漏了。my_first_pkg是包名命名规范是小写字母加下划线别用大写和连字符否则编译时会报奇怪的错。2.3 生成出来的目录里每个文件是干什么的生成完之后进到包里看一眼你会看到几个文件我逐个说清楚src/my_first_node.cpp节点源码你的主战场。CMakeLists.txtCMake 构建脚本告诉编译器怎么编译、依赖谁、安装到哪。package.xml包的身份证声明包名、版本、依赖、构建工具。include/my_first_pkg/头文件目录单文件节点用不上但工程里会往这里放公共头文件。这四个东西里CMakeLists.txt和package.xml是新手最容易忽略、也最容易出问题的地方。代码写得再对这两处配错一样编译不过或者跑不起来。下一节我们先看代码再回头收拾这两个配置文件。3. 逐行拆解一个最小 C 节点的骨架与运行逻辑现在来看真正的主角——代码。我先把一个最简版本摆出来它短到你可能觉得这就完了但它确实是一个能跑、能注册进 ROS 2 图、能被ros2 node list看到的合法节点。3.1 二十行版本先把最小可运行路径跑通#include rclcpp/rclcpp.hpp int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node std::make_sharedrclcpp::Node(my_first_node); RCLCPP_INFO(node-get_logger(), Hello, ROS 2!); rclcpp::spin(node); rclcpp::shutdown(); return 0; }逐行说。第一行引入rclcpp的头文件这是 ROS 2 的 C 客户端库所有节点相关的类型都在里面。rclcpp::init(argc, argv)做的是初始化客户端库这件事——解析命令行参数、初始化底层通信比如 DDS。注意它必须在你创建任何节点之前调用否则会崩。std::make_sharedrclcpp::Node(my_first_node)创建了一个节点对象字符串就是这个节点的名字这个名字之后会出现在ros2 node list的输出里。用make_shared而不是裸new是因为 ROS 2 的 API 从设计上就走共享指针管理生命周期spin会持有它。RCLCPP_INFO是一条日志宏第一个参数要从节点的 logger 拿打印出来你会看到带时间戳和节点名的日志。最后rclcpp::spin(node)是关键——它让节点进入事件循环持续处理回调没有它程序打印完一行就退出了。rclcpp::shutdown()负责优雅关闭。3.2 类封装版本这才是工程里的常规写法上面那种写法适合验证环境但真到工程里节点会被写成类因为你要挂发布者、订阅者、定时器、参数裸main放不下这些。标准写法是这样的#include rclcpp/rclcpp.hpp class MyFirstNode : public rclcpp::Node { public: MyFirstNode() : Node(my_first_node) { RCLCPP_INFO(this-get_logger(), Node has been started.); } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node std::make_sharedMyFirstNode(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }区别在于节点逻辑被封装进一个继承自rclcpp::Node的类构造函数里调Node(my_first_node)来命名自己。this-get_logger()拿的是这个类自身的 logger比之前那种手动传节点对象的方式更自然。这种结构的最大好处是可扩展以后你要加一个发布者就在类里加一个成员变量和一个create_publisher调用要加定时器就加一个create_wall_timer。所有东西都挂在类上生命周期清晰。我强烈建议你从一开始就养成用类写节点的习惯别图省事用裸main否则后面每加一个功能都要重构一次。3.3 init、spin、shutdown 这三件套缺一个都不行这三行main里的调用我见过太多人删掉其中某一个然后一头雾水。说清楚它们各自的作用和删掉的后果调用作用删掉/写错的后果rclcpp::init(argc, argv)初始化客户端库、解析参数、启动通信层创建节点时崩溃或通信完全失败rclcpp::spin(node)进入事件循环持续分发回调程序打印后立即退出节点秒退rclcpp::shutdown()优雅关闭释放资源资源泄漏退出时可能报错spin这一行尤其容易被误解。很多人以为它是让程序别退出的sleep其实它是在跑一个事件循环——定时器到期、收到消息、服务请求进来这些事件都会在spin里被分发到对应的回调函数。一个什么都不做的节点spin就是空转等待一旦你加了定时器或订阅者spin就成了这些逻辑的调度中心。所以你不能用while(true)去替代它那样回调根本不会被处理。4. 让编译系统认识你的节点package.xml 与 CMakeLists.txt代码写完了但它现在只是一段躺在src里的文本编译器还不认识它。接下来这一步是新手翻车的高发区——配置package.xml和CMakeLists.txt。我用一句话概括这两者的分工package.xml告诉系统我要依赖谁CMakeLists.txt告诉编译器怎么编译、装到哪。两者配合缺一不可。4.1 package.xml依赖清单漏一个都编译不过打开package.xml你会在里面看到几行depend或build_depend之类的标签。用pkg create --dependencies rclcpp生成的话rclcpp已经被加进去了。如果你手动建包或者后期要用新库必须自己补。举个例子你后面要发一个字符串消息就得用std_msgs那么package.xml里得有dependrclcpp/depend dependstd_msgs/depend为什么这里漏了会出问题因为colcon在编译你的包之前会先看package.xml按里面的依赖去准备环境、解析依赖顺序。你代码里#include了std_msgs但package.xml没声明编译到链接阶段就会报找不到符号或者找不到头文件。一个实用习惯每当你代码里引入一个新库的头文件先到这里把对应依赖补上再回头写CMakeLists.txt。4.2 CMakeLists.txt三段式结构看懂就不怕C 包的CMakeLists.txt看着长但对你写第一个节点来说真正要动的只有三块cmake_minimum_required(VERSION 3.8) project(my_first_pkg) find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) add_executable(my_first_node src/my_first_node.cpp) ament_target_dependencies(my_first_node rclcpp) install(TARGETS my_first_node DESTINATION lib/${PROJECT_NAME} ) ament_package()第一块find_package是找依赖你把package.xml里声明过的东西在这里也find_package一遍。第二块add_executable加ament_target_dependencies是从源码编译出可执行文件并链接依赖。第三块install是把编好的东西装到install目录的正确位置。我见过太多人只写了前两块忘了install结果colcon build一切正常、零报错但一ros2 run就提示找不到可执行文件。原因就出在这ros2 run去的是install目录找你没告诉 CMake 把可执行文件装过去它自然找不到。4.3 ament_target_dependencies 和 target_link_libraries 的区别这两个都是链接依赖的写法容易混。ament_target_dependencies是 ROS 2 封装好的它会自动处理头文件路径、库路径、以及 ROS 相关的一堆编译选项写 ROS 节点优先用它。target_link_libraries是标准 CMake 的写法用于链接普通的三方库。经验法则是依赖 ROS 的包用ament_target_dependencies依赖非 ROS 的普通库比如某个数学库用target_link_libraries。混用不会立刻报错但遇到找不到头文件这种玄学问题时先回来检查这里是不是用错了宏。5. 编译、运行到验证把节点真正跑起来配置文件改完终于到了激动人心的一步。但别急ROS 2 的编译和运行之间隔着一道环境加载的坎很多人就是在这里被绊住的。5.1 colcon build正确的编译姿势回到工作空间根目录编译cd ~/ros2_ws colcon build --packages-select my_first_pkg--packages-select表示只编译指定的包而不是把整个工作空间的所有包都编一遍。工作空间里包一多全量编译很慢指定编译能省时间。编译成功后你会看到install/my_first_pkg/lib/my_first_pkg/my_first_node这个可执行文件——这就是你前面install(TARGETS ...)那段的成果。如果编译报错务必把报错往上翻colcon的报错经常是最后一行是结果真正的错误在中间。5.2 source 与 overlay为什么每次开新终端都要重新 source编译完运行前必须先加载环境source install/setup.bash这一步不能省也不能source 一次就管一辈子。source的作用是把install目录下的环境信息可执行文件路径、库路径、包索引等注入当前终端会话。它只对当前终端生效你关掉终端再开一个新的环境就没了得重新 source。这就是新手最常见的我明明编译成功了怎么在新终端里ros2 run还是找不到包的根源。想省事的话可以在~/.bashrc里加一行自动 source但我建议你至少前几次手动 source把这件事的因果关系刻进肌肉记忆别一上来就交给bashrc遮住问题。5.3 用命令行工具确认节点真的活着运行节点ros2 run my_first_pkg my_first_node正常情况下你会看到[INFO] [my_first_node]: Node has been started.这时候节点正在spin里转。关键验证步骤来了——再开一个终端记得 source输入ros2 node list如果输出里有/my_first_node说明你的节点已经成功注册进 ROS 2 图整个链路彻底跑通了。你还可以进一步用ros2 node info /my_first_node看它的详细信息它当前订阅了什么、发布了什么。如果ros2 node list里没有你的节点那就说明节点跑是跑了但可能名字不对、或者根本没进spin。6. 新手最容易栽的几个坑与对应排查思路前面几节讲的是应该怎么做这一节讲做错了怎么办。我把这些年见过、也自己踩过的高频问题整理成一张排查表遇到问题时按表对号入座比漫无目的地搜索高效得多。现象最可能的原因排查动作ros2 run提示找不到包当前终端没 sourceinstall/setup.bash重新 source再ros2 pkg list确认包找得到找不到可执行文件CMakeLists.txt漏了install(TARGETS...)补上 install 段重新 build编译报找不到rclcpppackage.xml或find_package漏了依赖两处都补 rclcpp重新 build节点启动后立刻退出main里漏了rclcpp::spin(node)补上 spin改了代码运行结果没变没重新 build或 build 了没重新 source重新colcon build并 source编译期一堆找不到符号引入新库但没在依赖里声明补package.xml和 CMake 依赖6.1 找不到包和找不到可执行文件要分清这两个报错看着像成因完全不同分清楚能省一半时间。找不到包通常是环境问题——没 source或者 source 的是别的路径。找不到可执行文件包已经找到了但包里面没有那个名字的程序——要么install没写要么你在ros2 run里写错了可执行文件名。可执行文件名来自CMakeLists.txt里add_executable的第一个参数不是你节点的名字这两个经常不一样别写混。6.2 节点秒退的经典原因节点启动打印一行日志然后立刻结束最典型的元凶就是漏了rclcpp::spin(node)。程序执行到main末尾自然就退出了没有spin就是一条直线走完。还有一种情况是你加了定时器但逻辑写错导致spin立刻返回。排查方法很简单在spin前后各打一条日志看看有没有进去。6.3 编译通过但运行报动态库错误有时候colcon build全绿一运行却报error while loading shared libraries。这多半是运行环境没加载完整或者你的包依赖了某个没有正确装到install里的库。先确认 source 了正确的环境再检查依赖是否在package.xml里声明、有没有被正确安装。这类问题在叠加多个工作空间时尤其常见理清 overlay 的层级能帮大忙。7. 让第一个节点动起来加一个定时发布者的进阶练习光打印一句日志节点是活的但没用。真正让你理解节点价值的是让它发出一串数据。我们给节点加一个定时器和一个发布者让它每个固定间隔往一个话题上发一条消息——这就是 ROS 2 里最经典的talker。7.1 定时器加发布者节点通信的第一步改造后的类版本大致是这样#include rclcpp/rclcpp.hpp #include std_msgs/msg/string.hpp using namespace std::chrono_literals; class Talker : public rclcpp::Node { public: Talker() : Node(talker), count_(0) { publisher_ this-create_publisherstd_msgs::msg::String(chatter, 10); timer_ this-create_wall_timer( 500ms, std::bind(Talker::on_timer, this)); } private: void on_timer() { auto message std_msgs::msg::String(); message.data Hello ROS 2 std::to_string(count_); RCLCPP_INFO(this-get_logger(), Publishing: %s, message.data.c_str()); publisher_-publish(message); } rclcpp::Publisherstd_msgs::msg::String::SharedPtr publisher_; rclcpp::TimerBase::SharedPtr timer_; size_t count_; };注意几个新东西create_publisher类型(话题名, 队列长度)队列长度是通信层的缓冲写 10 是常见做法create_wall_timer(500ms, 回调)是挂一个固定周期定时器500ms这种字面量需要using namespace std::chrono_literals;回调里publisher_-publish(message)才是真正发出去。count_是个计数器让每次发的消息内容都不一样方便你观察。别忘了把std_msgs加进package.xml和CMakeLists.txt的依赖里否则编译不过。7.2 日志和参数让节点看起来正经一点跑起来之后用ros2 topic echo /chatter就能看到不断刷出来的消息。如果你想让节点更像工程里的样子可以再往里加日志分级RCLCPP_DEBUG、RCLCPP_WARN和参数声明。日志直接用RCLCPP_*系列宏第一个参数传this-get_logger()参数则用this-declare_parameter在构造时声明之后可以用命令行ros2 run my_first_pkg talker --ros-args -p 参数名:值在启动时覆盖。这两样东西一开始不必全上但知道它们存在、知道怎么加会让你后面写真正的功能节点时心里有底。我个人带新人的体会是第一个 C 节点最值得花时间的不是代码本身而是那套依赖声明—编译配置—环境加载的链路。真正的老手和新人拉开差距的地方往往就是遇到报错时能不能顺着这条链路一步步定位是先看环境 source 没 source再看package.xml依赖全不全然后看 CMake 里install写没写最后看代码里spin有没有。这套顺序一旦养成后面不管遇到多复杂的编译问题你都有章法可循。另外分享一个小习惯每次开新终端准备跑节点前先敲一句ros2 pkg list | grep 你的包名确认环境里能看到你的包再往下走——这一步能挡掉我见过的至少一半莫名其妙的报错。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。