1 std::queue 应用于自定义数据结构

通常,std::queue 用于存储基本数据类型,如 int、float、char 等。然而,std::queue 同样可以存储自定义的数据结构,只要这些数据结构满足一定的要求。

(1)存储自定义数据结构的要求

要使自定义数据结构能够存储在 std::queue 中,该数据结构必须满足以下条件:

  • 可复制性:自定义数据结构必须能够被复制。这意味着它必须有一个有效的复制构造函数和一个赋值运算符。当元素被压入队列或从队列中弹出时,会涉及到复制操作。
  • 析构函数:自定义数据结构应该有一个合适的析构函数,以确保在元素从队列中删除时能够正确地释放资源。

(2)使用 std::queue 存储自定义数据结构

下面是一个简单的例子,展示了如何使用 std::queue 存储自定义的数据结构:

#include <iostream>  
#include <queue>  
#include <string>  

// 自定义数据结构  
struct MyData {
	int id;
	std::string name;

	// 构造函数  
	MyData(int id, const std::string& name) : id(id), name(name) {}

	// 输出数据结构的内容  
	void print() const {
		std::cout << "ID: " << id << ", Name: " << name << std::endl;
	}
};

int main()
{
	// 创建一个存储 MyData 类型元素的队列  
	std::queue<MyData> dataqueue;

	// 创建一些 MyData 对象并将其压入队列中  
	dataqueue.push(MyData(1, "Alice"));
	dataqueue.push(MyData(2, "Bob"));
	dataqueue.push(MyData(3, "Charlie"));

	// 遍历队列并输出每个元素的内容  
	while (!dataqueue.empty()) {
		MyData topData = dataqueue.front(); // 获取队列顶元素但不移除它  
		topData.print(); // 输出队列顶元素的内容  
		dataqueue.pop(); // 移除队列顶元素  
	}

	return 0;
}

上面代码的输出为:

ID: 1, Name: Alice
ID: 2, Name: Bob
ID: 3, Name: Charlie

这个例子定义了一个名为 MyData 的自定义数据结构,它包含两个成员变量:id 和 name。然后,创建了一个 std::queue<MyData> 类型的队列,用于存储 MyData 对象。通过调用 push 方法将 MyData 对象压入队列中,并通过循环和 top、pop 方法遍历并输出队列中每个元素的内容。

(3)注意事项

  • 内存管理:当自定义数据结构包含动态分配的内存(如指针、动态数组等)时,需要确保在复制和析构过程中正确地管理这些内存。
  • 性能考虑:对于大型或复杂的自定义数据结构,频繁地复制元素可能会对性能产生显著影响。在这种情况下,可以考虑使用指针或智能指针(如 std::shared_ptr 或 std::unique_ptr)来存储数据结构的引用,而不是直接存储数据结构本身。
  • 异常安全性:在自定义数据结构的复制构造函数和赋值运算符中,应确保操作是异常安全的,以避免在发生异常时导致资源泄漏或其他问题。

2 std::queue 的主要应用场景

下面是 std::queue 的一些主要应用场景:

(1)简单的消息队列:

在分布式系统或微服务架构中,消息队列用于在不同的服务或组件之间传递消息。虽然 std::queue 不适用于这种大规模或分布式场景,但在小型系统或原型中,它可以作为简单的消息队列使用。

(2)任务调度:

在多任务系统中,任务通常需要按照它们到达的顺序执行。std::queue 可以用来存储待执行的任务,并按照它们被添加的顺序来调度。

(3)广度优先搜索(BFS):

在图的遍历算法中,广度优先搜索(BFS)是一种常见的策略。它逐层遍历图的所有节点,直到找到目标或遍历完所有节点。std::queue 是实现BFS的理想选择。

(4)线程安全的数据交换:

在多线程编程中,有时需要在不同的线程之间安全地传递数据。通过使用带有互斥锁的 std::queue,可以实现线程安全的数据交换。

(5)事件处理:

在事件驱动的系统(如GUI或游戏)中,事件(如点击、按键或定时事件)通常按照它们发生的顺序处理。std::queue 可以用来存储待处理的事件。

(6)缓冲区管理:

