怎么new一个指针_【译】Rust与智能指针
原文鏈接:https://dev.to/imaculate3/that-s-so-rusty-smart-pointers-245l
原文標題:That's so Rusty!: Smart pointers
公眾號:Rust碎碎念
譯者:Praying
如果你一直在訂閱這個系列,關于所有權的那篇文章[1]可能給你帶來了這種印象——Rust 確實是個好東西,C++不應該在生產環境中使用。智能指針可能會改變你的想法。用現代的話來說,Smart pointers 是指那些有點(嗯......)額外(東西)的指針。他們本質上還是管理其所指向的對象的內存地址,并且當對象不再被使用的時候會將其釋放。這消除了很多因不恰當的內存管理而引起的 bug,并使得編程不再那么枯燥乏味。C++智能指針為原始指針提供了一個安全的替代方案,而 Rust 智能指針則在保證安全的前提下擴展了語言功能。
智能指針可以像普通指針一樣被解引用,但是在賦值(assignment)和析構(deallocation)時會表現出不同的行為。因此,有不同類型的智能指針。在本文中,我們將會探討它們如何被用于實現各種鏈表:
- 單鏈表
- 共享鏈表
- 雙鏈表
簡單鏈表
鏈表是一個節點的線性集合,在鏈表中,每個節點指向下一個節點。在一個單鏈表中,每個節點有它自己的數據和指向下一個節點的指針,最后一個節點指向 NULL 表示鏈表結尾。
Rust
在 Rust 中,一個單鏈表節點可以定義如下:
struct?Node?{????value:?i32,
????next:?Node,
}
但是它會因各種原因而無法編譯。首先,因為next可以是 NULL,所以next應該是一個Option,(Option 中的 NULL)相當于 Rust 中的 NULL。此外,Rust 結構體在編譯時必須是確定性大小的。如果Option是Node的一個字段,Node的大小可能和鏈表的長度一樣長,也有可能是無限長的。為了解決這個問題,指針就派上用場了,因為它們擁有有限的大小,畢竟它們只是地址。最直觀的智能指針是 Box(Box)。它在堆上分配數據,并且當它離開作用域的時候,指針和其指向的數據都會被丟棄(drop)。在賦值時,Box 遵循 Rust 的所有權規則;在賦值時,數據和指針的所有權都被移動(move)。把next類型改為Box>,準確地抓住了一個節點的本質。下面的例子展示了兩個節點是如何被單向鏈接為一個鏈表的:
struct?Node?{????value:?i32,
????next:?Box<Option>,
}fn?main()?{let?a?=?Node?{
????????value:?5,
????????next:?Box::new(None),
????};let?b?=?Node?{
????????value:?10,
????????next:?Box::new(Some(a)),
????};println!("b?is?{:?}",?b);//?println!("a?is?{:?}",?a);
}
它可以成功運行,但是如果沒有注釋最后的打印語句會導致編譯錯誤,因為a在當它被賦予b.next的時候被移動(move)了。
C++
C++中與 Box 等價的是 unique pointer。顧名思義,unique pointer 顯式地擁有對象,當達到析構條件時,它會刪除被管理的對象而不管其它指向該對象的指針。出于這個原因,應該只有一個 unique pointer 管理一個對象。如果要把一個對象賦值給另一個 unique pointer,這個指針就必須要被移動(move);所有權被轉移并且先前的指針就是無效的了。聽起來很熟悉?是的,因為 Rust 的所有權系統也有類似的行為。C++ unique pointer 能提供類似的好處,但是他們不能提供編譯期的內存安全保證;對一個無效的指針進行解引用會在運行時出錯。下面是一個通過 unique pointer 來實現鏈表節點的例子:
#include?#include?
#include?
using?namespace?std;
struct?Node
{
????int?value;
????unique_ptr?next;
};void?printNode(unique_ptr?n){if?(n?!=?nullptr)
????{cout?<"value:?"?<value?<",?";
????????printNode(move(n->next));
????}cout?<'\n';
}int?main(){
????Node?a{5,?nullptr};unique_ptr?upA(&a);
????Node?b{10,?move(upA)};unique_ptr?upB(&b);
????printNode(move(upB));//?printNode(move(upA));
}
實現printNode()是因為 C++不能像 Rust 那樣生成toStirng()實現。unique pointerupA被移動(move)以賦值給節點 b 的next,這些指針在傳遞給函數的時候也必須被移動(move)。因為upA是 null,所以沒有注釋最后一條 print 語句會導致一個段錯誤。
共享鏈表(Shared linked list)
在共享鏈表中,兩個或以上的鏈表共享一個或多個節點。下圖展示了一個示例,在該示例中,節點 C-D 被兩個分別以 A 和 B 開始的鏈表共享。
Rust
為了支持共享鏈表,節點必須能夠有多個所有者。我們能將 Box 用于這類鏈表么?
#[derive(Debug)]struct?Node?{
????value:?i32,
????next:?Box<Option>,
}fn?main()?{let?a?=?Node?{
????????value:?5,
????????next:?Box::new(None),
????};let?b?=?Node?{
????????value:?10,
????????next:?Box::new(Some(a)),
????};println!("b?is?{:?}",?b);let?c?=?Node?{
????????value:?20,
????????next:?Box::new(Some(a)),
????};
}
編譯器不會同意因為,Box 只能有一個所有者。
為了支持多個所有者,Rust 有引用計數智能指針,縮寫為Rc。Rc指針通過 clone 來共享,clone 操作會創建一份(Rc的)拷貝,這份拷貝指向相同的數據并增加引用計數。當這些指針失效時,引用計數會減少。
為了讓節點可以共享,next的類型從Box>> 變更為 Rc>。這個變化證明了定義另一個結構體——SharedNode 以區分簡單節點的合理性。a中的節點通過b和c克隆它的智能指針來共享。這一次,編譯器是滿意的。
#[derive(Debug)]struct?SharedNode?{
????value:?i32,
????next:?Rc<Option>,
}use?std::rc::Rc;fn?main()?{let?a?=?Rc::new(Some(SharedNode?{
????????value:?5,
????????next:?Rc::new(None),
????}));let?b?=?SharedNode?{
????????value:?10,
????????next:?Rc::clone(&a),
????};let?c?=?SharedNode?{
????????value:?20,
????????next:?Rc::clone(&a),
????};println!("a?is?{:?}",?a);println!("b?is?{:?}",?b);println!("c?is?{:?}",?c);
}
引用計數( Reference counts)
使用函數Rc::strong_count()可以追蹤引用計數是如何更新的。在下面的例子中,SharedNode 的引用數在 clone 它連接到節點 b 和 c 時增加,當 c 退出作用域時,引用數就會減少。
let?a?=?Rc::new(Some(SharedNode?{??value:?5,
??next:?Rc::new(None),
}));
println!("Rc?count?of?a?after?creating?a?=?{}",?Rc::strong_count(&a));
let?b?=?SharedNode?{
??value:?10,
??next:?Rc::clone(&a),
};
println!("Rc?count?of?a?after?creating?b?=?{}",?Rc::strong_count(&a));
{
??let?c?=?SharedNode?{
??????value:?20,
??????next:?Rc::clone(&a),
??};
??println!("Rc?count?of?a?after?creating?c?=?{}",?Rc::strong_count(&a));
}
println!(
??"Rc?count?of?a?after?c?goes?out?of?scope?=?{}",
??Rc::strong_count(&a)
);
變更(Mutation)
在可變性那篇文章[2]中,我們知道 Rust 不喜歡默認可變性,部分是因為多個可變引用會導致數據競爭(data races)和競態條件(race conditions)。智能指針是可變的,這一點很重要,否則他們的功能會受限。為了彌補這一差距,Rust 提供了RefCell——另一種類型的智能指針,該智能指針提供了內部可變性:一種通過將借用規則執行推遲到運行時來對不可變引用進行修改。內部可變性是有用的,但是因為引用是在運行時被分析的,相較于編譯期分析,它可能會導致不安全的代碼在運行時炸開并且引起性能衰退。
下面的例子演示了Rc和Box類型如何被變更。RefCell有 borrow_mut()函數,該函數返回一個可變的智能指針RefMut,該指針可以被解引用(使用*操作符)和變更。借用規則仍然適用,因此,如果在同一個作用域中使用了多個 RefCell,程序將在運行時發生 panic。
struct?Node?{
????value:?i32,
????next:?BoxOption>>,
}#[derive(Debug)]struct?SharedNode?{
????value:?i32,
????next:?RcOption>>,
}use?crate::List::{Cons,?Nil};use?std::cell::RefCell;use?std::rc::Rc;fn?main()?{println!("Mutating?node");let?node_a?=?Node?{
????????value:?5,
????????next:?Box::new(RefCell::new(None)),
????};let?a?=?Box::new(RefCell::new(Some(node_a)));let?b?=?Node?{?value:?10,?next:?a?};println!("Before?mutation?b?is?{:?}",?b);if?let?Some(ref?mut?x)?=?*b.next.borrow_mut()?{
????????(*x).value?+=?10;
????}println!("After?mutation?b?is?{:?}",?b);println!("Mutating?shared?node?...");let?node_a?=?SharedNode?{
????????value:?5,
????????next:?Rc::new(RefCell::new(None)),
????};let?a?=?Rc::new(RefCell::new(Some(node_a)));let?b?=?SharedNode?{
????????value:?10,
????????next:?Rc::clone(&a),
????};let?c?=?SharedNode?{
????????value:?20,
????????next:?Rc::clone(&a),
????};println!("Before?mutation?a?is?{:?}",?a);println!("Before?mutation?b?is?{:?}",?b);println!("Before?mutation?c?is?{:?}",?c);if?let?Some(ref?mut?x)?=?*a.borrow_mut()?{
????????(*x).value?+=?10;
????}println!("After?mutation?a?=?{:?}",?a);println!("After?mutation?b?=?{:?}",?b);println!("After?mutation?c?=?{:?}",?c);
}
C++
在 C++中與RC等價的是 shared pointer。它有相似的引用計數行為并且變更(mutation)更加簡單。下面的代碼片段展示它是如何被用于創建共享鏈表:
#include?#include?
#include?
using?namespace?std;
struct?SharedNode
{
????int?value;
????shared_ptr?next;
};void?printSharedNode(shared_ptr?n){if?(n?!=?nullptr)
????{cout?<"value:?"?<value?<"?->?";
????????printSharedNode(n->next);
????}cout?<'\n';
}int?main(){
????SharedNode?node_a{5,?nullptr};shared_ptr?a(&node_a);cout?<"Reference?count?of?a:?"?<endl;
????SharedNode?node_b{10,?a};shared_ptr?b(&node_b);cout?<"Reference?count?of?a,?after?linking?to?b:?"?<endl;
????SharedNode?node_c{20,?a};shared_ptr?c(&node_c);cout?<"Reference?count?of?a,?after?linking?to?c:?"?<endl;//?mutation
????a->value?=?2;
????printSharedNode(a);
????printSharedNode(b);
????printSharedNode(c);
????a.reset();cout?<"Reference?count?of?a?on?reset:?"?<endl;//?cout?<value?<
}
輸出如下:
盡管 shared pointer 用起來更加簡單,但是它也不能避免 C++的安全問題。未注釋上面最后一條打印語句會導致運行時的段錯誤。
雙鏈表
在一個雙鏈表中,每個節點都有兩個指針分別指向下一個節點和前一個節點。因此,一個雙鏈表節點有prev字段,類型和next相同。
Rust
使用之前我們用過的指針可以創建名為DoubleNode的雙鏈表。設置和更新prev和next字段需要內部可變性,因此需要RefCell。為了讓DoubleNode能夠被下一個節點和前一個節點所擁有,我們將會使用Rc。兩端節點prev和next字段是可能為空的,所以我們將使用Option。因此,prev和next字段的類型就變成了 Rc>>。
簡單起見,我們創建一個鏈表,該鏈表有兩個節點node_a和node_b以及它們對應的指針a和b。node_b創建時帶有a的一個 clone 副本(next 字段),作為a的下一個節點,并使用內部可變性,node_a的前一個節點指向node_b。這些都在下面的代碼中被實現,代碼中在鏈接節點之前和之后都會打印出節點信息和引用計數。
use?std::cell::RefCell;use?std::rc::Rc;
#[derive(Debug)]
struct?DoubleNode?{
????value:?i32,
????next:?RcOption>>,
????prev:?RcOption>>,
}fn?main()?{let?node_a?=?DoubleNode?{
????????value:?5,
????????next:?Rc::new(RefCell::new(None)),
????????prev:?Rc::new(RefCell::new(None)),
????};let?a?=?Rc::new(RefCell::new(Some(node_a)));let?node_b?=?DoubleNode?{
????????value:?10,
????????next:?Rc::clone(&a),
????????prev:?Rc::new(RefCell::new(None)),
????};let?b?=?Rc::new(RefCell::new(Some(node_b)));println!("Before?linking?a?is?{:?},?rc?count?is?{}",
????????a,
????????Rc::strong_count(&a)
????);println!("Before?linking?b?is?{:?},?rc?count?is?{}",
????????b,
????????Rc::strong_count(&b)
????);if?let?Some(ref?mut?x)?=?*a.borrow_mut()?{
????????(*x).prev?=?Rc::clone(&b);
????}println!("After?linking?a?rc?count?is?{}",?Rc::strong_count(&a));//println!("After?linking?a?is?{:?}",?a);println!("After?linking?b?rc?count?is?{}",?Rc::strong_count(&b));//println!("After?linking?b?is?{:?}",?b);
}
這段代碼可以正常編譯運行,但是當最后兩行被注釋的打印語句取消注釋后,輸出結果就變得有趣了。
對任何一個節點的打印都會無限循環,然后導致棧溢出。這是因為要從一個節點中導出字符串,我們就要展開它所有的字段。要打印node_a,我們打印它的字段:value(5),next(None)和prev(node_b),prev指向一個DoubleNode,因此我們以類似的方式打印它:value(10),next(node_a)和prev(None),next指向DoubleNode,所以我們將其展開,返回的操作繼續打印node_a,這個循環就會永久持續下去。這是一個結果表現為堆棧溢出的循環引用的例子。
循環引用的另一個結果是內存泄漏,當內存沒有被釋放時,就會發生內存泄漏。當成功運行上面的代碼時,可以看出,指針a和指針b的引用計數都是 2。在 main 函數結尾,Rust 會試圖丟棄b,這會使得node_b只剩下 1 個引用,即node_a的prev指針。這個引用計數會一直維持在 1,從而阻止node_b被丟棄。因此,兩個節點都不會被丟棄,從而導致內存泄漏。因為上面的程序運行時間較短,操作系統會清理內存。在像服務器程序這種長期運行的程序中,內存泄漏更為嚴重。這是少數幾個可以從 Rust 編譯器中溜走的 bug。
這意味著在 Rust 中就無法實現雙鏈表了嘛?不,它可以通過另一種稱為 weak pointer 的指針來實現。weak pointer 是這樣一種指針,它持有一個對象的非擁有引用(non-owning reference),該對象由一個共享指針管理。標記為Weak,weak pointer 類似于Rc因為它們都可以共享所有權,但是 weak pointer 并不影響析構。下面的例子展示了它們是如何解決雙鏈表的難題。
use?std::rc::{Rc,?Weak};
#[derive(Debug)]
struct?DoubleNode?{
??value:?i32,
??next:?RcOption>>,
??prev:?WeakOption>>,
}fn?main()?{let?node_a?=?DoubleNode?{
??????value:?5,
??????next:?Rc::new(RefCell::new(None)),
??????prev:?Weak::new(),
??};let?a?=?Rc::new(RefCell::new(Some(node_a)));let?node_b?=?DoubleNode?{
??????value:?10,
??????next:?Rc::clone(&a),
??????prev:?Weak::new(),
??};let?b?=?Rc::new(RefCell::new(Some(node_b)));println!("Before?cycle?a?is?{:?},?rc?count?is?{}",
??????a,
??????Rc::strong_count(&a)
??);println!("Before?cycle?b?is?{:?},?rc?count?is?{}",
??????b,
??????Rc::strong_count(&b)
??);if?let?Some(ref?mut?x)?=?*a.borrow_mut()?{
??????(*x).prev?=?Rc::downgrade(&b);
??}println!("After?cycle?a?rc?count?is?{},?weak?count?is?{}",
??????Rc::strong_count(&a),
??????Rc::weak_count(&a)
??);println!("After?cycle?a?is?{:?}",?a);println!("After?cycle?b?rc?count?is?{},?weak?count?is?{}",
??????Rc::strong_count(&b),
??????Rc::weak_count(&b)
??);println!("After?cycle?b?is?{:?}",?b);
}
打印節點時沒有出現棧溢出說明循環引用已經被移除了。
通過把prev指針改為 weak pointer 實現了這個目標。weak pointer 是通過對共享指針進行降級而不是對其 clone,并且它不會影響有效引用計數。
通過追蹤引用計數,我們可以看到循環引用是如何被避免的。在對節點鏈接兩次后,a有一個強計數 2,b 有一個強計數 1 和一個弱計數 1。在 main 函數結尾處,Rust 會嘗試丟棄b,使node_b僅剩下一個弱計數 1。因為 weak pointer 不影響析構,所以這個節點會被丟棄。在node_b丟棄后,它對a的鏈接也被移除,從而將a的強計數降為 1。當a離開作用域時,node_a的強計數變為 0,從而可以被丟棄。本質上,循環引可以用通過減少某些引用的重要性被解決。這一點在輸出中也很明顯,在輸出中,weak pointer 沒有被展開,而僅僅是注釋為(Weak)。
C++
在 C++中也有 weak pointer 與 Rust 中的相對應。它們以相同的方式用于避免循環引用。它們可以被用于實現雙鏈表,如下面代碼所示:
#include?#include?
#include?
using?namespace?std;
struct?DoubleNode
{
??int?value;
??shared_ptr?next;
??weak_ptr?prev;
};void?printDoubleNode(shared_ptr?n){if?(n?!=?nullptr)
??{cout?<"value:?"?<value?<",?prev:?(Weak)?->";
??????printDoubleNode(n->next);
??}cout?<'\n';
}int?main(){
??DoubleNode?node_a{5,?nullptr,?weak_ptr()};shared_ptr?a(&node_a);
??DoubleNode?node_b{10,?a,?weak_ptr()};shared_ptr?b(&node_b);cout?<"Before?linking,?rc?count?of?a:?"?<endl;cout?<"Before?linking,?rc?count?of?b:?"?<endl;
??printDoubleNode(a);
??printDoubleNode(b);
??a->prev?=?b;cout?<"After?linking,?rc?count?of?a:?"?<endl;cout?<"After?linking,?rc?count?of?b:?"?<endl;
??printDoubleNode(a);
??printDoubleNode(b);
}
下面的輸出表明 weak pointer 沒有影響引用計數。
除了語法上的差異,Rust 智能指針看起來與 C++非常相似。它們是為了解決類似的問題而設計的。Rust 智能指針維護了編譯時的保證(除了循環引用),而 C++智能指針更容易操作,引用計數操作是線程安全的。你更喜歡哪個?
參考資料
[1]所有權的那篇文章: https://dev.to/imaculate3/that-s-so-rusty-ownership-493c
[2]可變性那篇文章: https://dev.to/imaculate3/that-s-so-rusty-mutables-5b40
總結
以上是生活随笔為你收集整理的怎么new一个指针_【译】Rust与智能指针的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python中bs4_python b
- 下一篇: 栈顶指针到底指向哪_被称为“程序员试金石