Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1.2 指针概览(下):原始指针及Rust里的各类指针

1.2.1. 一点回顾

上一节中我们使用了引用的例子来模拟指针,但模拟的效果差了很多,我们想要区分原始指针(raw pointer)和智能指针(smart pointer)在内部的区别,具体想要的效果如下:

raw pointer versus smart pointer diagram

这个图我在上一篇文章 1.1. 指针概览(上) 中有过详细解释,这里就不再作介绍。

1.2.2. Rust的引用和指针

这篇文章我们会换一个更逼真的例子,使用更复杂的类型展示指针内部的区别:

use std::mem::size_of;  
  
static B: [u8; 10] = [99, 97, 114, 114, 121, 116, 111, 119, 101, 108];  
static C: [u8; 11] = [116, 104, 97, 110, 107, 115, 102, 105, 115, 104, 0];  
  
fn main() {  
    let a: usize = 42;  
    let b: Box<[u8]> = Box::new(B);  
    let c: &[u8; 11] = &C;  
      
    println!("a (unsigned 整数)");  
    println!("地址: {}", &a);  
    println!("大小: {:?} bytes", size_of::<usize>());  
    println!("值: {:?}\n", a);  
  
    println!("b (装在Box里)");  
    println!("地址: {:p}", &b);  
    println!("大小: {:?} bytes", size_of::<Box<[u8]>>());  
    println!("指向: {:p}\n", b.as_ptr());  
  
    println!("c (C的引用)");  
    println!("地址: {:p}", &c);  
    println!("大小: {:?} bytes", size_of::<&[u8; 11]>());  
    println!("指向: {:p}\n", c);  
      
    println!("B (10 bytes的数组):");  
    println!("地址: {:p}", &B);  
    println!("大小: {:?} bytes", size_of::<[u8; 10]>());  
    println!("值: {:?}\n", B);  
  
    println!("C (11 bytes的数组):");  
    println!("地址: {:p}", &C);  
    println!("大小: {:?} bytes", size_of::<[u8; 11]>());  
    println!("值: {:?}\n", C);  
}
  • 使用了std::mem::size_of函数,用于获取各类型所占的内存空间(以字节为单位)
  • 静态变量BC的大小和内容与上一篇文章一样
  • ausize类型,值为42
  • b使用了智能指针Box<T>来包裹的B,这时候Box<T>里面值的所有权就转移到Box<T>上了
  • c是一个普通的引用
  • 每个变量都打印了地址,使用取地址符号&来实现;每个变量也都打印了它们在内存中和所占的字节数,使用std::mem::size_of函数来实现
  • 我们打印出了abcBC,但是由于类型不同其表示的意义也不一样:aBC会打印实际存储的值;bc的“指向”行会打印它们所引用数据的地址(b.as_ptr()以及对c使用{:p}

输出:

a (unsigned integer)
address: 42
size: 8 bytes
value: 42

b (inside a Box)
address: 0x16d1aa5f0
size: 16 bytes
points to: 0x1031f1c10

c (reference to C)
address: 0x16d1aa600
size: 8 bytes
points to: 0x102c8aeba

B (10-byte array):
address: 0x102c8aeb0
size: 10 bytes
value: [99, 97, 114, 114, 121, 116, 111, 119, 101, 108]

C (11-byte array):
address: 0x102c8aeba
size: 11 bytes
value: [116, 104, 97, 110, 107, 115, 102, 105, 115, 104, 0]
  • 我的电脑是64位的所以usizea所占的内存是8字节
  • b的类型是Box<T>,一个智能指针,所以它要占16字节——两个usize类型所占的大小(一个usize字段用于存储指针,另一个用于存储长度)
  • c是一个普通的引用,一个指针,所以占8字节——一个usize类型的大小(用于存储指针)
  • B是一个有10个元素的数组,元素类型是u8,一个u8占一字节,所以10个u8就占10字节
  • C是一个有11个元素的数组,元素类型是u8,一个u8占一字节,所以11个u8就占11字节

我们真正需要注意的是cb所存储的指针:

  • c存储指向C的指针,输出中我们可以看到c存储的指针是0x102c8aeba,而C所处的地址正是0x102c8aeba,对应上了
  • b存储指向堆上B字节副本的指针(由Box::new(B)创建)。b存储的指针是0x1031f1c10,但是静态变量B所在的地址是0x102c8aeb0,并没有对应上,是为什么呢? 这是因为B是放在静态内存里的,而Box<[u8]>会在堆上另分配一块缓冲区并把数组拷过去。由于b并不指向静态的B,而是指向堆上的那块分配,所以地址对不上。

我们再讲一个例子,还是刚才的BC两个静态变量,我们在前一篇文章讲过BC实际上是文本内容,但是没有进行解码,所以是以u8的格式存储在变量里的,这里我们就来实现解码操作。这样做同时还能创建啊一个与理想状态(在 1.2.1. 一点回顾 中出现的那张图)更加相似的内存地址布局:

use std::borrow::Cow;  
use std::ffi::CStr;  
use std::os::raw::c_char;  
  
static B: [u8; 10] = [99, 97, 114, 114, 121, 116, 111, 119, 101, 108];  
static C: [u8; 11] = [116, 104, 97, 110, 107, 115, 102, 105, 115, 104, 0];  
  
fn main() {  
    let a = 42;  
    let b: String;  
    let c: Cow<str>;  
      
    unsafe {  
        let b_ptr = &B as *const u8 as *mut u8;  
        b = String::from_raw_parts(b_ptr, 10, 10);  
          
        let c_ptr = &C as *const u8 as *const c_char;  
        c = CStr::from_ptr(c_ptr).to_string_lossy();  
    }  
      
    println!("a: {}, b: {}, c: {}", a, b, c);  
}
  • std::borrow::Cow是一个智能指针,Cow指的是Clone on write,意思就是需要写入时才进行克隆,平时只需要读的时候就不用了

  • std::ffi::CStr类似于C语言的字符串类型,它允许Rust读取以0结尾的字符串

  • std::os::raw::c_char是平台上 C 语言char类型的别名(常常是i8,有时是u8)。现代代码更推荐使用std::ffi::c_char

  • main函数中的变量abc分别是i32StringCow<str>类型

  • 由于下面的操作需呀使用到原始指针(例如可变原始指针*mut T和不可变原始指针*const T),所以得写在unsafe块里:

    • 第一步想要获得B的可变原始指针,也就是转化为*mut u8,但肯定不能直接获得,所以我们得先写&B获得B的引用,然后再使用as *const u8转化为指向的数据为u8的不可变原始指针,再使用as *mut u8转化为可变原始指针。

    • 为什么要获得可变的原始指针呢?因为我们需要使用String::from_raw_parts函数把数字解码为文本。它有三个参数:buflengthcapacity(对应String类型这个智能指针的三个字段)。buf处我们写数据的原始引用也就是b_ptrlengthcapacity都写10因为我们知道里面存了10个元素。这步之后B所对应的字符串就解码出来了。

    • C也进行类似的操作,不同之处在于C以元素0结尾,是C语言存储字符串的方式,所以代码与对B进行解码稍有不同:我们得获得C的不可变原始指针,类型还得从u8变到c_char(平台相关,常常是i8),所以先写&C获得引用,再写as *const u8获得原始指针,最后写as *const c_char将类型转为c_char

    • 使用CStr::from_ptr函数,把c_ptr这个Cc_char类型不可变原始指针传进去,再使用to_string_lossy方法即可以得到解码后的字符串。

  • 最后我们把abc都打印出来。

输出:

a: 42, b: carrytowel, c: thanksfish
  • 打印出来的这一行是:

    • a的值是42可以直接打印
    • b解码出来的值是carrytowel
    • c解码出来是thanksfish
  • 打印之后,当b被丢弃时进程仍会异常退出。在某些平台上你可能还会看到分配器诊断信息(例如 macOS 的 malloc: *** error for object ...: pointer being freed was not allocated);本次本地运行中 abort 没有产生额外的 stderr 文本。

这是因为String::from_raw_parts会接管一块必须由分配器分配出来的内存;这里我们却把它指向了静态数据,于是分配器会尝试释放并不属于它的内存。

1.2.3. 原始指针(raw pointer)

什么是原始指针

unsafe Rust提供了两种类似于引用的新型指针,它们叫做原始指针或者裸指针,英文是raw pointer。只有使用原始指针时才需要放到unsafe块里,因为可能会出现问题,单创建一个原始指针并不会产生问题,故不需要放到unsafe块里。

和引用类似,这种原始指针要么是可变的要么是不可变的:

  • 可变的:*mut T
  • 不可变的:*const T,意味着不能通过该指针修改其指向的值(除非先把它转换成*mut T)。 注意:这里面的*是类型的一部分不代表解引用,*const T这三个标记放在一起才是一个类型,比如*const String

*const T*mut T的差别很小,可以相互自由的转换。Rust的引用(不论是&mut T还是&T)在编译阶段都会被编译器转为原始指针,这意味着无需进入unsafe块就能获得原始指针的性能

引用和原始指针的不同之处在于:

  • 允许通过同时具有不可变和可变指针多个指向同一位置的可变指针来忽略借用规则(借用规则详见 【Rust指南】4.4. 引用与借用
  • 原始指针无法保证能指向合理的内存,而引用可以。
  • 原始指针允许为null
  • 原始指针不实现任何自动清理功能

我们看一个转化为原始指针的简单例子(刚才的例子稍微又些复杂):

fn main(){
	let a: i64 = 42;
	let a_ptr: *const i64 = &a as *const i64;

	println!("a: {}({:p})", a, a_ptr);
}

输出:

a: 42(0x16f30a620)

解引用(dereference)

解引用指的是指针从RAM内存提取数据的过程叫做对指针进行解引用(dereferencing a pointer)

我们再看一个把引用转化为原始指针的例子:

fn main() {  
    let a: i64 = 42;  
    let a_ptr: *const i64 = &a as *const i64;  
    let a_addr: usize = unsafe { std::mem::transmute(a_ptr) };  
      
    println!("a: {}({:p}..0x{:x})", a, a_ptr, a_addr + 7);  
}
  • ai64类型,值是42
  • a_ptra的不可变原始指针
  • a_addrunsafe块里(涉及使用原始指针的代码需要放到unsafe块里)使用std::mem::transmute函数把a_ptr转化为usize类型
  • 输出时先输出a的值,再输出指向a的原始指针,最后输出addr + 7的值

输出:

a: 42(0x16d9ea608..0x16d9ea60f)

关于原始指针的一些提醒

  • 在底层,引用(&mut T&T)最终会被实现为原始指针。但引用带有额外的保障,应该始终作为首选项使用。
  • 访问原始指针的值总是不安全的
  • 原始指针不拥有值的所有权,在访问时编译器不会检查数据的合法性
  • Rust允许多个原始指针指向同一数据,但无法保证共享数据的合法性

使用原始指针的情况

有的时候原始指针不得不被使用,比如:

  • 某些系统或第三方库需要使用,例如与C交互
  • 共享对某些内容的访问至关重要,运行时性能要求高

1.2.4. Rust指针生态

  • 原始指针是unsafe的
  • 智能指针倾向于包装原始指针,附加更多的能力(语义)。也就是不仅仅是对内存地址解引用,还有其它能力,具体如下:
名称简介强项弱项
原始指针Raw Pointer*mut T*const T,自由基,闪电般快,极其 unsafe速度、与外界交互Unsafe
Box<T>可把任何东西都放在 Box 里。可接受几乎任何类型的长期存储。新的安全编程时代的主力军。将值集中存储在 Heap大小增加
Rc<T>是 Rust 的能干而睿智的簿记员。它知道谁借了什么,何时借了什么。对值的共享访问大小增加;运行时成本;线程不安全
Arc<T>是 Rust 的大使。它可以跨线程共享值,保证这些值不会相互干扰。对值的共享访问;线程安全大小增加;运行时成本
Cell<T>变”态“专家,具有改变不可变值的能力内部可变性;与T同等大小线程不安全;不能直接拿到内部值的引用
RefCell<T>对不可变引用执行改变,但有代价内部可变性;可与 Rc、Arc 嵌套使用大小增加;运行时成本;线程不安全;缺乏编译时保障
Cow<T>封闭并提供对借用数据的不可变访问,并在需要修改或所有权时延迟克隆数据只读访问时避免写入         大小可能会增大             
String可处理可变长度的文本,展示了如何构建安全的抽象。                     动态按需增长;运行时保证正确编码过度分配内存大小          
Vec<T>程序最常用的存储系统;它在创建和销毁值时保持拥有序。                 动态按需增长               过度分配内存大小          
RawVec<T>Vec<T> 和其动态大小类型的基石;知道如何按需给数据提供一个家。     动态按需增长;与内存分配器一起配合寻找空间不直接适用于你的代码
Unique<T>作为值的唯一所有者,可保证拥有完全控制权。                           需要独占值的类型(如String)的基础不适合直接用于应用程序代码
NonNull<T>许多标准库智能指针内部使用的非空原始指针包装T协变;可用Option<NonNull<T>>做niche优化解引用仍不安全;通常不直接用于应用程序代码