C++中的单例模式及具体应用示例


AI 摘要

本文深入探讨了C++中的单例模式及其在机器人自主导航中的应用,特别是如何通过单例模式来管理地图数据。文章详细介绍了单例模式的基本结构、优缺点以及在多线程环境中的应用,强调了其在保证数据一致性和资源管理中的重要性。

接着,文章通过一个实际的路径规划系统示例,展示了如何结合单例模式设计地图管理模块。该系统使用了广度优先搜索(BFS)算法来进行路径规划,并通过多线程实现地图更新和路径规划的并行处理。主要的代码模块包括地图管理类
MapManager、路径规划类 BFSPlanner、以及 ProjectNode 管理多线程操作。通过 CMake
构建系统,用户可以方便地配置和编译该项目。

文章还提供了具体的实现代码,涵盖了地图更新、路径规划、C++单例模式的使用,以及多线程编程的技巧。通过这种方式,读者不仅能掌握 C++
单例模式的实现,还能学到如何将其应用于实际的机器人导航与路径规划系统中。

最后,文章提供了如何在 Ubuntu 系统中构建和运行该示例项目的详细步骤,包括项目的编译、执行以及展示了系统的运行效果。


   C++中的单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。单例模式在很多情况下非常有用,例如在管理全局配置、日志系统、数据库连接等场景中。


一、C++单例模式的简单介绍

1、单例模式的结构

单例模式通常包含以下几个核心部分:

  1. 私有构造函数:构造函数私有化,防止外部创建实例。
  2. 静态私有实例:提供一个静态成员函数来创建或返回类的唯一实例。
  3. 公共静态方法:提供全局访问点,通常命名为 getInstance(),用于获取单例对象。
  4. 删除复制构造函数和赋值运算符:防止复制或赋值,以确保只有一个实例存在。

2、优点

  1. 控制实例数量:确保类只有一个实例,节省资源。
  2. 全局访问:提供一个全局访问点,方便管理状态或数据。
  3. 延迟初始化:可以在第一次使用时才创建实例(懒加载)。

3、 缺点

  1. 隐藏依赖关系:由于全局访问点,可能导致代码难以测试和维护,因为依赖关系不明显。
  2. 多线程问题:在多线程环境下需要特别小心,确保线程安全。
  3. 难以扩展:由于构造函数私有化,无法直接从其他类派生出子类。

4、总结

   C++中的单例模式是一种非常实用的设计模式,特别是在需要共享全局状态或资源时。通过确保只有一个实例存在,单例模式提供了一种有效的管理方法。在多线程环境中使用单例模式时,务必考虑线程安全性。

5、标志性的代码结构

class Singleton {
private:// 1. 私有化构造函数,禁止外部创建Singleton() {}// 2. 禁止拷贝构造和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;public:// 3. 静态方法,提供全局唯一的访问点static Singleton& getInstance() {static Singleton instance;  // 4. 静态实例,延迟初始化,线程安全return instance;}
};

在这里插入图片描述

二、C++实现单例模式的具体应用示例

1、应用示例——总体介绍

   在机器人的自主导航中,机器人经常需要访问二维栅格地图(Occupancy Grid),包括随时间更新的障碍物信息、路径规划和定位模块对地图的查询。如果不同模块频繁读取地图信息而不加管理,可能会导致数据不一致或资源竞争问题。通过单例模式实现一个全局的地图管理类,可以确保数据一致性并简化代码、节省资源。
   在我设计的应用示例中包含单例模式的地图管理、地图更新线程、规划线程三部分内容,再加上主节点,完整的文件路径结构如下:
C++单例模式应用示例——BFS规划和地图更新/
│
├── include/
│   ├── bfs_planner.h
│   ├── map_manager.h
│   └── node.h
│
├── src/
│   ├── map/
│   │   └── map_manager.cpp
│   ├── node/
│   │   ├── node_map.cpp
│   │   ├── node_planner.cpp
│   │   └── node.cpp
│   └── planner/
│       └── bfs_planner.cpp
│
├── CMakeLists.txt
以下是对每个文件的概括总结:
include 文件夹
  1. bfs_planner.h

    • 该文件声明了 BFSPlanner 类,负责实现基于广度优先搜索(BFS)算法的路径规划。主要功能包括计算路径、打印路径、以及显示带路径和障碍物的地图。
  2. map_manager.h

    • 该文件声明了 MapManager 类,用于管理地图数据。它提供获取当前地图、更新地图、打印地图及获取地图尺寸的功能,并通过互斥锁保证线程安全。
  3. node.h

    • 该文件声明了 ProjectNode 类,负责管理项目的核心功能。它启动了两个线程:一个线程负责地图的更新,另一个线程负责路径规划。
src 文件夹
map 文件夹
  1. map_manager.cpp
    • 该文件实现了 MapManager 类的方法。包括地图数据的随机更新(生成障碍物)、打印地图以及获取地图尺寸。通过线程安全的方式管理地图的更新和访问。
node 文件夹
  1. node_map.cpp

    • 该文件实现了与地图更新相关的线程。它模拟地图的随机更新,并定期打印更新后的地图。
  2. node_planner.cpp

    • 该文件实现了路径规划的线程。它从 MapManager 获取地图数据,使用 BFSPlanner 进行路径计算,并打印计算结果。它循环执行路径规划,每两秒进行一次更新。
  3. node.cpp

    • 该文件实现了 ProjectNode 类的构造和析构方法。它初始化了 BFSPlanner 实例并启动了地图更新线程和路径规划线程。
