【Rust中级教程】1.2. 指针概览(下):原始指针及Rust里的各类指针 1.2 指针概览(下)原始指针及Rust里的各类指针1.2.1. 一点回顾上一节中我们使用了引用的例子来模拟指针但模拟的效果差了很多我们想要区分原始指针(raw pointer)和智能指针(smart pointer)在内部的区别具体想要的效果如下这个图我在上一篇文章 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函数用于获取各类型所占的内存空间以字节为单位静态变量B和C的大小和内容与上一篇文章一样a是usize类型值为42b使用了智能指针BoxT来包裹的B这时候BoxT里面值的所有权就转移到BoxT上了c是一个普通的引用每个变量都打印了地址使用取地址符号来实现每个变量也都打印了它们在内存中和所占的字节数使用std::mem::size_of函数来实现我们打印出了a、b、c、B和C但是由于类型不同其表示的意义也不一样a、B和C会打印实际存储的值b和c的“指向”行会打印它们所引用数据的地址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位的所以usize的a所占的内存是8字节b的类型是BoxT一个智能指针所以它要占16字节——两个usize类型所占的大小一个usize字段用于存储指针另一个用于存储长度c是一个普通的引用一个指针所以占8字节——一个usize类型的大小用于存储指针B是一个有10个元素的数组元素类型是u8一个u8占一字节所以10个u8就占10字节C是一个有11个元素的数组元素类型是u8一个u8占一字节所以11个u8就占11字节我们真正需要注意的是c和b所存储的指针-c存储指向C的指针输出中我们可以看到c存储的指针是0x102c8aeba而C所处的地址正是0x102c8aeba对应上了-b存储指向堆上B字节副本的指针由Box::new(B)创建。b存储的指针是0x1031f1c10但是静态变量B所在的地址是0x102c8aeb0并没有对应上是为什么呢这是因为B是放在静态内存里的而Box[u8]会在堆上另分配一块缓冲区并把数组拷过去。由于b并不指向静态的B而是指向堆上的那块分配所以地址对不上。我们再讲一个例子还是刚才的B和C两个静态变量我们在前一篇文章讲过B和C实际上是文本内容但是没有进行解码所以是以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: Cowstr; 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函数中的变量a、b和c分别是i32、String和Cowstr类型由于下面的操作需呀使用到原始指针例如可变原始指针*mut T和不可变原始指针*const T所以得写在unsafe块里第一步想要获得B的可变原始指针也就是转化为*mut u8但肯定不能直接获得所以我们得先写B获得B的引用然后再使用as *const u8转化为指向的数据为u8的不可变原始指针再使用as *mut u8转化为可变原始指针。为什么要获得可变的原始指针呢因为我们需要使用String::from_raw_parts函数把数字解码为文本。它有三个参数buf、length和capacity对应String类型这个智能指针的三个字段。buf处我们写数据的原始引用也就是b_ptrlength和capacity都写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这个C的c_char类型不可变原始指针传进去再使用to_string_lossy方法即可以得到解码后的字符串。最后我们把a、b和c都打印出来。输出a: 42, b: carrytowel, c: thanksfish打印出来的这一行是a的值是42可以直接打印b解码出来的值是carrytowelc解码出来是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块就能获得原始指针的性能。引用和原始指针的不同之处在于- 允许通过同时具有不可变和可变指针或多个指向同一位置的可变指针来忽略借用规则借用规则详见 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); }a是i64类型值是42a_ptr是a的不可变原始指针a_addr在unsafe块里涉及使用原始指针的代码需要放到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速度、与外界交互UnsafeBoxT可把任何东西都放在Box里。可接受几乎任何类型的长期存储。新的安全编程时代的主力军。将值集中存储在 Heap大小增加RcT是 Rust 的能干而睿智的簿记员。它知道谁借了什么何时借了什么。对值的共享访问大小增加运行时成本线程不安全ArcT是 Rust 的大使。它可以跨线程共享值保证这些值不会相互干扰。对值的共享访问线程安全大小增加运行时成本CellT变”态“专家具有改变不可变值的能力内部可变性与T同等大小线程不安全不能直接拿到内部值的引用RefCellT对不可变引用执行改变但有代价内部可变性可与 Rc、Arc 嵌套使用大小增加运行时成本线程不安全缺乏编译时保障CowT封闭并提供对借用数据的不可变访问并在需要修改或所有权时延迟克隆数据只读访问时避免写入大小可能会增大String可处理可变长度的文本展示了如何构建安全的抽象。动态按需增长运行时保证正确编码过度分配内存大小VecT程序最常用的存储系统它在创建和销毁值时保持拥有序。动态按需增长过度分配内存大小RawVecTVecT和其动态大小类型的基石知道如何按需给数据提供一个家。动态按需增长与内存分配器一起配合寻找空间不直接适用于你的代码UniqueT作为值的唯一所有者可保证拥有完全控制权。需要独占值的类型如String的基础不适合直接用于应用程序代码NonNullT许多标准库智能指针内部使用的非空原始指针包装对T协变可用OptionNonNullT做niche优化解引用仍不安全通常不直接用于应用程序代码