本篇博客我們來解釋幾個名詞怔毛,棧
、堆
腾降、作用域
拣度、所有權(quán)
、所有權(quán)移動
棧
棧是在代碼運行時螃壤,可供使用的一塊內(nèi)存抗果。它的存取數(shù)據(jù)方式是先進后出
,或者說后進先出
奸晴。想象有一個箱子冤馏,你往里放本子,最先放入的本子寄啼,是在箱子底下逮光,當你要使用本子時,總是從頂上取一個使用墩划,也就是取最后放入的一個本子涕刚。
因為這種存取數(shù)據(jù)時總是在棧頂操作,而不需要去內(nèi)存中尋找一個位置乙帮,所以棧的操作是時分迅速的杜漠。
還有一個點是,存在棧里的數(shù)據(jù),都是以知的固定大小驾茴。這一點的意思是盼樟,例如要讓用戶輸入一個名字,因為不知道用戶會輸入多少字符锈至,所以這個數(shù)據(jù)就無法放在棧中晨缴,因為無法事先知道明確的大小。
堆
在編譯時大小未知或大小可能變化的數(shù)據(jù)裹赴,要改為存儲在堆上。堆是缺乏組織的:當向堆放入數(shù)據(jù)時诀浪,你要請求一定大小的空間棋返。操作系統(tǒng)在堆的某處找到一塊足夠大的空位,把它標記為已使用雷猪,并返回一個表示該位置地址的 指針(pointer)
睛竣。這個過程稱作 在堆上分配內(nèi)存(allocating on the heap)
,有時簡稱為 分配(allocating)
求摇。
作用域
作用域可以理解為一個東西在程序中的有效范圍射沟。對于Rust來說,當一個變量出了作用域后与境,對應的內(nèi)存就會自動被釋放掉验夯,變量變?yōu)闊o效狀態(tài)。
{ // s 在這里無效, 它尚未聲明
let s = "hello"; // 從此處起摔刁,s 是有效的
// 使用 s
} // 此作用域已結(jié)束挥转,s 不再有效
字符串類型 String
之前在數(shù)據(jù)類型一節(jié),沒有講到 String共屈,是因為牽扯到堆棧的問題绑谣,所以放在這里講。
fn main() {
// 像這種直接硬編碼在代碼里的字符串拗引,是放在棧上的借宵,并且不可改變
let name = "Jack";
// 使用String::from創(chuàng)建的,是在堆上分配內(nèi)存矾削,并且是可以改變的
let mut my_name = String::from("Jack");
my_name.push_str(", My name is Jack");
// 輸出 Jack, My name is Jack
println!("{}", my_name);
}
當調(diào)用
String::from
時壤玫,它的實現(xiàn) (implementation) 請求其所需的內(nèi)存。這在編程語言中是非常通用的哼凯。
所有權(quán)
- Rust 中每一個值都有一個被稱為
所有者
的變量 - 值垦细,有且只有一個所有者
- 當所有者(變量)離開作用域時,這個值被丟棄挡逼,內(nèi)存被釋放
移動
先看下面一段代碼
fn main() {
let x = 10;
let y = x;
println!("x: {}, y:{}", x, y);
let name1 = "Fred";
let name2 = name1;
println!("name1: {}, name2: {}", name1, name2);
}
很正常括改,最后輸出了 x: 10, y:10
和 name1: Fred, name2: Fred
再看下面這段代碼
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);
let name2 = name1;
println!("name2: {}", name2);
// 編譯出錯,這句會出錯
println!("name1 again: {}", name1);
}
為什么加了最后一句會編譯出錯呢,這里涉及到一個概念嘱能,移動
吝梅。首先 name1 指向的值是分配在堆上的。當將 name1 賦值 給 name2后惹骂,在有一些編程語言苏携,兩個變量會指向同一塊堆內(nèi)存區(qū)域,但是對于Rust來說对粪,不是這樣的右冻,Rust在這里會直接讓 name1 失效
,避免兩個指針指向同一塊堆內(nèi)存著拭。因為 Rust 會自動釋放內(nèi)存纱扭,這樣可以避免當兩個變量超出作用域時,導致重復的內(nèi)存釋放問題儡遮。將 name1 賦值給 name2乳蛾,這個操作叫做移動
,name1移動到了name2鄙币,移動后肃叶,name1自動失效,所以最后一句訪問 name1 會編譯出錯十嘿。
更詳細的內(nèi)容 官方文檔
這里要記住因惭,對于那些固定大小的數(shù)據(jù)類型,
i32
,f32
,bool
绩衷,char
等不會存在移動
的問題筛欢。但是對于存儲在堆
上的數(shù)據(jù),不管是String還是后面自定義的數(shù)據(jù)類型唇聘,這樣的操作都會觸發(fā)移動
有沒有辦法將指上堆內(nèi)存的變量賦值給另一個變量不觸發(fā)移動
呢版姑?有!方法就是克隆迟郎,看下面的代碼剥险。
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);
let name2 = name1.clone();
println!("name2: {}", name2);
println!("name1 again: {}", name1);
}
和之前的代碼只有第5行變了,當調(diào)用了 clone()
函數(shù)后宪肖,會導致 name1 指向的堆上的內(nèi)存復制一份表制。所以這里就沒有移動
。String內(nèi)部實現(xiàn)了 clone()
控乾,當我們自定義數(shù)據(jù)結(jié)構(gòu)時么介,如果要有克隆功能,需要自己實現(xiàn) clone()
方法蜕衡。這個后面會講到壤短。
移動與函數(shù)
說完了移動
,就需要說一下移動和函數(shù)相關的東西。如果將一個值作為參數(shù)久脯,去調(diào)用一個函數(shù)纳胧,如果這個值是在棧上,那么不會發(fā)生什么帘撰,但是如果這個值是分配在堆上跑慕,那么它會移動到函數(shù)內(nèi)部。
看下面的代碼(注意看代碼的注釋)
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);
// name1 的值移動了函數(shù)里
takes_ownership((name1));
// name1 已經(jīng)無效摧找,這里再使用就會編譯出錯
// println!("name1 again: {}", name1);
}
fn takes_ownership(str: String) {
println!("i have ownership: {}", str);
}
下面的代碼核行,函數(shù)在結(jié)束時將 所有權(quán)
返回
fn main() {
let name1 = String::from("Fred");
println!("name1: {}", name1);
// 因為name1不是mut的,所以這里的name1相當于創(chuàng)建了一個
// 新的變量name1, 本質(zhì)上并不是之前的
let name1 = takes_and_gives_back(name1);
println!("name1 again: {}", name1);
}
fn takes_and_gives_back(str: String) -> String {
println!("i have ownership: {}", str);
// 這里將值返回蹬耘,所有權(quán)移出函數(shù)
str
}