planner 文件夹
  1. bfs_planner.cpp
    • 该文件实现了 BFSPlanner 类的路径规划功能。通过广度优先搜索算法计算从起点到终点的路径,并提供路径打印和带路径显示的地图输出。
其他
  • CMakeLists.txt

    • 该文件是 CMake 构建系统的配置文件,定义了项目的构建规则。它指定了项目名称、C++标准版本、使用的线程库等,并列出了所有源文件以生成可执行文件。
   总的来说,这个应用示例项目实现了一个基于 BFS 算法的路径规划系统。它通过多个模块协同工作,MapManager 模拟并更新地图,BFSPlanner 负责路径规划,而 ProjectNode 类管理整个流程,包括启动地图更新和路径规划的线程。整个系统通过多线程来模拟地图更新并实时计算路径,展示路径规划的过程。

2、应用示例——主节点

   node.hnode.cpp 这两个文件定义并实现了 ProjectNode 类的功能。ProjectNode 类的作用是管理路径规划和地图更新的多线程操作。
   node.h :
#ifndef NODE_H
#define NODE_H#include "map_manager.h"
#include "bfs_planner.h"
#include <iostream>
#include <thread>
#include <chrono>
#include <memory>  // 引入 std::shared_ptrnamespace Project {class ProjectNode {
public:ProjectNode(); ~ProjectNode(); // Threadsvoid mapThread();void plannerThread();std::thread map_thread_;std::thread planner_thread_;private:std::shared_ptr<BFSPlanner> bfsplanner_;  // BFSPlanner 的共享指针
};}  // namespace Project#endif  // NODE_H
   node.cpp :
#include "../../include/node.h"namespace Project {ProjectNode::ProjectNode() {bfsplanner_.reset(new BFSPlanner());  // 使用 reset() 创建 BFSPlanner 实例map_thread_ = std::thread(&ProjectNode::mapThread, this);planner_thread_ = std::thread(&ProjectNode::plannerThread,this);
}
ProjectNode::~ProjectNode() {map_thread_.join();planner_thread_.join();
}}  // namespace Projectint main() 
{Project::ProjectNode Node;std::cout << "Starting Map Manager Simulation..." << std::endl;return 0;
}
node.h

node.h 文件定义了 ProjectNode 类的接口,主要功能是启动和管理两个线程:一个用于更新地图,另一个用于执行路径规划。

主要功能:
  1. ProjectNode 构造函数

    • 构造函数初始化了 bfsplanner_ 作为 BFSPlanner 类的共享指针。BFSPlanner 是用于路径规划的核心类。
    • 同时,构造函数启动了两个线程:map_thread_ 用于地图更新,planner_thread_ 用于路径规划。
  2. ProjectNode 析构函数

    • 析构函数确保两个线程在对象销毁前都能正确地完成任务。通过调用 join() 方法等待线程结束,保证线程安全退出。
  3. mapThreadplannerThread

    • 这两个函数定义了每个线程的行为,但在 node.cpp 中具体实现。
  4. 成员变量

    • bfsplanner_:一个 std::shared_ptr<BFSPlanner>,用于存储 BFSPlanner 类的实例,这个实例负责路径规划。
    • map_thread_planner_thread_:两个 std::thread 类型的成员变量,分别表示用于地图更新和路径规划的线程。
node.cpp

node.cpp 文件实现了 ProjectNode 类的构造函数和析构函数,并创建了两个线程来分别执行地图更新和路径规划任务。

主要实现:
  1. ProjectNode 构造函数

    • bfsplanner_.reset(new BFSPlanner()):这行代码创建了一个新的 BFSPlanner 实例,并将其指针赋给 bfsplanner_reset() 方法用来保证指针指向一个新的实例。
    • map_thread_ = std::thread(&ProjectNode::mapThread, this):创建并启动地图更新线程,线程将调用 ProjectNode 类的 mapThread() 方法。
    • planner_thread_ = std::thread(&ProjectNode::plannerThread, this):创建并启动路径规划线程,线程将调用 ProjectNode 类的 plannerThread() 方法。
  2. ProjectNode 析构函数

    • 在析构函数中,调用 map_thread_.join()planner_thread_.join() 等待两个线程完成任务后再退出。这保证了在 ProjectNode 对象销毁之前,所有线程的执行会正确结束。
  3. main 函数

    • main() 函数创建了一个 ProjectNode 对象并启动整个模拟过程。输出 “Starting Map Manager Simulation…” 用于标识模拟开始。
mapThreadplannerThread

虽然这两个线程函数在 node.h 中声明了,分别在 node_map.cppnode_planner.cpp 中进行具体实现。

  • mapThread 负责定期调用 MapManager 来更新地图并打印更新后的地图。
  • plannerThread 负责定期获取地图信息,并使用 BFSPlanner 来计算路径。

这两个线程循环执行,模拟一个实时更新的环境,其中一个线程在更新地图,而另一个线程在计算路径,确保整个过程并行进行。

   总的来说, node.h:定义了 ProjectNode 类,它启动并管理两个线程(地图更新线程和路径规划线程),并包含了与路径规划和地图管理相关的必要成员。 node.cpp:实现了 ProjectNode 的构造和析构函数,启动了两个线程,并确保它们在对象销毁时正确地退出。这两个文件的主要功能是通过多线程实现地图更新和路径规划的并行处理,从而模拟一个动态环境中的路径规划过程。