在数据处理或通信系统中,经常需要临时存储数据,等待进一步处理或传输。std::queue 可以用作这种临时存储的缓冲区。

2.1 std::queue 应用于简单的消息队列

尽管 std::queue 在分布式系统或微服务架构中并不是理想的消息队列解决方案,但在小型系统或原型中,它确实可以作为简单的消息队列使用。下面是一个简单的示例,展示了如何在小型系统中使用 std::queue 来模拟消息队列的行为。

#include <iostream>  
#include <queue>  
#include <mutex>  
#include <condition_variable>  
#include <thread>  
#include <string>  

// 定义消息类型  
struct Message {
	int id;
	std::string content;

	Message(int id, const std::string& content) : id(id), content(content) {}
};

// 简单的消息队列类  
class SimpleMessageQueue {
public:
	// 入队操作  
	void enqueue(const Message& message) {
		std::lock_guard<std::mutex> lock(m_mutex);
		m_queue.push(message);
		m_cond_var.notify_one(); // 通知可能正在等待的消费者线程  
	}

	// 出队操作  
	Message dequeue() {
		std::unique_lock<std::mutex> lock(m_mutex);
		m_cond_var.wait(lock, [this] { return !m_queue.empty(); }); // 等待队列不为空  
		Message message = m_queue.front();
		m_queue.pop();
		return message;
	}

	// 检查队列是否为空  
	bool empty() {
		std::lock_guard<std::mutex> lock(m_mutex);
		return m_queue.empty();
	}

private:
	std::queue<Message> m_queue;
	std::mutex m_mutex;
	std::condition_variable m_cond_var;
};

// 生产者线程函数  
void producer(SimpleMessageQueue& queue) {
	for (int i = 0; i < 6; ++i) {
		Message message(i, "Message " + std::to_string(i));
		queue.enqueue(message);
		std::cout << "Produced message: " << message.content << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作  
	}
}

// 消费者线程函数  
void consumer(SimpleMessageQueue& queue) {
	while (true) {
		if (!queue.empty()) {
			Message message = queue.dequeue();
			std::cout << "Consumed message: " << message.content << std::endl;
		}
		else {
			std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 等待一段时间再检查  
		}
	}
}

int main() 
{
	SimpleMessageQueue messageQueue;

	// 创建生产者线程和消费者线程  
	std::thread producerThread(producer, std::ref(messageQueue));
	std::thread consumerThread(consumer, std::ref(messageQueue));

	// 等待生产者线程完成  
	producerThread.join();

	// 注意:在真实应用中,消费者线程通常不会在这里结束,它可能会一直运行,直到系统关闭。  
	// 在这个示例中,为了简单起见,我们让主线程等待消费者线程运行一段时间后结束。  
	std::this_thread::sleep_for(std::chrono::seconds(10)); // 让消费者线程有足够的时间处理消息  
	consumerThread.detach(); // 在这里分离消费者线程,让它在后台继续运行  

	return 0;
}

上面代码的输出为:

Produced message: Message 0Consumed message: Message 0

Produced message: Message 1
Consumed message: Message 1
Produced message: Message 2
Consumed message: Message 2
Produced message: Message 3
Consumed message: Message 3
Produced message: Message 4
Consumed message: Message 4
Produced message: Message 5
Consumed message: Message 5

上面的代码定义了一个 SimpleMessageQueue 类,它封装了 std::queue 并提供了线程安全的 enqueue 和 dequeue 方法。然后使用 std::mutex 来保护队列免受并发访问的影响,并使用 std::condition_variable 来在队列为空时阻塞消费者线程,并在有新消息时通知它们。

producer 函数模拟了生产者线程的行为,它生成消息并将其放入队列中。consumer 函数模拟了消费者线程的行为,它不断从队列中取出消息并处理它们。

在 main 函数中,创建了生产者线程和消费者线程,并让主线程等待生产者线程完成。注意,在真实的应用程序中,消费者线程通常会一直运行,直到系统关闭。在这个简单的示例中,为了演示目的,在主线程中等待一段时间后让消费者线程在后台继续运行。

2.2 std::queue 应用于任务调度

下面是一个使用 std::queue 来实现简单四则运算表达式求值的示例。这个示例将实现一个基本的计算器,能够处理加、减、乘、除四种运算。为了实现这个计算器,需要使用两个队列:一个用于存储操作数,另一个用于存储操作符。

