C++中的单例模式及具体应用示例
AI 摘要
本文深入探讨了C++中的单例模式及其在机器人自主导航中的应用,特别是如何通过单例模式来管理地图数据。文章详细介绍了单例模式的基本结构、优缺点以及在多线程环境中的应用,强调了其在保证数据一致性和资源管理中的重要性。
接着,文章通过一个实际的路径规划系统示例,展示了如何结合单例模式设计地图管理模块。该系统使用了广度优先搜索(BFS)算法来进行路径规划,并通过多线程实现地图更新和路径规划的并行处理。主要的代码模块包括地图管理类
MapManager
、路径规划类BFSPlanner
、以及ProjectNode
管理多线程操作。通过 CMake
构建系统,用户可以方便地配置和编译该项目。文章还提供了具体的实现代码,涵盖了地图更新、路径规划、C++单例模式的使用,以及多线程编程的技巧。通过这种方式,读者不仅能掌握 C++
单例模式的实现,还能学到如何将其应用于实际的机器人导航与路径规划系统中。最后,文章提供了如何在 Ubuntu 系统中构建和运行该示例项目的详细步骤,包括项目的编译、执行以及展示了系统的运行效果。
C++中的单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。单例模式在很多情况下非常有用,例如在管理全局配置、日志系统、数据库连接等场景中。
一、C++单例模式的简单介绍
–
1、单例模式的结构
单例模式通常包含以下几个核心部分:
- 私有构造函数:构造函数私有化,防止外部创建实例。
- 静态私有实例:提供一个静态成员函数来创建或返回类的唯一实例。
- 公共静态方法:提供全局访问点,通常命名为
getInstance()
,用于获取单例对象。 - 删除复制构造函数和赋值运算符:防止复制或赋值,以确保只有一个实例存在。
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
文件夹
-
bfs_planner.h
- 该文件声明了
BFSPlanner
类,负责实现基于广度优先搜索(BFS)算法的路径规划。主要功能包括计算路径、打印路径、以及显示带路径和障碍物的地图。
- 该文件声明了
-
map_manager.h
- 该文件声明了
MapManager
类,用于管理地图数据。它提供获取当前地图、更新地图、打印地图及获取地图尺寸的功能,并通过互斥锁保证线程安全。
- 该文件声明了
-
node.h
- 该文件声明了
ProjectNode
类,负责管理项目的核心功能。它启动了两个线程:一个线程负责地图的更新,另一个线程负责路径规划。
- 该文件声明了
src
文件夹
map
文件夹
map_manager.cpp
- 该文件实现了
MapManager
类的方法。包括地图数据的随机更新(生成障碍物)、打印地图以及获取地图尺寸。通过线程安全的方式管理地图的更新和访问。
- 该文件实现了
node
文件夹
-
node_map.cpp
- 该文件实现了与地图更新相关的线程。它模拟地图的随机更新,并定期打印更新后的地图。
-
node_planner.cpp
- 该文件实现了路径规划的线程。它从
MapManager
获取地图数据,使用BFSPlanner
进行路径计算,并打印计算结果。它循环执行路径规划,每两秒进行一次更新。
- 该文件实现了路径规划的线程。它从
-
node.cpp
- 该文件实现了
ProjectNode
类的构造和析构方法。它初始化了BFSPlanner
实例并启动了地图更新线程和路径规划线程。
- 该文件实现了
planner
文件夹
bfs_planner.cpp
- 该文件实现了
BFSPlanner
类的路径规划功能。通过广度优先搜索算法计算从起点到终点的路径,并提供路径打印和带路径显示的地图输出。
- 该文件实现了
其他
-
CMakeLists.txt
- 该文件是 CMake 构建系统的配置文件,定义了项目的构建规则。它指定了项目名称、C++标准版本、使用的线程库等,并列出了所有源文件以生成可执行文件。
总的来说,这个应用示例项目实现了一个基于 BFS 算法的路径规划系统。它通过多个模块协同工作,MapManager
模拟并更新地图,BFSPlanner
负责路径规划,而 ProjectNode
类管理整个流程,包括启动地图更新和路径规划的线程。整个系统通过多线程来模拟地图更新并实时计算路径,展示路径规划的过程。
2、应用示例——主节点
node.h
和 node.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_ptr
namespace Project {
class ProjectNode {
public:
ProjectNode();
~ProjectNode();
// Threads
void 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 Project
int main()
{
Project::ProjectNode Node;
std::cout << "Starting Map Manager Simulation..." << std::endl;
return 0;
}
node.h
node.h
文件定义了 ProjectNode
类的接口,主要功能是启动和管理两个线程:一个用于更新地图,另一个用于执行路径规划。
主要功能:
-
ProjectNode
构造函数:- 构造函数初始化了
bfsplanner_
作为BFSPlanner
类的共享指针。BFSPlanner
是用于路径规划的核心类。 - 同时,构造函数启动了两个线程:
map_thread_
用于地图更新,planner_thread_
用于路径规划。
- 构造函数初始化了
-
ProjectNode
析构函数:- 析构函数确保两个线程在对象销毁前都能正确地完成任务。通过调用
join()
方法等待线程结束,保证线程安全退出。
- 析构函数确保两个线程在对象销毁前都能正确地完成任务。通过调用
-
mapThread
和plannerThread
:- 这两个函数定义了每个线程的行为,但在
node.cpp
中具体实现。
- 这两个函数定义了每个线程的行为,但在
-
成员变量:
bfsplanner_
:一个std::shared_ptr<BFSPlanner>
,用于存储BFSPlanner
类的实例,这个实例负责路径规划。map_thread_
和planner_thread_
:两个std::thread
类型的成员变量,分别表示用于地图更新和路径规划的线程。
node.cpp
node.cpp
文件实现了 ProjectNode
类的构造函数和析构函数,并创建了两个线程来分别执行地图更新和路径规划任务。
主要实现:
-
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()
方法。
-
ProjectNode
析构函数:- 在析构函数中,调用
map_thread_.join()
和planner_thread_.join()
等待两个线程完成任务后再退出。这保证了在ProjectNode
对象销毁之前,所有线程的执行会正确结束。
- 在析构函数中,调用
-
main
函数:main()
函数创建了一个ProjectNode
对象并启动整个模拟过程。输出 “Starting Map Manager Simulation…” 用于标识模拟开始。
mapThread
和 plannerThread
虽然这两个线程函数在 node.h
中声明了,分别在 node_map.cpp
和 node_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.h
和 map_manager.cpp
是该项目中用于管理地图的文件。下面将详细介绍这两个文件中的程序。
map_manager.h
map_manager.h
是 MapManager
类的头文件,声明了该类的接口。
主要功能:
-
getInstance
:- 这是一个静态方法,用于获取
MapManager
类的唯一实例。它通过静态局部变量实现单例模式,确保在整个程序运行过程中只有一个MapManager
实例。
- 这是一个静态方法,用于获取
-
getMap
:- 返回当前的地图数据,类型为
std::vector<int>
,它表示一个一维数组来存储地图的信息。这个方法使用了互斥锁 (std::mutex
) 来确保在多线程环境中线程安全。
- 返回当前的地图数据,类型为
-
updateMapRandom
:- 模拟地图的更新,随机生成障碍物。更新过程通过使用
std::random_device
和std::uniform_int_distribution
随机决定地图上的每个位置是障碍物还是通路。该方法也使用互斥锁确保线程安全。
- 模拟地图的更新,随机生成障碍物。更新过程通过使用
-
printMap
:- 打印地图数据。该方法根据
current_map_
的值打印地图,使用 ANSI 转义序列来着色输出。障碍物用红色 (X
) 表示,通行区域用绿色 (.
) 表示。
- 打印地图数据。该方法根据
-
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.cpp
是 MapManager
类的实现文件,定义了该类的方法。
主要实现:
-
getInstance
:- 使用静态局部变量
instance
实现单例模式,这样即使在多线程环境下,MapManager
类的实例也能确保唯一性和线程安全。
- 使用静态局部变量
-
MapManager
构造函数:- 构造函数初始化地图尺寸为 10x10,并将
current_map_
初始化为全 0,表示一个没有障碍物的地图。current_map_
被扩展为std::vector<int>
,其大小为MAP_WIDTH_ * MAP_HEIGHT_
。
- 构造函数初始化地图尺寸为 10x10,并将
-
getMap
:- 使用
std::lock_guard<std::mutex>
进行线程安全操作,确保对地图数据的访问不会受到其他线程干扰。返回当前地图的数据。
- 使用
-
updateMapRandom
:- 随机生成一个地图,模拟障碍物的更新。每个地图单元的值要么是 0(表示通行),要么是 1(表示障碍物)。通过使用
std::random_device
和std::mt19937
随机生成值,70% 的概率是通行区域(0),30% 的概率是障碍物(1)。每次更新后,打印 “Map updated!”。
- 随机生成一个地图,模拟障碍物的更新。每个地图单元的值要么是 0(表示通行),要么是 1(表示障碍物)。通过使用
-
printMap
:- 打印地图到控制台。使用 ANSI 转义序列打印不同颜色的字符:
X
表示障碍物,红色显示。.
表示通行区域,绿色显示。
- 打印时每一行表示地图的一行,使用
MAP_WIDTH_
和MAP_HEIGHT_
计算并显示二维地图。
- 打印地图到控制台。使用 ANSI 转义序列打印不同颜色的字符:
线程安全
所有访问和修改地图的操作都使用 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
方法
mapThread
是 ProjectNode
类中的一个线程执行函数,功能是周期性地更新地图并打印更新后的地图。
功能与实现:
-
循环执行:
while (true)
使得该线程不断地执行,形成一个无限循环,直到线程被外部停止。这个线程每隔一段时间(5秒)执行一次地图更新。
-
地图更新:
MapManager::getInstance().updateMapRandom()
:调用MapManager
类的updateMapRandom
方法来随机更新地图中的障碍物。此方法模拟了动态的环境,其中地图上的障碍物会在每个周期内随机变化。
-
地图打印:
MapManager::getInstance().printMap()
:在更新地图后,调用printMap
方法打印当前地图的状态。地图会被打印在控制台上,其中障碍物使用红色(X
)表示,通行区域使用绿色(.
)表示。
-
线程休眠:
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
方法,该方法用于路径规划。它主要完成了以下几个步骤:
功能与实现:
-
获取当前地图:
auto flat_map = MapManager::getInstance().getMap();
- 从
MapManager
中获取当前地图数据。flat_map
是一个一维的整数数组,表示地图的各个格子(0 表示通行区域,1 表示障碍物)。
-
获取地图尺寸:
std::pair<int, int> mapsize = MapManager::getInstance().getMapSize();
- 获取地图的宽度和高度,分别用于后续二维数组的创建。
-
将一维地图转换为二维地图:
- 创建一个二维数组
grid
,大小为mapsize.first
行和mapsize.second
列。 - 通过循环将一维的
flat_map
转换为二维的grid
,每个元素表示地图中的一个格子。
- 创建一个二维数组
-
调用路径规划:
auto path = bfsplanner_->planPath(grid, {0, 0}, {mapsize.second - 1, mapsize.first - 1});
- 使用
bfsplanner_
(BFSPlanner
类的实例)调用planPath
方法进行路径规划。规划的起点是(0, 0)
,终点是地图的右下角(mapsize.second - 1, mapsize.first - 1)
。
-
打印路径规划结果:
- 如果找到路径,打印成功消息,并调用
bfsplanner_->printMapWithPath
打印带路径的地图。 - 如果没有找到路径,打印失败消息。
- 如果找到路径,打印成功消息,并调用
-
线程休眠:
std::this_thread::sleep_for(std::chrono::seconds(2));
- 每次计算路径后,线程会休眠 2 秒,再进行下一次路径规划。这模拟了一个实时的路径规划过程。
bfs_planner.cpp
bfs_planner.cpp
文件实现了 BFSPlanner
类,它负责执行广度优先搜索(BFS)来计算路径,并打印包含路径的地图。
功能与实现:
-
isValid
方法:- 用于判断某个坐标是否在地图范围内并且该位置是否为通行区域(值为 0)。
- 如果坐标有效且地图上该位置是通行的,返回
true
,否则返回false
。
-
printPath
方法:- 打印路径中的每个坐标点。
- 每个路径点格式为
(x, y)
,路径中的所有点将被打印出来,形成一条路径。
-
printMapWithPath
方法:- 打印包含路径和障碍物的地图。方法通过将障碍物标记为
'X'
、路径点标记为'.'
,起点标记为'S'
,终点标记为'G'
来显示地图。 - 使用颜色输出(通过 ANSI 转义序列),红色表示障碍物,绿色表示路径,黄色表示起点,青色表示终点。
- 打印包含路径和障碍物的地图。方法通过将障碍物标记为
-
planPath
方法:- 执行广度优先搜索(BFS)来寻找从起点到终点的路径。
- 使用队列
q
存储待处理的坐标及其路径。每个元素是一个包含坐标和路径的二元组。 - 从起点开始,探索四个方向(上、下、左、右),将有效的且未被访问过的坐标加入队列。
- 如果找到目标点,则返回从起点到目标点的路径。
- 如果队列为空且未找到目标点,则表示没有路径可达,返回空路径。
BFS 算法细节:
- 队列存储路径: 广度优先搜索使用队列来确保按层次逐步扩展路径。每次扩展一个新的节点时,都会将当前路径加入队列继续搜索。
- 路径返回: 一旦找到目标点,当前路径被返回。
- 路径输出: 找到的路径通过
printPath
和printMapWithPath
方法显示在控制台上。
总的来说, plannerThread
方法通过调用 BFSPlanner
的 planPath
方法实现路径规划,定期计算从起点到终点的路径。每次路径规划之后,打印路径规划结果,并每 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(SOURCES
src/map/map_manager.cpp
src/planner/bfs_planner.cpp
src/node/node.cpp
src/node/node_map.cpp
src/node/node_planner.cpp
)
# 生成可执行文件 map_simulation
add_executable(map_simulation ${SOURCES})
# 链接线程库
target_link_libraries(map_simulation PRIVATE Threads::Threads)
CMakeLists.txt 文件解析
-
cmake_minimum_required(VERSION 3.10)
- 指定了 CMake 的最低版本要求为 3.10。CMake 版本 3.10 或更高版本是必需的才能正确地处理构建。
-
project(MapManagerProject VERSION 1.0 LANGUAGES CXX)
- 定义了项目的名称(
MapManagerProject
)和版本(1.0)。 LANGUAGES CXX
表示项目是使用 C++ 编写的。
- 定义了项目的名称(
-
set(CMAKE_CXX_STANDARD 11)
- 设置项目使用的 C++ 标准为 C++11。通过
CMAKE_CXX_STANDARD
变量设置 C++ 编译标准为 11。
- 设置项目使用的 C++ 标准为 C++11。通过
-
set(CMAKE_CXX_STANDARD_REQUIRED True)
- 强制要求 CMake 使用 C++11 标准进行编译。如果 C++11 编译器不可用,CMake 会报错。
-
find_package(Threads REQUIRED)
- 查找并配置线程库。这使得 CMake 能够处理多线程支持,并链接适当的线程库。在本项目中,CMake 需要找到线程库来支持多线程编程。
-
include_directories(include)
- 指定
include
目录为头文件搜索路径。CMake 会在这个目录下查找项目中的头文件。
- 指定
-
set(SOURCES ...)
- 使用
set()
命令定义了一个名为SOURCES
的变量,包含所有的源文件路径。项目中的所有源文件(例如map_manager.cpp
、bfs_planner.cpp
、node.cpp
等)都被列出并包含在此变量中。 - 这些源文件都在
src
目录下的子目录中,分别属于map
、planner
和node
模块。
- 使用
-
add_executable(map_simulation ${SOURCES})
- 创建一个名为
map_simulation
的可执行文件,使用SOURCES
变量中列出的所有源文件进行编译。 - CMake 会将这些源文件编译并链接成一个名为
map_simulation
的可执行文件。
- 创建一个名为
-
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: 创建构建目录并构建项目
在项目目录下执行以下命令来构建项目:
-
进入项目根目录:(具体路径根据个人实际情况修改)
cd /home/gly/test/bfsmap
-
创建构建目录:
通常建议在项目目录中创建一个build
目录来存放构建文件:mkdir build cd build
-
运行 CMake 配置项目:
使用 CMake 指定项目的根目录进行配置:cmake ..

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

步骤 3: 运行项目
编译完成后,可以运行生成的可执行文件。
- 运行可执行文件:
./map_simulation
运行示例如下所示:
bfsmap运行效果