3、单例模式的地图管理模块

   结合第一部分的介绍,容易写出类似于如下结构的单例模式的地图管理类MapManager,除了单例模式的必要框架,还增加了获取当前地图数据的接口函数getMap()、获取地图大小的接口函数getMapSize(),方便规划模块调用,增加了随机更新地图的接口函数updateMapRandom()、打印地图的接口函数printMap(),方便地图更新模块对地图进行维护更新(真实应用时应该根据实时的传感器信息对地图进行更新维护,这里为了简化采用了随机更新)
   map_manager.h :
#ifndef MAP_MANAGER_H
#define MAP_MANAGER_H#include <vector>
#include <mutex>
#include <random>
#include <iostream>namespace Project {class MapManager {
public:// 获取单例实例static MapManager& getInstance();// 获取当前地图数据std::vector<int> getMap();// 模拟地图的更新void updateMapRandom();// 打印地图void printMap();// 获取地图尺寸std::pair<int,int> getMapSize();private:// 私有构造函数,禁止外部实例化MapManager();// 禁用拷贝构造和赋值运算符MapManager(const MapManager&) = delete;MapManager& operator=(const MapManager&) = delete;// 地图数据std::vector<int> current_map_;// 线程安全的互斥锁std::mutex map_mutex_;// 地图尺寸int MAP_WIDTH_;int MAP_HEIGHT_;
};}  // namespace Project#endif  // MAP_MANAGER_H
   函数定义在map_manager.cpp中,如下所示:

#include "../../include/map_manager.h"namespace Project {// 静态方法:获取 MapManager 的唯一实例
MapManager& MapManager::getInstance() {static MapManager instance;  // C++11 保证静态局部变量线程安全return instance;
}// 私有构造函数:初始化地图数据和尺寸
MapManager::MapManager() : MAP_WIDTH_(10), MAP_HEIGHT_(10) {  // 初始化地图尺寸current_map_.resize(MAP_WIDTH_ * MAP_HEIGHT_, 0);  // 初始化为 10x10 的全 0 栅格地图
}// 获取当前地图数据(加锁以确保线程安全)
std::vector<int> MapManager::getMap() {std::lock_guard<std::mutex> lock(map_mutex_);return current_map_;
}// 获取地图尺寸
std::pair<int, int> MapManager::getMapSize() {return {MAP_HEIGHT_, MAP_WIDTH_};
}// 模拟地图更新(随机生成障碍物)
void MapManager::updateMapRandom() {std::lock_guard<std::mutex> lock(map_mutex_);  // 确保线程安全std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(1, 10);  // 生成范围为 [1, 10]// 随机更新地图中的障碍物信息for (int& cell : current_map_) {// 1-7 为无障碍物点(70%),8-10 为障碍物点(30%)cell = (dis(gen) <= 7) ? 0 : 1;}std::cout << "Map updated!" << std::endl;
}// 打印地图数据(使用 ANSI 转义序列实现彩色输出)
void MapManager::printMap() {std::lock_guard<std::mutex> lock(map_mutex_);  // 确保线程安全std::cout << std::endl;std::cout << std::endl;std::cout << "Current Map:" << std::endl;std::cout << std::endl;for (int i = 0; i < MAP_HEIGHT_; ++i) {for (int j = 0; j < MAP_WIDTH_; ++j) {int value = current_map_[i * MAP_WIDTH_ + j];if (value == 1) {// 红色表示障碍物std::cout << "\033[31mX \033[0m";  // 红色障碍物} else {// 绿色表示可通行区域std::cout << "\033[32m. \033[0m";  // 绿色可通行区域}}std::cout << std::endl;}std::cout << std::endl;std::cout << std::endl;
}}  // namespace Project

map_manager.hmap_manager.cpp 是该项目中用于管理地图的文件。下面将详细介绍这两个文件中的程序。

map_manager.h

map_manager.hMapManager 类的头文件,声明了该类的接口。

主要功能:
  1. getInstance

    • 这是一个静态方法,用于获取 MapManager 类的唯一实例。它通过静态局部变量实现单例模式,确保在整个程序运行过程中只有一个 MapManager 实例。
  2. getMap

    • 返回当前的地图数据,类型为 std::vector<int>,它表示一个一维数组来存储地图的信息。这个方法使用了互斥锁 (std::mutex) 来确保在多线程环境中线程安全。
  3. updateMapRandom

    • 模拟地图的更新,随机生成障碍物。更新过程通过使用 std::random_devicestd::uniform_int_distribution 随机决定地图上的每个位置是障碍物还是通路。该方法也使用互斥锁确保线程安全。
  4. printMap

    • 打印地图数据。该方法根据 current_map_ 的值打印地图,使用 ANSI 转义序列来着色输出。障碍物用红色 (X) 表示,通行区域用绿色 (.) 表示。
  5. getMapSize

    • 获取地图的尺寸(高度和宽度)。该方法返回一个 std::pair<int, int>,分别表示地图的高度和宽度。
私有成员:
  • current_map_

    • 一个 std::vector<int>,存储当前地图的数据,大小为 MAP_WIDTH_ * MAP_HEIGHT_
  • map_mutex_

    • 一个 std::mutex,用于确保对地图数据的访问是线程安全的。
  • MAP_WIDTH_MAP_HEIGHT_

    • 地图的宽度和高度,默认初始化为 10。
map_manager.cpp

map_manager.cppMapManager 类的实现文件,定义了该类的方法。

主要实现:
  1. getInstance

    • 使用静态局部变量 instance 实现单例模式,这样即使在多线程环境下,MapManager 类的实例也能确保唯一性和线程安全。
  2. MapManager 构造函数

    • 构造函数初始化地图尺寸为 10x10,并将 current_map_ 初始化为全 0,表示一个没有障碍物的地图。current_map_ 被扩展为 std::vector<int>,其大小为 MAP_WIDTH_ * MAP_HEIGHT_
  3. getMap

    • 使用 std::lock_guard<std::mutex> 进行线程安全操作,确保对地图数据的访问不会受到其他线程干扰。返回当前地图的数据。
  4. updateMapRandom

    • 随机生成一个地图,模拟障碍物的更新。每个地图单元的值要么是 0(表示通行),要么是 1(表示障碍物)。通过使用 std::random_devicestd::mt19937 随机生成值,70% 的概率是通行区域(0),30% 的概率是障碍物(1)。每次更新后,打印 “Map updated!”。
  5. printMap

    • 打印地图到控制台。使用 ANSI 转义序列打印不同颜色的字符:
      • X 表示障碍物,红色显示。
      • . 表示通行区域,绿色显示。
    • 打印时每一行表示地图的一行,使用 MAP_WIDTH_MAP_HEIGHT_ 计算并显示二维地图。
线程安全

所有访问和修改地图的操作都使用 std::mutex 加锁,这确保了在多线程环境下,访问地图数据时不会发生竞争条件。

   总的来说MapManager 类的作用是负责管理和操作地图数据。它提供了获取地图、更新地图和打印地图的方法,并且能够确保这些操作在多线程环境下是安全的。通过 getInstance 实现单例模式,确保地图管理器在整个程序中只有一个实例,避免了重复创建和内存浪费。updateMapRandom 方法模拟了一个动态更新的地图,每次调用都会随机生成新的障碍物。

4、地图更新线程

   目前的地图更新线程较简单,直接调用地图管理模块提供的随机更新接口即可,node_map.cpp 文件定义了 ProjectNode 类的 mapThread 方法,主要用于模拟读取地图数据和更新地图的过程。下
   node_map.cpp:
#include "../../include/node.h"namespace Project {// 模拟模块 A:读取地图数据
void ProjectNode::mapThread() {while (true) {MapManager::getInstance().updateMapRandom();MapManager::getInstance().printMap();std::this_thread::sleep_for(std::chrono::seconds(5));}
}}  // namespace Project
mapThread 方法

mapThreadProjectNode 类中的一个线程执行函数,功能是周期性地更新地图并打印更新后的地图。

功能与实现:
  1. 循环执行:

    • while (true) 使得该线程不断地执行,形成一个无限循环,直到线程被外部停止。这个线程每隔一段时间(5秒)执行一次地图更新。
  2. 地图更新:

    • MapManager::getInstance().updateMapRandom():调用 MapManager 类的 updateMapRandom 方法来随机更新地图中的障碍物。此方法模拟了动态的环境,其中地图上的障碍物会在每个周期内随机变化。
  3. 地图打印:

    • MapManager::getInstance().printMap():在更新地图后,调用 printMap 方法打印当前地图的状态。地图会被打印在控制台上,其中障碍物使用红色(X)表示,通行区域使用绿色(.)表示。
  4. 线程休眠:

    • std::this_thread::sleep_for(std::chrono::seconds(5)):在每次更新地图和打印地图之后,线程会休眠 5 秒,模拟一个定时更新地图的过程。这使得地图的更新不至于过于频繁,模拟环境变化的周期性。
   总的来说mapThread 方法通过一个无限循环不断更新地图,打印地图,并在每次更新之间休眠 5 秒。这模拟了一个动态变化的地图环境,可以用于测试和验证路径规划算法。在多线程程序中,mapThread 会在一个独立的线程中运行,确保地图更新过程不会阻塞主线程或其他任务的执行。

5、规划线程

   规划线程plannerThread 方法通过调用 BFSPlanner 的 planPath 方法实现路径规划,定期计算从起点到终点的路径。每次路径规划之后,打印路径规划结果,并每 2 秒休眠一次。目前仅在bfs_planner.cpp中提供了BFSPlanner 一种简单的规划器,感兴趣的小伙伴可自行添加其他规划器。BFSPlanner 类 使用广度优先搜索(BFS)算法来计算路径,并能打印出路径及包含路径和障碍物的地图。通过 isValid 确保搜索过程中只访问有效且可通行的区域。
   node_planner.cpp