#include <iostream>  
#include <queue>  
#include <thread>  
#include <functional>  
#include <mutex>  
#include <condition_variable>  

// 任务类型定义  
using Task = std::function<void()>;

// 任务调度器类  
class TaskScheduler {
public:
	TaskScheduler() : m_stop(false) {}

	// 添加任务到队列  
	void addTask(const Task& task) {
		std::lock_guard<std::mutex> lock(m_mutex);
		m_taskQueue.push(task);
		m_condVar.notify_one(); // 通知可能正在等待的工作线程  
	}

	// 从队列中获取并执行任务  
	void work() {
		while (true) {
			std::unique_lock<std::mutex> lock(m_mutex);
			m_condVar.wait(lock, [this] { return !m_taskQueue.empty() || m_stop; });

			if (m_stop) {
				break; // 如果停止标志被设置,退出循环  
			}

			Task task = std::move(m_taskQueue.front());
			m_taskQueue.pop();

			lock.unlock(); // 释放锁,以便其他线程可以添加任务  

			// 执行任务  
			task();
		}
	}

	// 停止任务调度器  
	void stop() {
		std::lock_guard<std::mutex> lock(m_mutex);
		m_stop = true;
		m_condVar.notify_all(); // 通知所有等待的线程  
	}

private:
	std::queue<Task> m_taskQueue;
	std::mutex m_mutex;
	std::condition_variable m_condVar;
	bool m_stop;
};

// 示例任务函数  
void printTask(int id) {
	std::cout << "Executing task " << id << std::endl;
}

int main() 
{
	TaskScheduler scheduler;

	// 创建多个任务并添加到任务队列中  
	for (int i = 0; i < 10; ++i) {
		int taskId = i;
		Task task = [taskId]() { printTask(taskId); };
		scheduler.addTask(task);
	}

	// 启动工作线程来执行任务  
	std::thread workerThread([&scheduler]() { scheduler.work(); });

	// 让主线程等待一段时间,以便工作线程可以执行任务  
	std::this_thread::sleep_for(std::chrono::seconds(5));

	// 停止任务调度器和工作线程  
	scheduler.stop();
	workerThread.join();

	return 0;
}

上面代码的输出为:

Executing task 0
Executing task 1
Executing task 2
Executing task 3
Executing task 4
Executing task 5
Executing task 6
Executing task 7
Executing task 8
Executing task 9

这个示例定义了一个 TaskScheduler 类,它包含一个 std::queue<Task> 来存储待执行的任务。addTask 方法用于将任务添加到队列中,并通知可能正在等待的工作线程。work 方法在一个单独的线程中运行,不断从队列中取出任务并执行它们。当 stop 方法被调用时,工作线程会退出循环并停止执行。

在 main 函数中,创建了一个 TaskScheduler 对象,并添加了多个任务到队列中。然后,启动了一个工作线程来执行这些任务。主线程等待一段时间以允许工作线程执行任务,然后调用 stop 方法来停止任务调度器和工作线程。

2.3 std::queue 应用于广度优先搜索(BFS)

在广度优先搜索(BFS)中,std::queue 是一个理想的数据结构,用于存储待访问的节点。以下是一个使用 std::queue 实现 BFS 的示例,它遍历一个无向图并打印出节点的访问顺序。

#include <iostream>  
#include <vector>  
#include <queue>  
#include <unordered_set>  

using namespace std;

// 定义图的数据结构,使用邻接列表表示  
vector<vector<int>> graph;

// BFS 函数  
void bfs(int start) {
	queue<int> q;
	unordered_set<int> visited;
	q.push(start); // 将起始节点加入队列  
	visited.insert(start); // 标记起始节点为已访问  

	while (!q.empty()) {
		int current = q.front(); // 取出队列中的第一个节点  
		q.pop(); // 从队列中移除该节点  
		cout << current << " "; // 访问该节点  

		// 遍历当前节点的所有邻居  
		for (int neighbor : graph[current]) {
			if (visited.find(neighbor) == visited.end()) {
				q.push(neighbor); // 将未访问的邻居加入队列  
				visited.insert(neighbor); // 标记邻居为已访问  
			}
		}
	}
}

