接下來我們要進入 Rust 的重頭戲了,處理記憶體
在這之前,我們先來認識 Stack 跟 Heap 這兩個資料結構
我們將分成記憶體處理方式 / 儲存方式 / 分配記憶體的彈性限制 以及運作模式來解釋這兩個差別
在程式開發中,常會用來存取區域變數
Stack 負責儲存固定長度
的資料格式(即是在執行期也不能改其長度),以連續,且 array 的形式來存放資料
會存放在 Stack 中的資料為此變數的 指標
容量
長度
,這些資料基本上都是固定不會變動的
指標(pointer)
會指向該變數的 heap 位置容量
表示 heap 位置中可以容納多少個元素長度
目前變數中有多少元素
我們用 Vec 來看一下:
Vec 在產生一個產生一個實體時,會以 stack 方式分配一個固定位置給 x
這個變數,並存放上述的 指標
容量
長度
一開始會給 Vec 固定的記憶體大小,這個記憶體大小怎麼算?
上述說的三個資料,一個資料佔據 8 bytes,三個就會是 24 byte
為什麼一個資料佔據的是 8 bytes ?
因為在 64 位元的系統上,一個欄位就是 64 位元,而 64 位元就是 8 bytes
fn main() {
let x = vec!['A', 'B', 'C'];
// std::mem::size_of_val 會回傳佔據多少 bytes
// & 回傳 x 變數,但不是變數的值
let size = std::mem::size_of_val(&x);
// capacity() 回傳可容納的元素數量
let cap = x.capacity();
println!("{:?}", x);
println!("容量: {}", cap);
println!("佔據的記憶體大小(stack): {} byte", size);
}
結果:
['A', 'B', 'C']
容量: 3
佔據的記憶體大小(stack): 24 byte
Stack 是以 FILO 概念來針對記憶體做儲存,
FILO (First In Last Out)是什麼呢?
就像我們在裝箱行李一樣,放越裡面的東西,到飯店後越後面拿出來。
也因為 Stack 負責處理固定長度的資料格式,在處理記憶體上的效率會比 Heap 更好一點
但也因為沒辦法彈性的調整存取空間,也常常會導致 stack over flow 的情況產生
我們可以使用 Push Pop 來針對資料作增加及移除的動作
順帶一提, Stack 的儲存空間取決於機器的記憶體空間
Heap 跟 Stack 會有很大的差異,在程式開發中,我們通常會拿來存取全域變數
儲存的資料格式並沒有限定長度(即是在執行期可以改其長度),
將會以 array 以及 trees 的方式來做存取,存放的地方採隨機制,
哪裡有空位並且已經釋出就可以進行存取
因為 Heap 並沒有限制存取的資料長度,
所以在執行的效能會比 Stack 差,且他並不像 Stack 一樣,
針對變數的生命週期需要去做手動的設定,
一旦忘記就有可能會造成記憶體的浪費
我們來看一下 Vec 型態的資料
我們以剛剛的範例來看,上面有提到 Stack 會存放變數所指向的 heap 位置以及長度等固定資料,那所以 heap 存了什麼東西?
fn main() {
let mut x = vec!['A', 'B', 'C'];
let address = &x as *const Vec<char>;
let size = std::mem::size_of_val(&x);
let cap = x.capacity();
println!("{:?}", x);
println!("容量: {}", cap);
println!("佔據的記憶體大小: {} byte", size);
}
結果分別是:
['A', 'B', 'C']
容量: 3
佔據的記憶體大小: 24 byte
我們可以用以下的方法來看
fn main() {
let x = vec!['A', 'B', 'C'];
let address = &x as *const Vec<char>;
let ptr = x.as_ptr();
println!("{:?}", x);
println!("查看 Vec 的前三個元素的資料內容:");
unsafe {
for i in 0..3 {
println!("位置 {:?} 的資料內容: {}", ptr.offset(i), *ptr.offset(i));
}
}
}
結果:
查看 Vec 的前三個元素的資料內容:
位置 0x600003a5c040 的資料內容: A
位置 0x600003a5c044 的資料內容: B
位置 0x600003a5c048 的資料內容: C
如果我們的 x 是可以更動的,這時候會長怎樣呢?
x 更改前後的元素數量不一樣,所以我們把 unsafe 裡面的 for 改成大一點的數字來方便觀察
可以看到,在更改前的 x 為 ['A', 'B', 'C']
Stack 位置存放在 0x16ef76450
元素存放在 heap 的 0x600001860040
0x600001860044
0x600001860048
當我們 push 一個元素進去後,Stack 的位置還是一樣的
不過 heap 的位置不一樣了,主要是有做任何更動時,會把他視為一個新的東西,並且另外去找記憶體存放
fn main() {
let mut x = vec!['A', 'B', 'C'];
// &x 是將這個變數指向 x ,並且轉變為指標,且不可變
let address = &x as *const Vec<char>;
// 回傳 x 的 heap 位址
let ptr = x.as_ptr();
println!("{:?}", x);
println!("x 的記憶體位置在:{:?}", address);
println!("查看 Vec 的前三個元素的資料內容:");
// 我們需要去看記憶體的內容,在 Rust 中算是危險的動作,因此需要加上 unsafe,並且要小心動作
unsafe {
for i in 0..5 {
// 變數前面加上 * 指的是要去看這變數的 value
println!("位置 {:?} 的資料內容: {}", ptr.offset(i), *ptr.offset(i));
}
};
x.push('D');
println!("{:?}", x);
println!("x 的記憶體位置在:{:?}", address);
let new_ptr = x.as_ptr();
unsafe {
for i in 0..5 {
println!(
"位置 {:?} 的資料內容: {}",
new_ptr.offset(i),
*new_ptr.offset(i)
);
}
}
}
結果:
['A', 'B', 'C']
x 的記憶體位置在:0x16ef76450
查看 Vec 的前三個元素的資料內容:
位置 0x600001860040 的資料內容: A
位置 0x600001860044 的資料內容: B
位置 0x600001860048 的資料內容: C
位置 0x60000186004c 的資料內容:
位置 0x600001860050 的資料內容:
['A', 'B', 'C', 'D']
x 的記憶體位置在:0x16ef76450
位置 0x600001a651a0 的資料內容: A
位置 0x600001a651a4 的資料內容: B
位置 0x600001a651a8 的資料內容: C
位置 0x600001a651ac 的資料內容: D
位置 0x600001a651b0 的資料內容:
Rust 新手上路,如有錯誤歡迎指正