Rust 原始指針功能探索
問(wèn)題
上一章使用 Rust 語(yǔ)言最后實(shí)現(xiàn)的樹結(jié)構(gòu)存在著空間浪費(fèi),主要體現(xiàn)在使用 Vec<T> 容器存儲(chǔ)一個(gè)結(jié)點(diǎn)的子結(jié)點(diǎn),以容器中含有 0 個(gè)元素表征一個(gè)結(jié)點(diǎn)沒有子結(jié)點(diǎn)。含有 0 個(gè)元素的容器,它本身也要占用微量但可觀的內(nèi)存空間。
類似問(wèn)題也存在于上一章的 C 程序——為了對(duì) Rust 程序進(jìn)行模擬,使用了 GLib 庫(kù)的 GPtrArray 容器,然而 C 語(yǔ)言為指針提供了值 NULL,可代替含有 0 個(gè)元素的 GPtrArray 容器,從而達(dá)到節(jié)約空間的目的。對(duì)于 Rust 語(yǔ)言,在目前我熟悉的知識(shí)范圍內(nèi),只能使用 Option<T> 對(duì) Vec<T> 進(jìn)行封裝,以 None 表達(dá)一個(gè)結(jié)點(diǎn)沒有子結(jié)點(diǎn),此舉也能夠節(jié)省空間,但是在構(gòu)建樹結(jié)構(gòu)的過(guò)程中,需要頻繁使用 match 語(yǔ)句解除 Option<T> 封裝。不過(guò),我知道在引用、智能指針等重重封裝之下,Rust 語(yǔ)言依然別有洞天——原始指針,本章嘗試探索和施展它的力量。
unsafe
Rust 語(yǔ)言的原始指針分為兩種類型,一種是 *const T,另一種是 *mut T,T 為變量類型。*const T 相當(dāng)于 C 語(yǔ)言里的常量指針——指針指向一個(gè)常量,即指針?biāo)笇?duì)象(數(shù)據(jù))不可修改。*mut T 相當(dāng)于 C 語(yǔ)言里的普通指針。
以下代碼創(chuàng)建了一個(gè)指向 i64 類型的變量的常量指針:
fn main() {
let a: i64 = 42;
let p = &a as *const i64;
unsafe {
println!("{}", *p);
}
}上述代碼,使用 Rust 語(yǔ)言的類型轉(zhuǎn)換語(yǔ)法 as ... 將 a 的引用轉(zhuǎn)換為常量指針類型,使得指針 p 指向變量 a。與引用相似,稱某個(gè)指針指向了某個(gè)變量,其意為該指針的值是變量所綁定的值的地址。此外,上述代碼出現(xiàn)了 unsafe 塊。在 Rust 語(yǔ)言中,創(chuàng)建原始指針是安全的,但是對(duì)指針進(jìn)行解引用——訪問(wèn)指針?biāo)笇?duì)象,是不安全的,必須將相應(yīng)代碼包含在 unsafe 塊內(nèi)。與上述代碼等價(jià)的 C 代碼為
#include <stdint.h>
#include <stdio.h>
int main(void) {
int64_t a = 42;
const int64_t *p = &a;
printf("%ld\n", *p);
return 0;
}在上述代碼中,無(wú)論是 Rust 還是 C,以解引用的方式修改 p 所指對(duì)象,例如
*p = 3;
會(huì)導(dǎo)致編譯器報(bào)錯(cuò)。
以下代碼演示了 *mut T 指針的基本用法:
fn main() {
let mut a: i64 = 42;
let p = &mut a as *mut i64;
unsafe {
*p = 3;
}
println!("{}", a);
}輸出為 3。
與上述代碼等價(jià)的 C 代碼如下:
#include <stdint.h>
#include <stdio.h>
int main(void) {
int64_t a = 42;
int64_t *p = &a;
*p = 3;
printf("%ld\n", a);
return 0;
}擁抱原始指針
基于原始指針,TreeNode 可以定義為
#[derive(Debug)]
struct TreeNode {
data: *const str,
children: *mut Vec<*mut TreeNode>
}與上一章的 TreeNode 相比,上述結(jié)構(gòu)體定義不再需要壽命標(biāo)注,因?yàn)?nbsp;data 是一個(gè)原始指針,不再是引用。引用的安全性由 Rust 編譯器負(fù)責(zé),故而限制非常多,而原始指針的安全性由編程者負(fù)責(zé),近乎零限制。
以下代碼可構(gòu)造樹結(jié)點(diǎn)的實(shí)例:
let mut root = TreeNode {data: "Root node", children: std::ptr::null_mut()};
println!("{:?}", root);輸出為
TreeNode { data: 0x556836ca1000, children: 0x0 }
由于 data 和 children 皆為原始指針。Rust 標(biāo)準(zhǔn)庫(kù)為原始指針類型實(shí)現(xiàn)的 Debug 特性輸出的是指針的值,即指針?biāo)缸兞康膬?nèi)存地址。再次強(qiáng)調(diào),變量的內(nèi)存地址,其含意是變量所綁定的值的內(nèi)存地址。另外,需要注意,上述代碼使用了 Rust 標(biāo)準(zhǔn)庫(kù)函數(shù) std::ptr::null_mut 為指針構(gòu)造空值。對(duì)于常量指針,可使用 std::ptr::null 構(gòu)造空值。
由于 root.data 的類型現(xiàn)在是 *const str,即該指針指向類型為 str 的值。該指針類型能否像 &str 那樣可通過(guò) println! 輸出嗎?動(dòng)手一試:
unsafe {
println!("{}", root.data);
}編譯器報(bào)錯(cuò),稱 *const str 未實(shí)現(xiàn) std::fmt::Display 特性。
下面試驗(yàn)一下指針解引用的方式:
unsafe {
println!("{}", *root.data);
}編譯器依然報(bào)錯(cuò),稱 str 的長(zhǎng)度在編譯期未知。這個(gè)報(bào)錯(cuò)信息,意味著 *root.data 的類型為 str,那么再其之前再加上 & 是否構(gòu)成 &str 類型呢?
unsafe {
println!("{}", &*root.data);
}問(wèn)題得以解決,輸出為
Root node
現(xiàn)在的 root.children 是空指針。要為 root 結(jié)點(diǎn)構(gòu)造子結(jié)點(diǎn),需要令 root.children 指向一個(gè) Vec<*mut TreeNode> 實(shí)例:
let mut root_children = vec![]; root.children = &mut root_children as *mut Vec<*mut TreeNode>;
然后按照以下代碼所述方式為 root 構(gòu)造子結(jié)點(diǎn):
let mut first = TreeNode {data: "First child node",
children: std::ptr::null_mut()};
let child_1 = &mut first as *mut TreeNode;
unsafe {
(*root.children).push(child_1);
}以下代碼可打印 root 的子結(jié)點(diǎn)信息:
unsafe {
println!("{:?}", *((*root.children)[0]));
}輸出為
TreeNode { data: 0x55e47fa200c2, children: 0x0 }
使用原始指針之后,樹結(jié)點(diǎn)的部分信息以內(nèi)存地址形式呈現(xiàn),若想查看該地址存儲(chǔ)的數(shù)據(jù),如上述代碼所示,需要對(duì)數(shù)據(jù)結(jié)構(gòu)中的指針解引用。若是遇到多重指針,需要逐級(jí)解引用。
鏈表
對(duì)于樹結(jié)點(diǎn)而言,使用 Vec<T> 容器存儲(chǔ)其子結(jié)點(diǎn)并非必須。本質(zhì)上,將樹的結(jié)構(gòu)表示為鏈?zhǔn)浇Y(jié)構(gòu)更為自然。在已初步掌握原始指針的情況下,應(yīng)當(dāng)運(yùn)用原始指針對(duì)樹結(jié)點(diǎn)的定義給出更為本質(zhì)的表達(dá),例如
#[derive(Debug)]
struct TreeNode {
data: *const str,
upper: *mut TreeNode, // 上層結(jié)點(diǎn)
prev : *mut TreeNode, // 同層前一個(gè)結(jié)點(diǎn)
next : *mut TreeNode, // 同層后一個(gè)結(jié)點(diǎn)
lower: *mut TreeNode // 下層結(jié)點(diǎn)
}基于上述樹結(jié)點(diǎn)定義構(gòu)建的樹結(jié)構(gòu),其根結(jié)點(diǎn)的 upper 域?yàn)榭罩担~結(jié)點(diǎn)的 lower 域?yàn)榭罩?。樹中任意一個(gè)結(jié)點(diǎn),與之有共同父結(jié)點(diǎn)的同層結(jié)點(diǎn)可構(gòu)成一個(gè)雙向鏈表。
以下代碼構(gòu)造了樹的三個(gè)結(jié)點(diǎn):
let mut root = TreeNode {data: "Root",
upper: std::ptr::null_mut(),
prev: std::ptr::null_mut(),
next: std::ptr::null_mut(),
lower: std::ptr::null_mut()};
let mut a = TreeNode {data: "A",
upper: std::ptr::null_mut(),
prev: std::ptr::null_mut(),
next: std::ptr::null_mut(),
lower: std::ptr::null_mut()};
let mut b = TreeNode {data: "B",
upper: std::ptr::null_mut(),
prev: std::ptr::null_mut(),
next: std::ptr::null_mut(),
lower: std::ptr::null_mut()};現(xiàn)在,讓 a 和 b 作為 root 的子結(jié)點(diǎn):
a.upper = &mut root as *mut TreeNode; a.next = &mut b as *mut TreeNode; b.upper = &mut root as *mut TreeNode; b.prev = &mut a as *mut TreeNode; root.lower = &mut a as *mut TreeNode;
可以通過(guò)打印各個(gè)結(jié)點(diǎn)的結(jié)構(gòu)及其地址確定上述代碼構(gòu)造的樹結(jié)構(gòu)是否符合預(yù)期:
// 打印結(jié)點(diǎn)結(jié)構(gòu)信息
println!("root: {:?}\na: {:?}\nb: {:?}", root, a, b);
// 打印結(jié)點(diǎn)的內(nèi)存地址
println!("root: {:p}, a: {:p}, b: {:p}", &root, &a, &b);結(jié)構(gòu)體的方法
上一節(jié)構(gòu)建樹結(jié)點(diǎn)的代碼有較多重復(fù)。由于 TreeNode 是結(jié)構(gòu)體類型,可為其定義一個(gè)關(guān)聯(lián)函數(shù),例如 new,用于簡(jiǎn)化結(jié)構(gòu)體實(shí)例的構(gòu)建過(guò)程。像這樣的關(guān)聯(lián)函數(shù),在 Rust 語(yǔ)言里稱為結(jié)構(gòu)體的方法。
以下代碼為 TreeNode 類型定義了 new 方法:
impl TreeNode {
fn new(a: &str) -> TreeNode {
return TreeNode {data: a,
upper: std::ptr::null_mut(),
prev: std::ptr::null_mut(),
next: std::ptr::null_mut(),
lower: std::ptr::null_mut()};
}
}以下代碼基于 TreeNode::new 方法構(gòu)造三個(gè)樹結(jié)點(diǎn):
let mut root = TreeNode::new("Root");
let mut a = TreeNode::new("A");
let mut b = TreeNode::new("B");定義結(jié)構(gòu)體的方法與定義普通函數(shù)大致相同,形式皆為
fn 函數(shù)名(參數(shù)表) -> 返回類型 {
函數(shù)體;
}
二者的主要區(qū)別是,前者需要在結(jié)構(gòu)體類型的 impl 塊內(nèi)定義。
結(jié)構(gòu)體方法有兩種,一種是靜態(tài)方法,另一種是實(shí)例方法。上述代碼定義的 new 方法即為靜態(tài)方法,需要通過(guò)結(jié)構(gòu)體類型調(diào)用該類方法。至于實(shí)例方法的定義,見以下示例
impl TreeNode {
fn display(&self) {
println!("{:p}: {:?}", self, self);
}
}TreeNode 的 display 方法可通過(guò)TreeNode 的實(shí)例調(diào)用,例如
let mut root = TreeNode::new("Root");
root.display();結(jié)構(gòu)體類型的實(shí)例方法定義中,&self 實(shí)際上是 Rust 語(yǔ)法糖,它是 self: &Self 的簡(jiǎn)寫,而 Self 是結(jié)構(gòu)體類型的代稱。對(duì)于 TreeNode 類型而言,self: &Self 即 self: &TreeNode。
堆空間指針
上述示例構(gòu)造的原始指針?biāo)缸兞康闹到晕挥跅?臻g。事實(shí)上,原始指針也能以智能指針為中介指向堆空間中的值。例如
let root = Box::into_raw(Box::new(TreeNode::new("Root")));
unsafe {
(*root).display();
}上述代碼中的 root 的類型為 *mut TreeNode,因?yàn)?nbsp;Box::into_raw 方法可將一個(gè) Box<T> 指針轉(zhuǎn)化為原始指針類型。
需要注意的是,在上述代碼中,堆空間中的值所占用的內(nèi)存區(qū)域是由智能指針分配,但是 Box::into_raw 會(huì)將 Box<T> 指針消耗掉,并將其分配的內(nèi)存區(qū)域所有權(quán)移交于原始指針。這塊區(qū)域的釋放,需由原始指針的使用者負(fù)責(zé),因此上述代碼實(shí)際上存在著內(nèi)存泄漏,因?yàn)?nbsp;root 指向的內(nèi)存區(qū)域并未被釋放。
釋放 root 所指內(nèi)存區(qū)域的最簡(jiǎn)單的方法是,將其所指內(nèi)存區(qū)域歸還于智能指針,由該智能指針負(fù)責(zé)釋放。例如
let root = Box::into_raw(Box::new(TreeNode::new("Root")));
unsafe {
let x = Box::from_raw(root);
}也可以手動(dòng)釋放原始指針?biāo)竷?nèi)存區(qū)域,例如
unsafe {
std::ptr::drop_in_place(root);
std::alloc::dealloc(root as *mut u8, std::alloc::Layout::new::<TreeNode>());
}然而現(xiàn)在我并不甚清楚上述代碼的內(nèi)在機(jī)理,簡(jiǎn)記于此,待日后細(xì)究。
C 版本
Rust 的原始指針本質(zhì)上與 C 指針是等價(jià)的。下面是基于 C 指針定義的樹結(jié)點(diǎn):
typedef struct TreeNode {
char *data;
struct TreeNode *upper;
struct TreeNode *prev;
struct TreeNode *next;
struct TreeNode *lower;
} TreeNode;以下代碼定義了樹結(jié)點(diǎn)的構(gòu)造函數(shù):
TreeNode *tree_node_new(char *a) {
TreeNode *x = malloc(sizeof(TreeNode));
x->data = a;
x->upper = NULL;
x->prev = NULL;
x->next = NULL;
x->lower = NULL;
}構(gòu)造三個(gè)樹結(jié)點(diǎn)并建立它們之間的聯(lián)系:
TreeNode *root = tree_node_new("Root");
TreeNode *a = tree_node_new("A");
TreeNode *b = tree_node_new("B");
root->lower = a;
a->upper = root;
a->next = b;
b->upper = root;
b->prev = a;也可以模仿 Rust 的樹結(jié)構(gòu) Debug 特性輸出結(jié)果,為樹結(jié)點(diǎn)定義一個(gè)打印函數(shù):
void tree_node_display(TreeNode *x) {
printf("%p: ", (void *)x);
printf("TreeNode { data: %p, upper: %p, prev: %p, next: %p, lower: %p }\n",
(void *)x->data, (void *)x->upper, (void *)x->prev,
(void *)x->next, (void *)x->lower);
}小結(jié)
目前多數(shù) Rust 教程吝嗇于對(duì)原始指針及其用法給出全面且深入的介紹,甚至將原始指針歸于 Rust 語(yǔ)言高級(jí)進(jìn)階知識(shí),我認(rèn)為這是非常不明智的做法。原始指針不僅應(yīng)當(dāng)盡早介紹,甚至應(yīng)當(dāng)鼓勵(lì)初學(xué)者多加使用。特別在構(gòu)造鏈表(包括棧、堆、隊(duì)列等結(jié)構(gòu))、樹、圖等形式的數(shù)據(jù)結(jié)構(gòu)時(shí),不應(yīng)該讓所謂的程序安全性凌駕于數(shù)據(jù)結(jié)構(gòu)的易用性(易于訪問(wèn)和修改)之上。
對(duì)于 Rust 語(yǔ)言初學(xué)者而言,引用、智能指針和原始指針這三者的學(xué)習(xí)次序,我的建議是原始指針 -> 智能指針 -> 引用,而非多數(shù) Rust 教程建議的引用 -> 智能指針 -> 原始指針。至于這三種指針的用法,我也是推薦先使用原始指針,在遇到難以克服的安全性問(wèn)題時(shí),再考慮使用智能指針或引用對(duì)代碼進(jìn)行重構(gòu),否則初學(xué)者何以知悉智能指針和引用出現(xiàn)和存在的原因呢?
以上就是Rust 原始指針功能探索的詳細(xì)內(nèi)容,更多關(guān)于Rust 原始指針的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Rust編程中的共享狀態(tài)并發(fā)執(zhí)行
雖然消息傳遞是一個(gè)很好的處理并發(fā)的方式,但并不是唯一一個(gè),另一種方式是讓多個(gè)線程擁有相同的共享數(shù)據(jù),本文給大家介紹Rust編程中的共享狀態(tài)并發(fā)執(zhí)行,感興趣的朋友一起看看吧2023-11-11
詳解在Rust語(yǔ)言中如何聲明可變的static類型變量
在Rust中,可以使用lazy_static宏來(lái)聲明可變的靜態(tài)變量,lazy_static是一個(gè)用于聲明延遲求值靜態(tài)變量的宏,本文將通過(guò)一個(gè)簡(jiǎn)單的例子,演示如何使用?lazy_static?宏來(lái)聲明一個(gè)可變的靜態(tài)變量,需要的朋友可以參考下2023-08-08
關(guān)于Rust?使用?dotenv?來(lái)設(shè)置環(huán)境變量的問(wèn)題
在項(xiàng)目中,我們通常需要設(shè)置一些環(huán)境變量,用來(lái)保存一些憑證或其它數(shù)據(jù),這時(shí)我們可以使用dotenv這個(gè)crate,接下來(lái)通過(guò)本文給大家介紹Rust?使用dotenv來(lái)設(shè)置環(huán)境變量的問(wèn)題,感興趣的朋友一起看看吧2022-01-01