#include "../../include/node.h"namespace Project {void ProjectNode::plannerThread() {while (true) {// 获取当前地图auto flat_map = MapManager::getInstance().getMap();std::pair<int, int> mapsize= MapManager::getInstance().getMapSize();std::vector<std::vector<int>> grid(mapsize.first, std::vector<int>(mapsize.second));// 将一维地图转换为二维地图for (int i = 0; i < mapsize.first; ++i) {for (int j = 0; j < mapsize.second; ++j) {grid[i][j] = flat_map[i * mapsize.second + j];}}std::cout << "[Path Planning Module] Calculating path..." << std::endl;// 调用 planPath() 函数规划路径auto path = bfsplanner_ -> planPath(grid, {0, 0}, {mapsize.second-1,  mapsize.first-1});if (!path.empty()) {// 规划成功,打印成功日志std::cout << "[Path Planning Module] Path successfully found!" << std::endl;// 打印带路径的地图bfsplanner_ -> printMapWithPath(grid, path, {0, 0}, { mapsize.second - 1,  mapsize.first - 1 });} else {// 规划失败,打印失败日志std::cout << "[Path Planning Module] Path planning failed. No path found!" << std::endl;}// 线程休眠 2 秒std::this_thread::sleep_for(std::chrono::seconds(2));}
}}  // namespace Project
   bfs_planner.cpp
#include "../../include/bfs_planner.h"namespace Project {// 判断给定的坐标是否在地图范围内且可通行bool BFSPlanner::isValid(const std::vector<std::vector<int>>& map, int x, int y) {return (x >= 0 && x < MAP_WIDTH_&& y >= 0 && y < MAP_HEIGHT_&& map[x][y] == 0);}// 打印路径void BFSPlanner::printPath(const std::vector<std::pair<int, int>>& path) {std::cout << "[Path] ";for (const auto& coord : path) {int x = coord.first;int y = coord.second;std::cout << "(" << x << ", " << y << ") ";}std::cout << std::endl;}// 打印包含路径和障碍物的二维地图void BFSPlanner::printMapWithPath(const std::vector<std::vector<int>>& map,const std::vector<std::pair<int, int>>& path,std::pair<int, int> start,std::pair<int, int> goal){std::cout << std::endl;std::cout << std::endl;std::cout << "Map with Path:" << std::endl;std::cout << std::endl;// 创建一个副本地图用于标记路径std::vector<std::vector<char>> displayMap(MAP_HEIGHT_, std::vector<char>(MAP_WIDTH_, ' '));// 将障碍物标记为 'X'for (int i = 0; i < MAP_HEIGHT_; ++i) {for (int j = 0; j < MAP_WIDTH_; ++j) {if (map[i][j] == 1) {displayMap[i][j] = 'X';}}}// 将路径点标记为 '.'for (const auto& coord : path) {int x = coord.first;int y = coord.second;displayMap[x][y] = '.';}// 标记起点为 'S' 和终点为 'G'displayMap[start.first][start.second] = 'S';displayMap[goal.first][goal.second] = 'G';// 打印地图for (const auto& row : displayMap) {for (const auto& cell : row) {switch (cell) {case 'X':std::cout << "\033[31m" << cell << " \033[0m";  // 红色障碍物break;case '.':std::cout << "\033[32m" << cell << " \033[0m";  // 绿色路径点break;case 'S':std::cout << "\033[33m" << cell << " \033[0m";  // 黄色起点break;case 'G':std::cout << "\033[36m" << cell << " \033[0m";  // 青色终点break;default:std::cout << "  ";  // 空格break;}}std::cout << std::endl;}std::cout << std::endl;std::cout << std::endl;}// 使用 BFS 计算从起点到目标点的路径std::vector<std::pair<int, int>> BFSPlanner::planPath(const std::vector<std::vector<int>>& map,std::pair<int, int> start,std::pair<int, int> goal){MAP_WIDTH_ = map[0].size();MAP_HEIGHT_ = map.size();std::vector<std::pair<int, int>> path;const std::vector<std::pair<int, int>> directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};std::vector<std::vector<bool>> visited(MAP_HEIGHT_, std::vector<bool>(MAP_WIDTH_, false));std::queue<std::pair<std::pair<int, int>, std::vector<std::pair<int, int>>>> q;// 初始化队列,从起点开始q.push(std::make_pair(start, std::vector<std::pair<int, int>>{start}));visited[start.first][start.second] = true;while (!q.empty()) {auto front = q.front();q.pop();auto current = front.first;auto currentPath = front.second;// 如果找到目标点,则返回路径if (current == goal) {return currentPath;}// 遍历四个方向for (const auto& direction : directions) {int dx = direction.first;int dy = direction.second;int nx = current.first + dx;int ny = current.second + dy;if (isValid(map, nx, ny) && !visited[nx][ny]) {visited[nx][ny] = true;auto newPath = currentPath;newPath.push_back(std::make_pair(nx, ny));q.push(std::make_pair(std::make_pair(nx, ny), newPath));}}}std::cout << "No path found!" << std::endl;return {};}}  // namespace Project
node_planner.cpp

node_planner.cpp 文件实现了 ProjectNode 类的 plannerThread 方法,该方法用于路径规划。它主要完成了以下几个步骤:

功能与实现:
  1. 获取当前地图:

    • auto flat_map = MapManager::getInstance().getMap();
    • MapManager 中获取当前地图数据。flat_map 是一个一维的整数数组,表示地图的各个格子(0 表示通行区域,1 表示障碍物)。
  2. 获取地图尺寸:

    • std::pair<int, int> mapsize = MapManager::getInstance().getMapSize();
    • 获取地图的宽度和高度,分别用于后续二维数组的创建。
  3. 将一维地图转换为二维地图:

    • 创建一个二维数组 grid,大小为 mapsize.first 行和 mapsize.second 列。
    • 通过循环将一维的 flat_map 转换为二维的 grid,每个元素表示地图中的一个格子。
  4. 调用路径规划:

    • auto path = bfsplanner_->planPath(grid, {0, 0}, {mapsize.second - 1, mapsize.first - 1});
    • 使用 bfsplanner_BFSPlanner 类的实例)调用 planPath 方法进行路径规划。规划的起点是 (0, 0),终点是地图的右下角 (mapsize.second - 1, mapsize.first - 1)
  5. 打印路径规划结果:

    • 如果找到路径,打印成功消息,并调用 bfsplanner_->printMapWithPath 打印带路径的地图。
    • 如果没有找到路径,打印失败消息。
  6. 线程休眠:

    • std::this_thread::sleep_for(std::chrono::seconds(2));
    • 每次计算路径后,线程会休眠 2 秒,再进行下一次路径规划。这模拟了一个实时的路径规划过程。
bfs_planner.cpp

bfs_planner.cpp 文件实现了 BFSPlanner 类,它负责执行广度优先搜索(BFS)来计算路径,并打印包含路径的地图。

功能与实现:
  1. isValid 方法:

    • 用于判断某个坐标是否在地图范围内并且该位置是否为通行区域(值为 0)。
    • 如果坐标有效且地图上该位置是通行的,返回 true,否则返回 false
  2. printPath 方法:

    • 打印路径中的每个坐标点。
    • 每个路径点格式为 (x, y),路径中的所有点将被打印出来,形成一条路径。
  3. printMapWithPath 方法:

    • 打印包含路径和障碍物的地图。方法通过将障碍物标记为 'X'、路径点标记为 '.',起点标记为 'S',终点标记为 'G' 来显示地图。
    • 使用颜色输出(通过 ANSI 转义序列),红色表示障碍物,绿色表示路径,黄色表示起点,青色表示终点。
  4. planPath 方法:

    • 执行广度优先搜索(BFS)来寻找从起点到终点的路径。
    • 使用队列 q 存储待处理的坐标及其路径。每个元素是一个包含坐标和路径的二元组。
    • 从起点开始,探索四个方向(上、下、左、右),将有效的且未被访问过的坐标加入队列。
    • 如果找到目标点,则返回从起点到目标点的路径。
    • 如果队列为空且未找到目标点,则表示没有路径可达,返回空路径。
BFS 算法细节:
  • 队列存储路径: 广度优先搜索使用队列来确保按层次逐步扩展路径。每次扩展一个新的节点时,都会将当前路径加入队列继续搜索。
  • 路径返回: 一旦找到目标点,当前路径被返回。
  • 路径输出: 找到的路径通过 printPathprintMapWithPath 方法显示在控制台上。
   总的来说, plannerThread 方法通过调用 BFSPlannerplanPath 方法实现路径规划,定期计算从起点到终点的路径。每次路径规划之后,打印路径规划结果,并每 2 秒休眠一次。BFSPlanner 使用广度优先搜索(BFS)算法来计算路径,并能打印出路径及包含路径和障碍物的地图。通过 isValid 确保搜索过程中只访问有效且可通行的区域。整个过程通过两个线程实现并行操作:一个线程负责地图更新,另一个线程负责路径规划,模拟了一个实时的动态环境和路径规划系统。

6、CMakeLists.txt 文件

   CMakeLists.txt 文件是 CMake 构建系统的配置文件,指定了如何编译和链接项目中的源代码文件。下面是文件的详细内容及介绍
   CMakeLists.txt :