int main() 
{
	// 初始化图,假设有 5 个节点  
	int numNodes = 5;
	graph.resize(numNodes);

	// 添加边:0-1, 0-2, 1-2, 2-0, 2-3, 3-3  
	graph[0].push_back(1);
	graph[0].push_back(2);
	graph[1].push_back(2);
	graph[2].push_back(0);
	graph[2].push_back(3);
	graph[3].push_back(3); // 添加自环  

	// 从节点 0 开始 BFS  
	cout << "BFS traversal starting from node 0: ";
	bfs(0);
	cout << endl;

	return 0;
}

上面代码的输出为:

BFS traversal starting from node 0: 0 1 2 3

这个示例首先创建了一个 graph 变量来表示图,然后使用 bfs 函数进行广度优先搜索。bfs 函数接收一个起始节点作为参数,并使用 std::queue<int> 来存储待访问的节点。同时,使用 std::unordered_set<int> 来跟踪已经访问过的节点,以避免重复访问。

在 bfs 函数中,首先将起始节点加入队列并标记为已访问。然后,进入一个循环,在该循环中,不断从队列中取出节点,访问它,并将其所有未访问的邻居加入队列。这个过程一直持续到队列为空,即所有可达的节点都被访问为止。

在 main 函数中,初始化了一个包含 5 个节点的图,并添加了一些边。然后,从节点 0 开始执行 BFS,并打印出节点的访问顺序。

3 实现一个简单的 std::queue 容器

下面是一个简单的 Myqueue 类的实现,它基于 std::list 作为底层容器。

#include <iostream>  
#include <list>  
#include <string> 
#include <stdexcept>  

template <typename T>
class SimpleQueue {
public:
	// 判断队列是否为空  
	bool empty() const {
		return elements.empty();
	}

	// 返回队列中元素的数量  
	size_t size() const {
		return elements.size();
	}

	// 在队列尾部插入元素  
	void push(const T& value) {
		elements.push_back(value);
	}

	// 从队列头部移除元素并返回它  
	T pop() {
		if (elements.empty()) {
			throw std::out_of_range("Queue is empty, cannot pop element.");
		}
		T frontElement = elements.front();
		elements.pop_front();
		return frontElement;
	}

	// 返回队列头部的元素(不移除)  
	T& front() {
		if (elements.empty()) {
			throw std::out_of_range("Queue is empty, cannot access front element.");
		}
		return elements.front();
	}

	// 返回队列头部的元素(不移除)的常量版本  
	const T& front() const {
		if (elements.empty()) {
			throw std::out_of_range("Queue is empty, cannot access front element.");
		}
		return elements.front();
	}

	// 返回队列尾部的元素(不移除)  
	T& back() {
		if (elements.empty()) {
			throw std::out_of_range("Queue is empty, cannot access back element.");
		}
		return elements.back();
	}

	// 返回队列尾部的元素(不移除)的常量版本  
	const T& back() const {
		if (elements.empty()) {
			throw std::out_of_range("Queue is empty, cannot access back element.");
		}
		return elements.back();
	}

private:
	std::list<T> elements;
};

int main() 
{
	SimpleQueue<int> queue;

	// 添加元素到队列  
	queue.push(1);
	queue.push(2);
	queue.push(3);

	// 访问队列头部元素  
	std::cout << "Front element: " << queue.front() << std::endl;

	// 移除并返回队列头部元素  
	std::cout << "Popped element: " << queue.pop() << std::endl;

	// 访问队列新的头部元素  
	std::cout << "New front element: " << queue.front() << std::endl;

	// 检查队列是否为空  
	std::cout << "Is queue empty? " << (queue.empty() ? "Yes" : "No") << std::endl;

	return 0;
}

上面代码的输出为:

Front element: 1
Popped element: 1
New front element: 2
Is queue empty? No

这个简单的 SimpleQueue 实现使用了 std::list 作为底层容器来存储元素。push 方法在列表的尾部添加元素,pop 方法从列表的头部移除元素,front 和 back 方法分别返回队列的头部和尾部元素。如果队列为空,这些方法会抛出 std::out_of_range 异常。

03-21 05:37