簡體   English   中英

C++ 使用智能指針的鏈表

[英]C++ Linked list using smart pointers

我只對帶有模板的鏈表使用原始指針。 比如成員數據, Node<T>* head; 當我插入一個節點時,其中一行是head = new Node<T>(data); .

但是,現在我需要使用智能指針,但我不確定如何將其更改為使用智能指針。 是否將成員數據更改為shared_ptr<Node<T>> head; 另一行將更改為
head = shared_ptr<Node<T>>( new <Node<T>>(data) ); ?

您“不需要”為鏈表使用智能指針,因為該語句沒有意義。 為低級數據結構使用智能指針。 您將智能指針用於高級程序邏輯。

就低級數據結構而言,您可以使用 C++ 標准庫中的標准容器類,例如std::list [*] ,它可以解決所有內存管理問題,而無需在內部使用任何智能指針。

如果您真的需要自己的高度專業化/優化的自定義容器類,因為整個 C++ 標准庫不適合您的要求,並且您需要替換std::liststd::vectorstd::unordered_map和其他優化的、經過測試的,有文件證明的安全容器——我非常懷疑! –,那么無論如何你都必須手動管理內存,因為這樣一個專門類的重點幾乎肯定是需要諸如內存池、寫時復制甚至垃圾收集之類的技術,所有這些都與典型的智能指針的沖突比較簡單的刪除邏輯。

赫伯·薩特的話來說:

永遠不要使用擁有原始指針和刪除,除非在實現您自己的低級數據結構的極少數情況下(即使如此,將其很好地封裝在類邊界內)。

Herb Sutter 和 Bjarne Stroustrup 的 C++ Core Guidelines 中也表達了這些內容:

這個問題不能通過將所有擁有指針轉換為 unique_ptrs 和 shared_ptrs 來解決(大規模),部分原因是我們需要/使用擁有“原始指針”以及我們基本資源句柄的實現中的簡單指針 例如,常見的向量實現有一個擁有指針和兩個非擁有指針。

使用原始指針在 C++ 中編寫鏈表類可能是一項有用的學術練習。 使用智能指針用 C++ 編寫鏈表類是一項毫無意義的學術練習。 在生產代碼中使用這兩個自制的東西幾乎是自動錯誤的。


[*]或者只是std::vector ,因為由於緩存位置,無論如何它幾乎總是更好的選擇。