# 最低 CMake 版本要求
cmake_minimum_required(VERSION 3.10)# 项目名称和版本
project(MapManagerProject VERSION 1.0 LANGUAGES CXX)# 设置 C++ 标准为 C++11
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)# 启用多线程支持
find_package(Threads REQUIRED)# 包含头文件目录
include_directories(include)# 添加所有的源文件
set(SOURCESsrc/map/map_manager.cppsrc/planner/bfs_planner.cppsrc/node/node.cppsrc/node/node_map.cppsrc/node/node_planner.cpp
)# 生成可执行文件 map_simulation
add_executable(map_simulation ${SOURCES})# 链接线程库
target_link_libraries(map_simulation PRIVATE Threads::Threads)
CMakeLists.txt 文件解析
  1. cmake_minimum_required(VERSION 3.10)

    • 指定了 CMake 的最低版本要求为 3.10。CMake 版本 3.10 或更高版本是必需的才能正确地处理构建。
  2. project(MapManagerProject VERSION 1.0 LANGUAGES CXX)

    • 定义了项目的名称(MapManagerProject)和版本(1.0)。
    • LANGUAGES CXX 表示项目是使用 C++ 编写的。
  3. set(CMAKE_CXX_STANDARD 11)

    • 设置项目使用的 C++ 标准为 C++11。通过 CMAKE_CXX_STANDARD 变量设置 C++ 编译标准为 11。
  4. set(CMAKE_CXX_STANDARD_REQUIRED True)

    • 强制要求 CMake 使用 C++11 标准进行编译。如果 C++11 编译器不可用,CMake 会报错。
  5. find_package(Threads REQUIRED)

    • 查找并配置线程库。这使得 CMake 能够处理多线程支持,并链接适当的线程库。在本项目中,CMake 需要找到线程库来支持多线程编程。
  6. include_directories(include)

    • 指定 include 目录为头文件搜索路径。CMake 会在这个目录下查找项目中的头文件。
  7. set(SOURCES ...)

    • 使用 set() 命令定义了一个名为 SOURCES 的变量,包含所有的源文件路径。项目中的所有源文件(例如 map_manager.cppbfs_planner.cppnode.cpp 等)都被列出并包含在此变量中。
    • 这些源文件都在 src 目录下的子目录中,分别属于 mapplannernode 模块。
  8. add_executable(map_simulation ${SOURCES})

    • 创建一个名为 map_simulation 的可执行文件,使用 SOURCES 变量中列出的所有源文件进行编译。
    • CMake 会将这些源文件编译并链接成一个名为 map_simulation 的可执行文件。
  9. target_link_libraries(map_simulation PRIVATE Threads::Threads)

    • 将线程库 Threads::Threads 链接到 map_simulation 可执行文件。PRIVATE 关键字意味着只有目标文件(即 map_simulation)会链接到线程库,不会将其传播到其他目标。
   总的来说,CMakeLists.txt 文件配置了该项目的构建过程,主要步骤包括:- 设置项目名称、版本和 C++ 编译标准(C++11)。- 查找并链接线程库,以支持多线程。- 指定头文件搜索路径和源文件路径。- 编译源文件并生成一个名为 map_simulation 的可执行文件。- 确保多线程库能够在程序中正确链接。此配置文件能够有效地管理项目的构建过程,确保所有源文件被正确编译并链接,支持多线程操作。

三、具体应用示例的运行演示

可以按照以下步骤在Ubuntu系统中运行该示例项目

步骤 1: 克隆/复制项目文件

将完整的项目文件复制到 Ubuntu 系统上的某个目录。本文中以目录/home/gly/test/bfsmap为例

在这里插入图片描述

步骤 2: 创建构建目录并构建项目

在项目目录下执行以下命令来构建项目:

  1. 进入项目根目录:(具体路径根据个人实际情况修改)

    cd /home/gly/test/bfsmap
    
  2. 创建构建目录
    通常建议在项目目录中创建一个 build 目录来存放构建文件:

    mkdir build
    cd build
    
  3. 运行 CMake 配置项目
    使用 CMake 指定项目的根目录进行配置:

    cmake ..
    
  1. 编译项目
    使用 make 编译项目:

    make
    

    如果一切正常,CMake 会自动生成 Makefile 并使用它来编译源代码,生成可执行文件 map_simulation

步骤 3: 运行项目

编译完成后,可以运行生成的可执行文件。

  1. 运行可执行文件
    ./map_simulation
    

   运行示例如下所示:

请添加图片描述

在这里插入图片描述

bfsmap运行效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/35216.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【STM32】从新建一个工程开始:STM32 新建工程的详细步骤

STM32 开发通常使用 Keil MDK、STM32CubeMX、IAR 等工具来创建和管理工程。此处是 使用 Keil MDK5 STM32CubeMX 创建 STM32 工程的详细步骤。 新建的标准库工程文件已上传至资源中&#xff0c;下载后即可直接使用。 标准库新建 STM32 工程的基本目录结构&#xff1a;STD_STM…

Java 大视界 -- 基于 Java 的大数据实时流处理中的窗口操作与时间语义详解(135)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

Fastdata极数:中国民宿行业发展趋势报告2025

2024年&#xff0c;中国游客出行次数大幅上涨&#xff0c;旅游相关支出也复苏强劲。2025年中国旅游业还将持续稳健的复苏及增长。同时&#xff0c;中国旅游业将见证一场深刻的变革&#xff0c;这场变革的推动力是消费者对旅游期望的转变&#xff0c;经济因素和年轻人全新价值观…

【自定义微信小程序拉下选择过滤组件】searchable-select

【自定义微信小程序拉下选择过滤组件】searchable-select 组件说明 点击输入框获取焦点&#xff0c;输入内容&#xff0c;自动匹配搜索结果&#xff0c;点击搜索结果&#xff0c;自动填充搜索结果。 组件使用 将组件文件夹放在项目中。在需要使用的页面的json文件中&#x…

推理大模型的后训练增强技术-Reasoning模型也进化到2.0了,这次居然学会用工具了

论文题目&#xff1a;START: Self-taught Reasoner with Tools 论文链接&#xff1a;https://arxiv.org/pdf/2503.04625 论文简介 Reasoning模型也进化到2.0了&#xff0c;这次居然学会用工具了&#xff01;✨ 最近有个叫START的方法&#xff0c;让大模型也能学着用工具&#…

Idea集成docker通过ca加密实现镜像打包

​ Idea集成docker实现镜像打包_ideadocker镜像打包-CSDN博客 ​ 之前通过这种方式虽然可以实现idea通过maven打jar包的同时把docker镜像也进行打包&#xff0c;但是这种方式存在很大漏洞&#xff0c;就是服务器的2375端口大开&#xff0c;任何人拿着idea通过这种方式都可以连…

SOC与电压的关系

与电池相关的参数都与SOC有关&#xff0c;也就是电池剩余容量的百分比即荷电状态。 SOC百分之二十时&#xff0c;对应3.2V,SOC80&#xff05;时对应3.3V。

