Appearance
19.2 裸指针
Rust 提供了两种裸指针供我们使用,*const T
和 *mut T
。我们可以通过*mut T
修改所指向的数据,而*const T
不能。在 unsafe 代码块中它们俩可以互相转换。
裸指针相对于其他的指针,如 Box
,&
,&mut
来说,有以下区别:
裸指针可以为空,而且编译器不保证裸指针一定指向一个合法的内存地址;
不会执行任何自动化清理工作,比如自动释放内存等;
裸指针赋值操作执行的是简单的内存浅复制,并且不存在 borrow checker 的限制。
创建裸指针是完全安全的行为,只有对裸指针执行“解引用”才是不安全的行为,必须在 unsafe 语句块中完成。
示例代码如下:
rust
fn main() {
let x = 1_i32;
let mut y : u32 = 1;
let raw_mut = &mut y as *mut u32 as *mut i32 as *mut i64; // 这是安全的
unsafe {
*raw_mut = -1; // 这是不安全的,必须在 unsafe 块中才能通过编译
}
println!("{:X} {:X}", x, y);
}
我们可以把裸指针通过as
运算符执行类型转换。转换类型之后,它就可以把它所指向的数据当成另外一个类型来操作了。原本变量y
的类型是 u32,但是我们对它取地址后,最后将指针类型转换成了 i64。此时,我们对该指针所指向的地址进行修改会发生“类型安全”问题以及“内存安全”问题。编译,执行,这段代码的执行结果为:
rust
FFFFFFFF FFFFFFFF
可见,x
原本是一个在栈上存在的不可变绑定,在我们通过裸指针对y
做了修改之后,x
的值也发生了变化。 原因就是,我们对指向y
的指针类型做了转换,让它以为自己指向的是 i64 类型,恰巧x
就在y
旁边,城门失火,殃及池鱼,x
就被顺带一起修改了。 从这个示例我们可以看到,unsafe 代码中可以做很多危险的事情。上面这个例子就是一个错误的 unsafe 用法。
再比如:
rust
fn raw_to_ref<'a>(p: *const i32) -> &'a i32 {
unsafe {
&*p
}
}
fn main() {
let p : &i32 = raw_to_ref(std::ptr::null::<i32>());
println!("{}", p);
}
编译,执行,可以看到发生了 core dump。为什么呢?因为 unsafe 代码写错了。这段代码里面直接用 unsafe 功能把一个裸指针转换为了一个共享引用,忽略了 Rust 里面共享引用必须遵循的规则。在 Rust 中,&型引用、&mut 型引用以及 Box 指针,全部要求是合法的非空指针。在 unsafe 代码中,我们必须自己从逻辑上保证这一点,否则就是不可容忍的严重 bug。(注意在 safe 代码中是没办法构造出这样的场景的。)有些初学者可能会在写 FFI,封装 C 代码的时候犯这样的错误。改正方法如下:
rust
fn raw_to_ref<'a>(p: *const i32) -> Option<&'a i32> {
if p.is_null() {
None
} else {
unsafe { Some(&*p) }
}
}
fn main() {
let p : Option<&i32> = raw_to_ref(std::ptr::null::<i32>());
println!("{:?}", p);
}
Rust 的各种指针还有一些重要约束,比如&mut 型指针最多只能同时存在一个。这些约束条件,在 unsafe 场景下是很容易被打破的,而编译器并没有能力帮我们自动检查出来。我们之所以需要 unsafe,只是因为某些代码只有在特定条件下才是安全的,而这个条件我们没有办法利用类型系统表达出来,所以这时候需要依靠我们自己来保证。
大家千万不要到处滥用 unsafe。当你不得不使用 unsafe 的时候,请一定注意,这并不意味着你就可以乱写不安全的代码,相反,它的意思是“编译器请相信我,这段代码依然是安全的,它的安全性由我自己负责”。
裸指针也有很多有用的成员方法,读者可以参考标准文档中的“Primitive Type Pointer”。比如,裸指针并不直接支持算术运算,而是提供了一系列成员方法 offset wrapping_offset 等来实现指针的偏移运算。