1 byte = 8 bit,sizeof
(C/C++) 和 {std, core}::mem::size_of
(Rust) 回傳的是 bytes。
sizeof(double) // 8
std::mem::size_of::<f64>() // 8
限於記憶體最小規劃為 bytes,二元值的實作為 8 bit 而非 1 bit。
C | C++ | Rust |
---|---|---|
bool (stdbool.h )/_Bool |
bool |
bool |
- Rust 的
bool
類型僅能轉換至整數類型,不可直接轉型至浮點數。反之,其它類型要透過判斷式變成布林,不能直接放在if
語句中。
C | Rust | ||
---|---|---|---|
unsigned char |
char |
u8 |
i8 |
unsigned short |
short |
u16 |
i16 |
unsigned +(int /long ) |
int /long /long int |
u32 /char |
i32 |
unsigned long long |
long long |
u64 |
i64 |
u128 |
i128 |
||
size_t |
ssize_t |
usize |
isize |
- Rust 的字元類型
char
為 UTF-8 編碼,因此實作上使用u32
而非u8
(ASCII 編碼)。 - 指標 (pointer) 的大小與
size
類型相同,依作業系統而定。32 位元為 4 bytes (32 bit),64 位元為 8 bytes (64 bit)。
C | Rust |
---|---|
float |
f32 |
long double /double |
f64 |
(外部套件提供)f128 |
Rust 擁有靜態容器,方便多變數的儲存,而不需要自己定義類型。
- 陣列 (array) 以連續記憶體儲存多個相同類型的資料,寫作
[T; N]
。其中 T 是任意已知大小的類型,N 為陣列長度,必須是靜態usize
數值。 - 元組 (tuple) 以連續記憶體儲存多個不同或相同類型的資料,寫作
(T1, T2)
。本質上可視為結構體,會有記憶體對齊,但是結構體是自己定義的類型。元組的欄位最高到 12 項支援自動 trait 的實作。
零大小類型 (ZST) 不占用記憶體空間,但是可以用來做語意上的識別,其指標仍與其它指標一樣擁有大小。C 在規範上不允許空的結構體,而 C++ 會使用 1 byte 識別空的結構體。
在 Rust 的單元 (unit) 類型 ()
、空結構體 struct
、空枚舉 enum
、永不 (Never) 類型 !
都是 ZST,單元類型除了代表 C/C++ 的 void 語意以外,也與其它類型無異。如果要做語意上的區分,空結構體可以更加靈活,因為它屬於自己定義的類型,可以實作第三方 trait。最後,空枚舉和 !
僅能作為類似「名稱空間 (namespace)」使用,因為它無法建立實體,但是仍然可以綁定無實體的靜態函式並被當成類型,並且被最佳化消除(如 Result<T, !>
是永不產生錯誤的類型)。
- 指標可視為
usize
的整數,但是為一個記憶體位址。 - 指標 (Pointer) 可以指到記憶體位址,但是無法保證指向的位置有效(已分配記憶體)。
- 參照 (Reference) 可以確保其指到有效位置。C++ 中位置無效時為未定義行為,Rust 透過生命週期參數
'a
保證一定有效。
C | C++ | Rust | |
---|---|---|---|
生指標 (Raw Pointer) | T* |
T const* /T* |
*const T /*mut T |
參照 (Reference) | N/A | T const& /T& |
&'a T /&'a mut T |
右值參照(移動語意) | N/A | T&& |
指派 = 就是移動語意 |
智慧指標(自擁有指標) | N/A | std::unique_ptr<T> |
Box<T> /alloc::box::Box<T> (no-std) |
- 移動語意是指變數綁定 (bind) 在數據上,數據不限定分配在單一變數上,因此轉移到下個變數時不會產生複製。這種設計有利於編譯器最佳化,甚至不用實際移動記憶體位址。過往 C 語言默認指派行為是複製,因此必須分析變數傳遞的分支來簡化複製行為,若為無分支,才等同移動語意。
- 自智慧指標 (Smart pointer) 可以當成長度為 1 的
Vec<T>
容器,指標可以跟一般的變數一樣傳遞,且可以產生深層複製(複製指向類型T
)。好處是保證指向類型不會移動,大小跟指標一樣,而且有自動釋放機制(透過物件導向的解構函式)。
陣列指標 &[T; N]
可以描述連續記憶體的位址,但是當 N 為動態長度時,可以變成 &[T]
,稱為 Slice。可以將 Slice 視為一個結構體包含第一個元素的指標 &T
與動態長度 usize
,以元組表示如 &[T] == (*const T, usize)
和 &mut [T] == (*mut T, usize)
。
- 動態長度的連續記憶體容器變成 Slice,例如
Vec<T>
變成&[T]
。 - 動態字串類型
String
的底層為Vec<u8>
,可以視為&[u8]
,但是字串為 UTF-8 編碼,u8
無法單獨檢索,char
(u32
) 才可以。因此&str
被用來描述編碼的字串 Slice。 - 與字串 Slice
&str
相同,原生平台的&std::ffi::CStr
字串對應平台的編碼字串std::ffi::CString
。
由於 Slice 類型是作為指標傳遞的,指向類型 [T]
、str
或 CStr
為無大小類型 (Unsized Type),記作 !Sized
trait(沒有實作 Sized
)。使用上,只有 ?Sized
可以填入無大小類型和有大小類型。
動態特徵 (Dynamic Trait) 使用特徵描述實體。某些類型,例如函式,每個實體有不同的類型,使用時只需要用特徵的功能。這時 dyn Trait
語法使用兩個指標指向實體與其特徵的函式,可以在需要類型名稱的情況下傳遞與儲存,例如函式參數的類型與結構體欄位的類型,能用參考指標 &'a dyn Trait
(生命週期 'a
)或自擁有的指標 Box<dyn Trait>
(靜態生命週期)。
Box<dyn Trait>
較為泛用,這樣實體能跟隨自擁有指標Box
移動且方便修改,除非實體本身不能移動(會用Pin<&mut dyn Trait>
)。dyn Trait + 'a
語法代表實體本身帶有生命週期變數,例如閉包函數捕捉了外部變數。實體的生命週期與攜帶的生命週期不同,從語法上可得知&'a dyn Trait + 'b
,設計上應盡量減少生命週期限制,使用靜態生命週期'static
。
跟 Slice 一樣,動態特徵 dyn Trait
是無大小類型 !Sized
。
C++ | Rust | |
---|---|---|
單執行緒 RC | std::shared_ptr<T> |
std::rc::Rc<T> |
跨執行緒 RC | std::atomic<T> |
std::sync::Arc<T> /std::sync::atomic::Atomic* |
單執行緒鎖 | 和跨執行緒共用 | std::cell::Cell<T: Copy> /std::cell::RefCell<T: Clone> |
跨執行緒鎖 | std::mutex |
std::sync::Mutex<T> |
跨執行緒讀寫鎖 | std::shared_mutex |
std::sync::RwLock<T> |
- 引用計數器 (RC) 用來在單/多執行緒間分享相同資料,當計數為 0 時釋放資源。
- 執行緒鎖用來避免資料競爭 (Data Race),但是可能有自鎖 (Dead lock) 行為。由於 RC 的內部類型通常不允許修改,因此要搭配執行緒鎖來保證寫入時沒有衝突。
- C++ 的鎖不持有資料,並且要主動呼叫守衛 (guard) 類型來鎖定,須注意操作的對象是正確的。
- Rust 有為原始類型提供原子操作,改善記憶體和操作速度。
可以將 Python/JavaScript 等腳本語言的 RC 行為理解為
a = 10 # 新資源
b = a # 引用 +1
c = b # 引用 +1
del a # 引用 -1
b += 1 # 使用引用值運算,並改變引用到新資源
let a = Rc::new(10); // 新資源
let mut b = a.clone(); // 引用 +1
let c = b.clone(); // 引用 +1
println!("{}", Rc::strong_count(&c)); // 3
drop(a); // 引用 -1
println!("{}", Rc::strong_count(&c)); // 2
b = Rc::new(*b + 1); // 使用引用值運算,並改變引用到新資源
println!("{}", Rc::strong_count(&c)); // 1
使用結構體 struct
或是元組 (T, U, V)
包裝類型時,申請記憶體必須以「最大連續記憶體」為倍數,剩下的空間會僅用做填充。舉例來說,(u8, u16)
的最大連續區塊是 u16
,所以從 2 bytes 為基準,申請的空間為 4,而非單純兩者相加的 3 bytes。這個「最小單位」可以使用 alignof
(C/C++) 和 {std, core}::mem::align_of
(Rust) 查詢。對齊的最大單位與平台位元數相同,可從 usize
的大小得知。
struct A { char b; short i; };
alignof(struct A) // 2
std::mem::align_of::<(u8, u16)>() // 2
看起來像這樣:(若有兩個 u8
放前面就不會浪費空間了!)
1 | 2 | 3 | 4 |
---|---|---|---|
u8 | u16(1) | u16(2) |
在 C/C++ 結構體的成員 (member)(在 Rust 稱為欄位 (field))的順序就是記憶體的排序,然而如果大小交錯排列,像 (bool, u64, bool, u64)
,會造成嚴重的空間浪費,所以基本上應該按類型的記憶體大小排列成員。不過在 Rust 中會自動在實作時達成,這樣撰寫時能夠以其它方式排列欄位。
// C required
#include <stdbool.h>
#include <stdalign.h>
struct A { bool b1; bool b2; long i1; long i2; };
alignof(struct A) // 8
sizeof(struct A) // 24
struct A { bool b1; long i1; bool b2; long i2; };
alignof(struct A) // 8
sizeof(struct A) // 32
std::mem::align_of::<(bool, u64, bool, u64)>() // 8
std::mem::size_of::<(bool, u64, bool, u64)>() // 24
// 強制變成 C 的布局,取消自動排列
#[repr(C)]
struct A {
a: u8,
b: u64,
c: u8,
d: u64,
}
std::mem::size_of::<A>() // 32
若為巢狀結構,外層的結構體則取最大欄位的 align_of
,但是實際大小 size_of
通常無法攤平對齊,需要仰賴編譯器最佳化(例如刪除無用的欄位)。