塔能科技:做节能界的“催化剂”,加速工厂能源改造变革

在全球坚定不移地迈向可持续发展的宏大进程中&#xff0c;节能降耗早已从一种发展理念&#xff0c;深度融入到经济社会发展的每一个脉络之中&#xff0c;成为企业在激烈市场竞争中实现降本增效的核心策略&#xff0c;更是推动整个社会朝着绿色、低碳、循环方向转型的关键支撑点…

【算法学习之路】11.并查集

并查集 前言一.简介二.基础并查集三.基础并查集题目12 四.种类并查集&#xff08;扩展域并查集&#xff09;五.种类并查集的题目 前言 我会将一些常用的算法以及对应的题单给写完&#xff0c;形成一套完整的算法体系&#xff0c;以及大量的各个难度的题目&#xff0c;目前算法也…

【微服务】SpringBoot整合LangChain4j 操作AI大模型实战详解

目录 一、前言 二、Langchain4j概述 2.1 Langchain4j 介绍 2.1.1 Langchain4j 是什么 2.1.2 主要特点 2.2 Langchain4j 核心组件介绍 2.3 Langchain4j 核心优势 2.4 Langchain4j 核心应用场景 三、SpringBoot 整合 LangChain4j 组件使用 3.1 前置准备 3.1.1 获取apik…

【图片批量转换合并PDF】多个文件夹的图片以文件夹为单位批量合并成一个PDF,基于wpf的实现方案

项目背景: 多个图片分布在不同文件夹,如何以文件夹为单位批量合并成一个PDF,还要保证文件夹里面图片大小和顺序 实现功能: 1、单张图片的转换PDF:一张图临时转一下 2、多张图片转换成PDF:多张图单独转成PDF 3、多级目录多张图转换成PDF:多级目录多张图单独转成多个PDF…

因果推荐|可解释推荐系统的反事实语言推理

论文&#xff1a;https://arxiv.org/pdf/2503.08051 代码&#xff1a;GitHub - kylokano/CausalX 很新的论文&#xff0c;南大五天前挂到arxiv的&#xff0c;代码基于Recbole&#xff0c;没给全但是提供了足够的验证。 1 动机 可解释推荐不仅提供高质量的推荐&#xff0c;而…

Zabbix安装(保姆级教程)

Zabbix 是一款开源的企业级监控解决方案,能够监控网络的多个参数以及服务器、虚拟机、应用程序、服务、数据库、网站和云的健康状况和完整性。它提供了灵活的通知机制,允许用户为几乎任何事件配置基于电子邮件的告警,从而能够快速响应服务器问题。Zabbix 基于存储的数据提供…

【spring boot 实现图片验证码 前后端】

导入hutool依赖 <!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.36</version>获取验证码接口 Autowiredprivate Captcha captcha;private final static Long VALIDA…

arthas基础命令

文章目录 1. help2. cat3. grep4. pwd5. cls6. session7. reset8. version9. history10. quit11. stop12. keymapArthas 命令行快捷键后台异步命令相关快捷键小结 1. help 作用&#xff1a;查看命令帮助信息 2. cat 作用&#xff1a;打印文件内容&#xff0c;和linux里的cat命…

痉挛性斜颈护理宝典:重拾生活平衡

痉挛性斜颈会给患者的生活带来诸多不便&#xff0c;有效的健康护理对缓解症状、提升生活质量十分关键。 在日常活动方面&#xff0c;患者应保持正确的姿势。站立和坐姿要挺直脊背&#xff0c;避免长时间低头或歪头&#xff0c;减少颈部肌肉的额外负担。睡眠时&#xff0c;选择高…

虚拟定位 1.2.0.2 | 虚拟定位,上班打卡,校园跑步模拟

Fake Location是一款运行于安卓平台上的功能强大、简单实用的虚拟定位软件。它能够帮助用户自定义位置到地图上的任意地方&#xff0c;以ROOT环境运行不易被检测&#xff0c;同时也支持免ROOT运行。提供路线模拟、步频模拟、WIFI模拟等方式&#xff0c;支持反检测。 大小&…

C++基础 [五] - String的模拟实现

目录 前言 string类的模拟实现 成员函数的实现 构造函数 拷贝构造函数 赋值运算符重载 析构函数 元素访问的实现 operator[ ] Iterator - 迭代器 容量大小的实现 size capacity reserve ​编辑resize 内容修改的实现 push_back append operator(char ch) …

嵌入式硬件--开发工具-AD使用常用操作

ad16.1.12 1.如何显示/隐藏其他图层 在pcb界面点击L--试图界面中找到“视图选项”--单层模式选择 not in single layer mode 在pcb界面点击L--试图界面中找到“视图选项”--单层模式选择 gray scale other layers 【Altium】AD如何只显示一层&#xff0c;隐藏其他层显示&…

浏览器好用的去广告插件和暗黑模式护眼插件

提升浏览体验&#xff1a;Edge浏览器的Adblock和Dark Mode扩展 Adblock&#xff1a;告别广告干扰 功能&#xff1a;高效拦截弹窗、横幅和视频广告&#xff0c;提升网页整洁度&#xff0c;加快加载速度&#xff0c;节省流量。安装链接&#xff1a;安装Adblock Dark Mode for E…