設置智能指針增強列表基本上有兩種選擇:

  1. 使用std::unique_ptr

     template<typename T> struct Node { Node* _prev; std::unique_ptr<Node> _next; T data; }; std::unique_ptr<Node<T> > root; //inside list

    那將是我的第一選擇。 唯一指針_next負責沒有內存泄漏,而_prev是一個觀察指針。 然而,復制之類的東西需要手工定義和實現。

  2. 使用shared_ptr

     template<typename T> struct Node { std::weak_ptr<Node> _prev; //or as well Node* std::shared_ptr<Node> _next; T data; }; std::shared_ptr<Node<T> > root; //inside list

    這是更安全的替代方案,但性能不如唯一指針。 此外,它是可復制的設計。

在這兩種想法中,一個節點擁有完整的剩余列表。 現在,當一個節點超出范圍時,剩余的列表沒有成為內存泄漏的危險,因為節點被迭代破壞(從最后一個開始)。

_prev指針在兩個選項中都只是一個觀察指針:它的任務不是保持先前的節點處於活動狀態,而只是提供訪問它們的鏈接。 為此,一個Node *通常就足夠了(--注意:觀察指針意味着您永遠不會在指針上執行與內存相關的操作,例如newdelete )。

如果您想要更高的安全性,您也可以為此使用std::weak_ptr 這可以防止諸如

std::shared_ptr<Node<T> > n;
{
    list<T> li;
    //fill the list
    n = li.root->next->next; //let's say that works for this example
}
n->_prev; //dangling pointer, the previous list does not exists anymore 

使用weak_ptr ,您可以lock()它並以這種方式_prev是否仍然有效。

我會看看 std::list 的接口,它是鏈表的 C++ 實現。 似乎您正在接近您的鏈接列表類的模板錯誤。 理想情況下,您的鏈表不應該關心所有權語義(即它是否使用原始 ptr、智能指針或堆棧分配變量進行實例化)。 下面是 STL 容器的所有權語義示例。 但是,還有來自更權威來源的更好的 STL 和所有權示例。

#include <iostream>
#include <list>
#include <memory>

using namespace std;

int main()
{

    // Unique ownership.
    unique_ptr<int> int_ptr = make_unique<int>(5);

    {
        // list of uniquely owned integers.
        list<unique_ptr<int>> list_unique_integers;

        // Transfer of ownership from my parent stack frame to the
        // unique_ptr list.
        list_unique_integers.push_back(move(int_ptr));

    } // list is destroyed and the integers it owns.

    // Accessing the integer here is not a good idea.
    // cout << *int_ptr << endl;
    // You can make a new one though.
    int_ptr.reset(new int(6));

    // Shared ownership.
    // Create a pointer we intend to share.
    shared_ptr<int> a_shared_int = make_shared<int>(5);

    {
        // A list that shares ownership of integers with anyone that has
        // copied the shared pointer.
        list<shared_ptr<int>> list_shared_integers;

        list_shared_integers.push_back(a_shared_int);

        // Editing and reading obviously works.
        const shared_ptr<int> a_ref_to_int = list_shared_integers.back();
        (*a_ref_to_int)++;
        cout << *a_ref_to_int << endl;

    } // list_shared_integers goes out of scope, but the integer is not as a
    // "reference" to it still exists.

    // a_shared_int is still accessible.
    (*a_shared_int)++;
    cout << (*a_shared_int) << endl;

} // now the integer is deallocated because the shared_ptr goes 
// out of scope.

了解所有權、內存分配/解除分配和共享指針的一個很好的練習是做一個教程,您可以在其中實現自己的智能指針。 然后,您將確切地了解如何使用智能指針,並且您將有一個 xen 時刻,您將意識到 C++ 中的幾乎所有內容都返回到 RAII(資源所有權)。

所以回到你問題的關鍵。 如果您想堅持使用 T 類型的節點,請不要將節點包裝在智能指針中。 Node 析構函數必須刪除底層原始指針。 原始指針可能指向指定為 T 的智能指針本身。當您的“LinkedList”類析構函數被調用時,它使用 Node::next 遍歷所有節點並調用delete node; 在它獲得指向下一個節點的指針之后。

您可以創建一個列表,其中節點是智能指針......但這是一個非常專業的鏈表,可能稱為 SharedLinkedList 或 UniqueLinkedList,具有非常不同的對象創建、彈出等語義。僅作為示例,UniqueLinkedList 會將節點移入向調用者彈出值時的返回值。 要對這個問題進行元編程,需要對傳遞的不同類型的 T 使用部分特化。 例如,類似於:

template<class T>
struct LinkedList
{
    Node<T> *head;
};

// The very start of a LinkedList with shared ownership. In all your access
// methods, etc... you will be returning copies of the appropriate pointer, 
// therefore creating another reference to the underlying data.
template<class T>
struct LinkedList<std::shared_ptr<T>>
{
    shared_ptr<Node<T>> head;
};

現在您開始實施您自己的 STL! 使用此方法,您已經可以看到問題的評論中提到的潛在問題。 如果節點有 shared_ptr next 它將導致調用該共享節點的析構函數,該析構函數將調用下一個共享節點析構函數等等(由於遞歸可能導致堆棧溢出)。 所以這就是為什么我不太關心這種方法。

結構看起來像

template<typename T> struct Node
{
T data;
shared_ptr<Node<T>> next;
};

節點的創建看起來像

shared_ptr<Node<int>> head(new Node<int>);

auto head = make_shared<Node>(Node{ 1,nullptr });

不要在類似數據結構的圖形中使用智能指針,因為它可能會導致堆棧溢出,由於析構函數或 inc 的遞歸調用導致許多性能問題,由於 dfs 和 bfs 算法的工作原理,decr 引用計數不是最優的

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM