文章目錄
- 前言
- 1. 源碼 forward_list.hpp
- 2. 使用示例
前言
用 C++ 創建了一個單向鏈表,用于練習使用現代 C++ 的特性,包括 3 點:
- 對于容器,使用 std::initializer_list 作為參數創建構造函數。
C++ Core Guidelines 中,推薦使用花括號 {} 初始化對象,并且對容器推薦使用 std::initializer_list 創建構造函數。 - 使用智能指針 unique_ptr 。
- 使用了 copy and move (即 copy and swap 慣用法的改進版)。
關于 copy and move ,可參見我的另一篇文章
《C++ 的 copy and swap 慣用法》https://blog.csdn.net/drin201312/article/details/149204977
1. 源碼 forward_list.hpp
/*
自定義單向鏈表。包含功能:
1. 創建鏈表。
2. 頭部插入元素,任意位置的前向插入。
3. 除最后一個元素之外,任意位置的后向插入。
4. 遍歷鏈表并顯示。
5. 根據輸入位置,刪除元素。
6. 根據輸入的數值,刪除鏈表中所有相同的元素。
7. 復制鏈表。
8. 清空鏈表。
*/#ifndef FORWARD_LIST_H_
#define FORWARD_LIST_H_
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <iterator>
#include <memory>
#include <stdexcept>
#include <string>
#include <stack>
#include <utility>// 鏈表的節點。
template<typename T>
class Node {public:Node() = default;// 常函數如果返回引用,則必須返回 const 引用。const T& get_data() const { // 返回節點內的數據。return data_; }// 給節點輸入數據。void set_data(const T& data) {data_ = data;}// 返回下一個節點地址的 reference。std::unique_ptr<Node<T>>& get_next_ptr() {return ptr_next_;}// 奪取下一個節點的 unique_ptrvoid set_next_ptr(std::unique_ptr<Node<T>>&& next_node_ptr) { ptr_next_ = std::move(next_node_ptr); }private:T data_{}; // 節點內的數據。整數會默認初始化為 0 。std::unique_ptr<Node<T>> ptr_next_; // 下一個節點的地址。
};template<typename T> // 必須設置 T,用作 Node 的類型。
class ForwardList {public: // 該部分必須考慮如何處理 7 個函數:4 個構造函數,2 個賦值操作符,1 個析構函數。ForwardList() = default; // 允許使用無括號 ForwardList foo 的形式創建對象。// 用 initializer_list 接收任意數量的參數。ForwardList(std::initializer_list<T> args) {std::cout << "Default initializer_list called.\n";// for (auto each : args | std::views::reverse) { // C++ 20 使用此方式逆序。// std::cout << "each= " << each << "\n";// push_front(each);// }// C++ 14 使用下面 std::rbegin 的方式逆序。for (auto it = std::rbegin(args); it != std::rend(args); ++it) {push_front(*it);}}// 拷貝構造函數,必須手動把鏈表復制過來。ForwardList(const ForwardList& other) { std::cout << "Copy constructor called.\n";if (other.nodes_quantity_ != 0) {std::stack<T> data_record; // 記錄 other 中的所有 dataNode<T>* running_node_ptr{other.front_node_ptr_.get()};for (size_t index = 0; index < other.nodes_quantity_; ++index) {data_record.emplace(running_node_ptr->get_data());running_node_ptr = running_node_ptr->get_next_ptr().get();}while (!data_record.empty()) { // 根據記錄的 data 新建整個鏈表。push_front(data_record.top());data_record.pop();}}}// 下面使用 copy and move 方法,把兩個賦值運算符函數合二為一。// 轉移 other 的資源,并且 other 應該是用 copy 或 move 新建的一個對象。void move_resource(ForwardList&& other) noexcept {std::cout << "move_resource called.\n";front_node_ptr_ = std::move(other.front_node_ptr_);nodes_quantity_ = other.nodes_quantity_;// 清理 other 中的資源。對于內置的數值類型, std::move 并不會將其歸零,必須手動修改。other.nodes_quantity_ = 0;}// 移動構造函數,把鏈表資源轉移到當前對象。ForwardList(ForwardList&& other) noexcept {std::cout << "Move constructor called.\n";// 初始化列表不能完成所有的轉移工作,所以直接調用函數,不使用初始化列表。move_resource(std::move(other)); };// 賦值運算符,把拷貝賦值運算符和移動賦值運算符進行二合一,并且能避免自賦值問題。ForwardList& operator=(ForwardList other) { // other 必須用值傳遞。std::cout << "operator= called.\n";move_resource(std::move(other)); return *this;}~ForwardList() {clear(); }// 頭部插入一個 node。void push_front(const T& data) { std::cout << "@push_front" << "\tdata= " << data << "\n";auto new_node_ptr = create_node(data); // 這里會進行 NRVO 。new_node_ptr->set_next_ptr(std::move(front_node_ptr_));// 每個新節點,都會成為首節點。front_node_ptr_ = std::move(new_node_ptr);}// 在指定 node 位置的后面再插入一個 node 。void insert_after(const size_t node_index, const T& data) { std::cout << "@insert_after, node_index= " << node_index << "\n";check_index(node_index); if (nodes_quantity_ == 0) {push_front(data);} else {auto new_node_ptr = create_node(data); // 這里會進行 NRVO 。Node<T>& current_node = goto_node(node_index);// 如果返回值是一個 reference,則該函數返回結果屬于左值。// 下面兩個輸入參數都是左值,需用 std::move 轉換為右值引用。new_node_ptr->set_next_ptr(std::move(current_node.get_next_ptr())); current_node.set_next_ptr(std::move(new_node_ptr));std::cout << "current_node.get_data()= " << current_node.get_data() << "\n";}}// 在指定 node 的位置,前面再插入一個 node 。void insert_before(size_t node_index, const T& data) { std::cout << "@insert_before, node_index= " << node_index << "\n";if (node_index == 0) {push_front(data);} else { // 第 n 個位置向前插入一個節點,等于第 n-1 位置向后插入一個節點。insert_after(node_index - 1, data);}}void show_list() const { // 顯示整個鏈表。std::cout << "\nshowing the whole forward list: \n";if (nodes_quantity_ == 0) {std::cout << "Empty forward list.\n";} else {// 用到原始指針時,盡量縮小范圍。因為只限于這個函數內部,生命周期極短,不會發生內存泄漏。Node<T>* running_node_ptr = front_node_ptr_.get();for (size_t i = 0; i < nodes_quantity_; ++i) {std::cout << "node[" << i << "].data= " << running_node_ptr->get_data() << "\n";running_node_ptr = running_node_ptr->get_next_ptr().get();}}}// 根據輸入的位置 index ,刪除元素。void delete_by_index(size_t node_index) {std::cout << "@delete_by_index, node_index= " << node_index << "\n";check_index(node_index); if (nodes_quantity_ != 0) { // 避開空鏈表。if (node_index == 0) {front_node_ptr_ = std::move(front_node_ptr_->get_next_ptr());} else { // 把前一個節點鏈接到后一個節點即可。Node<T>& previous_node = goto_node(node_index - 1);previous_node.set_next_ptr(std::move(previous_node.get_next_ptr()->get_next_ptr()));}--nodes_quantity_;}}// 根據輸入的值,刪除鏈表中所有具有相同值的 node 。void delete_by_value(const T& value) {std::cout << "@delete_by_value, value= " << value << "\n";std::stack<size_t> index_record; // 使用一個后進先出的容器來記錄 index 。Node<T>* running_node_ptr = front_node_ptr_.get();for (size_t index = 0; index < nodes_quantity_; ++index) {if (running_node_ptr->get_data() == value) {index_record.emplace(index); // 記錄當前 index。}running_node_ptr = running_node_ptr->get_next_ptr().get();}std::cout << "Found " << index_record.size() << " of " << value << "\n";while (!index_record.empty()) {size_t index = index_record.top(); // 先刪除大的索引,再刪除小的索引。index_record.pop(); // 必須同時刪除對應的 index 。delete_by_index(index);}}// 返回鏈表的大小。size_t size() const {return nodes_quantity_;}// 清除鏈表數據。void clear() {std::cout << "@clear, clearing the whole forward list.\n";while (front_node_ptr_) { // unique_ptr 只要指向新的資源,原有資源就被釋放。front_node_ptr_ = std::move(front_node_ptr_->get_next_ptr());}nodes_quantity_ = 0;}private:// 鏈表狀態有變化時,下面兩個成員變量,必須同步更新。size_t nodes_quantity_ = 0;// 鏈表為動態大小,因此要把數據放在堆區。std::unique_ptr<Node<T>> front_node_ptr_;// 移動到指定的節點位置,并且返回該節點。從 0 開始計算 node_index 。// node_index 可以是 0 ,也可以是最后一個 node 。Node<T>& goto_node(size_t node_index) { if (size() == 0) { // 處理空鏈表的情況。throw std::range_error("The forward list is empty!");}// 使用 get 返回的原始指針。因為原始指針只存活在這個函數內,生命周期極短,不會造成內存泄漏。check_index(node_index);Node<T>* running_node_ptr_ = front_node_ptr_.get();for (size_t i = 0; i < node_index; ++i) {// 獲取下一個節點的原始指針。running_node_ptr_ = (running_node_ptr_->get_next_ptr()).get();}return *running_node_ptr_; // 只能返回堆區對象,不可返回原始指針,避免對象的生命周期管理混亂。}// 使用輸入的數據,創建一個新的 node 。std::unique_ptr<Node<T>> create_node(const T& data){auto new_node_ptr{std::make_unique<Node<T>>()};new_node_ptr->set_data(data);++nodes_quantity_; return new_node_ptr; // 返回局部變量,編譯器進行 NRVO 。}// 不允許索引大于節點數量。void check_index (const size_t node_index) const{ // 始終允許 node_index 為 0 ,無須判斷節點數量。if ((node_index != 0) && (node_index >= nodes_quantity_)) {throw std::range_error("node_index + 1 > nodes_quantity_ \nnode_index= " + std::to_string(node_index) + " , nodes_quantity_= " + std::to_string(nodes_quantity_));}}
};
#endif // FORWARD_LIST_H_
2. 使用示例
#include "forward_list.hpp"
#include <cctype>
#include <iostream>void insert_and_show() {ForwardList<int> foo;foo.push_front(2025);ForwardList<int> list{2025, 888};list.insert_before(0, 200);list.insert_before(0, 200);list.insert_after(3, 300); list.insert_after(3, 300); list.show_list();list.show_list();
}void delete_and_show() {ForwardList<int> list{100, 2025, 888, 2025};// list.delete_by_index(1);list.delete_by_index(0);list.show_list();list.delete_by_value(200);list.show_list();list.delete_by_value(2025);list.show_list();
}void copy_and_move() {ForwardList<int> list{2025, 888};ForwardList<int> foo;std::cout << "\nfoo = list\n";foo = list; // 調用拷貝賦值運算符。std::cout << "\nfoo.show_list()= ";foo.show_list();ForwardList<int> bar;std::cout << "\nbar = std::move(list)\n";bar = std::move(list); // 調用移動賦值運算符。std::cout << "\nbar.show_list()= ";bar.show_list();// 再查看原有鏈表。std::cout << "\nlist.show_list()= ";list.show_list();
}
void clear_and_show() {ForwardList<int> foo{2025, 888};std::cout << "\nfoo.show_list()= ";foo.show_list();foo.clear();std::cout << "\nfoo.show_list()= ";foo.show_list();
}int main() {insert_and_show();// delete_and_show(); // copy_and_move(); // clear_and_show(); return 0;
}
調用 insert_and_show 的運行結果如下圖:
—————————— 本文結束 ——————————