
以這段Go代碼為例作為開場
func createInt() *int {
i := 42 // int 類型分配在棧上
return &i // 這里由于返回引用類型,分配到堆上
}
func main() {
num := createInt()
println(*num) // 程序結(jié)束時(shí),num指向的堆內(nèi)存會被釋放
}
這是一段Go程序,是健康可運(yùn)行的,createInt函數(shù)返回指針,main函數(shù)調(diào)用,這里叫做 內(nèi)存逃逸
Go語言中的Gc回收器+逃逸分析,這兩個東西組合保障了程序能夠正常運(yùn)行,且不必要擔(dān)心安全問題
當(dāng)這段逃逸的堆內(nèi)存沒有地方再引用時(shí),會被回收掉
懸垂引用
一個引用(或指針)仍然指向一塊內(nèi)存地址,但這塊內(nèi)存已經(jīng)被釋放或不再有效。此時(shí)再繼續(xù)操作程序就會異常
下面是一個C語言例子
#include <stdio.h>
int* createInt() {
int i = 42; // int 類型分配在棧上
return &i; // 返回 i 的地址
} // ?? 函數(shù)結(jié)束,棧的內(nèi)存會被回收
int main() {
int* num = createInt(); // 接收了已釋放內(nèi)存的地址(
printf("%d\n", *num); // ?? 未定義行為!可能打印 42,也可能打印垃圾值,或者崩潰
return 0;
}
C語言沒Go那一套逃逸分析的自動堆內(nèi)存分配機(jī)制,這里的代碼實(shí)現(xiàn)就稱為 懸垂引用,或野指針
如果再C語言中實(shí)現(xiàn)該邏輯,需要手動將函數(shù)createInt返回內(nèi)容分配到堆上,為什么要分配到堆上?
因?yàn)闂5臇|西在函數(shù)執(zhí)行完就被回收了,注意哈,C語言沒有Gc回收機(jī)制,但棧上的內(nèi)存操作系統(tǒng)是會自動管理的
改良后的完整代碼
#include <stdio.h>
#include <stdlib.h> // 包含malloc/free的頭文件
int* createInt() {
int* i = (int*)malloc(sizeof(int)); // 分配堆內(nèi)存
if (i == NULL) { // 檢查內(nèi)存分配是否成功(必做)
printf("內(nèi)存分配失敗\n");
exit(1);
}
*i = 42; // 給堆內(nèi)存賦值
return i; // 返回堆內(nèi)存地址(安全)
}
int main() {
int* num = createInt();
printf("%d\n", *num); // 輸出 42(安全)
free(num); // 釋放堆內(nèi)存(避免內(nèi)存泄漏)
num = NULL; // 置空指針(避免野指針)
return 0;
}
rust中懸垂引用
概念都是一樣的,rust中沒有C語言的malloc/free,沒有Go語言的Gc/逃逸分析
fn main() {
let i = create_int();
println!("{}", i);
}
fn create_int() -> &i32 {
let s = 32;
&s
}
rust的編譯器不僅僅用來轉(zhuǎn)換二進(jìn)制代碼,還提供了代碼解決方案,這是完整的報(bào)錯信息,里面已經(jīng)提示你正確的代碼書寫方式
error[E0106]: missing lifetime specifier
--> src/main.rs:12:20
|
12 | fn create_int() -> &i32 {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
12 | fn create_int() -> &'static i32 {
| +++++++
help: instead, you are more likely to want to return an owned value
|
12 - fn create_int() -> &i32 {
12 + fn create_int() -> i32 {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `naive` (bin "naive") due to 1 previous error
rust中解決懸垂引用的方法為,-> 生命周期
rust 兩大數(shù)據(jù)類型
在搞清楚生命周期之前,要先區(qū)分兩大類型
- 所有權(quán)類型:你是數(shù)據(jù)的“主人”,數(shù)據(jù)存在你的變量里
- 引用類型:你只是數(shù)據(jù)的“借閱者”,數(shù)據(jù)屬于別人,你只能看或改,不能銷毀它
fn main() {
let i = create_int();
let j = create_intptr(); // 會報(bào)錯
}
// 32 這個值是變量 s 的,該函數(shù)返回的是 s,誰接收誰能隨便用
// 不管你是堆還是棧分配,所有權(quán)會自動管理
fn create_int() -> i32 {
let s = 32;
s
}
// 32 這個值是變量 s 的,該函數(shù)返回的是 s 的引用指針,接收方只能看/改
// 但是,函數(shù)create_intptr執(zhí)行結(jié)束了,s 釋放銷毀了,main函數(shù)中的 j 將變成懸垂引用
fn create_intptr() -> &i32 {
let s = 32;
&s
}
換句話來說,
在局部塊兒(函數(shù)、結(jié)構(gòu)體..)返回引用類型,會產(chǎn)生懸空指針;返回所有權(quán)類型,可以隨便玩
rust 中解決懸空指針的方法為 -> 生命周期
這里整理了一份 所有權(quán)類型 和 引用類型 的具體體現(xiàn)
所有權(quán)類型
特征:變量直接包含數(shù)據(jù)(或在堆上擁有數(shù)據(jù)),當(dāng)變量離開作用域時(shí),數(shù)據(jù)會被自動釋放(Drop)。
符號:通常沒有 & 符號。
| 類別 | 具體類型示例 | 說明 |
|---|---|---|
| 基本標(biāo)量類型 |
i32, f64, bool, char
|
這些類型大小固定,直接存儲在棧上。賦值時(shí)會復(fù)制一份新數(shù)據(jù)。 |
| 元組 (Tuple) |
(i32, bool), (String, i32)
|
如果元組內(nèi)包含擁有所有權(quán)的類型,元組本身也擁有它們。 |
| 數(shù)組 (Array) |
[i32; 5], ["a", "b"]
|
大小固定,直接存儲在棧上(除非很大)。賦值時(shí)復(fù)制。 |
| 結(jié)構(gòu)體 (Struct) | struct User { name: String } |
如果結(jié)構(gòu)體字段是 String 等擁有所有權(quán)的類型,結(jié)構(gòu)體實(shí)例就擁有這些數(shù)據(jù)。 |
| 枚舉 (Enum) | enum Option<T> { Some(T), None } |
同上,擁有內(nèi)部數(shù)據(jù)的所有權(quán)。 |
| 字符串 (String) | String::from("hello") |
重點(diǎn):這是堆分配的字符串,變量擁有堆上數(shù)據(jù)的所有權(quán)。 |
| 集合 (Collections) |
Vec<T>, HashMap<K, V>, Box<T>
|
這些都在堆上分配數(shù)據(jù),變量擁有堆數(shù)據(jù)的句柄(指針+容量+長度),負(fù)責(zé)釋放內(nèi)存。 |
| 閉包 (Closure) |
|x| x + 1 (捕獲所有權(quán)時(shí)) |
閉包可以捕獲變量的所有權(quán)。 |
代碼示例
let a = 10; // a 擁有 10
let s = String::from("hi"); // s 擁有堆上的 "hi"
let v = vec![1, 2, 3]; // v 擁有堆上的數(shù)組
let my_struct = User { name: s }; // my_struct 現(xiàn)在擁有了 name (s 的所有權(quán)轉(zhuǎn)移了)
引用類型
特征:變量不包含數(shù)據(jù),只包含數(shù)據(jù)的地址。它們不負(fù)責(zé)釋放內(nèi)存。必須依附于某個擁有所有權(quán)的變量存在。
符號:必須帶有 & (不可變引用) 或 &mut (可變引用)。
| 類別 | 具體類型示例 | 說明 |
|---|---|---|
| 不可變引用 |
&i32, &String, &str
|
只能讀數(shù)據(jù),不能改??梢杂卸鄠€同時(shí)存在。 |
| 可變引用 |
&mut i32, &mut Vec<i32>
|
可以修改數(shù)據(jù)。同一時(shí)間只能有一個。 |
| 切片 (Slices) |
&[i32], &str
|
重點(diǎn):切片是對連續(xù)內(nèi)存部分的引用。&str 是對字符串?dāng)?shù)據(jù)的引用(通常指向 String 內(nèi)部或字面量)。 |
| 原始指針 |
*const T, *mut T
|
類似 C 的指針,不安全(unsafe),但也屬于引用語義(不擁有所有權(quán))。 |
代碼示例
let a = 10;
let r = &a; // r 是引用,借用 a,r 的類型是 &i32
let st = "hello"; // 注意:st是引用類型,引用的一個 "hello" 的串,"hello" 這個字符并不屬于變量 st
let s = String::from("hello");
let slice: &str = &s; // slice 是引用,借用 s 的一部分,類型是 &str
let mut x = 5;
let m = &mut x; // m 是可變引用,類型是 &mut i32
*m = 10; // 通過引用修改數(shù)據(jù)
rust 生命周期
生命周期是編譯器用來確保所有引用都是有效的機(jī)制。它們的主要目的是防止懸垂引用,即引用指向了已經(jīng)被釋放的內(nèi)存
語法規(guī)則
生命周期標(biāo)注以 ' 開頭,通常使用小寫字母,如 'a, 'b, 'c, 'static 等。通常會搭配泛型符號使用。
rust中,泛型不僅僅能用來定義自定義類型,也可用于自定義生命周期類型
static是一個特殊的關(guān)鍵字,下面會單獨(dú)說
不使用生命周期,會報(bào)錯
fn main() {
let st = longest("hello", "world");
println!("{}", st);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
加上生命周期
fn main() {
let st = longest("hello", "world");
println!("{}", st);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
這里聲明了一個生命周期為'a的泛型,返回的引用也使用了'a生命周期,這樣就保證了返回的引用指向的數(shù)據(jù)和傳入的引用指向的數(shù)據(jù)是同一個生命周期,不會出現(xiàn)懸垂引用
拓展
結(jié)構(gòu)體類型使用
struct Excerpt<'a> {
part: &'a str,
}
// impl 方法實(shí)現(xiàn)中也需聲明生命周期: impl<'a> xxx<'a>
impl<'a> Excerpt<'a> {
fn return_str(&self, announcement: &str) -> &str {
println!("Announcement: {}", announcement);
self.part // 返回的是 self 的一部分,生命周期與 self 綁定
}
}
fn main() {
let st = Excerpt { part: "hello" };
println!("{}", st.return_str("world"));
}
多個生命周期
fn get_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
y // 明確返回的是 x 的切片,與 y 的生命周期無關(guān) 返回x可編譯成功,返回y會編譯失敗
}
fn main() {
let st = get_first("hello", "world");
println!("{}", st);
}
'static 生命周期
系統(tǒng)預(yù)設(shè)的一個特殊的生命周期,表示該引用在整個程序運(yùn)行期間都有效。(那怕是聲明方的程序已經(jīng)?;厥樟?
以字面量字符引用為例let s = "world";,這個類型編輯器顯示是&str類型,這里其實(shí)給省略了,它應(yīng)當(dāng)是一個定義了全局生命周期的&'static str類型。
案例_1
// 報(bào)錯
fn longest_1() -> &str {
let s = "world";
s
}
// 正常
fn longest_2(x: &str) -> &str {
let y = "world";
y
}
這是因?yàn)閞ust編譯器存在一套生命周期省略規(guī)則,有一條規(guī)則為 如果函數(shù)有一個輸入引用參數(shù)(如 &str),且返回值是引用類型,編譯器會自動將返回值的生命周期推斷為與這個輸入?yún)?shù)的生命周期相同。
longest_1 手動聲明返回類型為 -> &'static str 可以編譯通過
這個除外,編譯器根據(jù)邏輯,不確定使用x還是y 的生命周期
fn longest_6(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
案例_2
// 沒顯示聲明,任何生命周期都可以傳 'static 'a 'b
fn partial(s: &str) {
println!("Static string: {}", s);
}
// 顯示聲明了 static 周期
fn global(s: &'static str) {
println!("Static string: {}", s);
}
總結(jié)
各語言解決懸空指針的方法
- Go 逃逸分析+Gc垃圾回收
- C 手動分配堆內(nèi)存,手動回收
- rust 生命周期