Skip to content

什么是裸指针

在 Rust 中,裸指针(又叫原始指针,Raw Pointer)是一种不受 Rust 借用规则保护的指针类型。它们与 C 语言中的指针类似,只是没有经过 Rust 的安全检查和 borrow checker 的限制。 尽管 Rust 强调内存安全,但引入裸指针是为了支持一些无法在完全安全的 Rust 代码中完成的底层操作,例如与 C 语言代码进行互操作(FFI),或者在需要手动管理内存,实现特定的数据结构时。使用裸指针需要特别小心,因为它们容易导致内存安全问题,如空指针、悬垂指针、非法内存访问等等。

裸指针有两种类型:*const T*mut T。前者是不可变的裸指针,后者是可变的裸指针。其中,T 是指针指向的类型,例如,*const i32 表示一个指向 i32 类型的不可变裸指针,*mut String 表示一个指向 String 类型的可变裸指针。与引用不同,裸指针:

  • 允许同时拥有多个不可变指针和可变指针指向同一数据,或者同时拥有多个可变指针。
  • 不保证指向有效的内存(可能为 null,或指向已释放的内存)。
  • 不保证指向已初始化的数据。
  • 没有自动的生命周期管理和析构。

要创建一个裸指针,最常见的方式是从一个引用转换而来。例如:

rust
fn main() {
    let x = 10;
    let ptr: *const i32 = &x as *const i32; // 创建一个指向 x 的不可变裸指针
}

在上述代码示例中,我们使用 &x 获取 x 的引用,并将其转换为 *const i32 类型,得到了一个指向 x 的不可变裸指针 ptr

需要注意的是,由于裸指针无法保证指向合法内存,因此在使用裸指针时,需要在 unsafe 块中进行操作,以告诉 Rust 编译器这是没有经过安全检查的操作。例如:

rust
fn main() {
    let x = 10;
    let ptr: *const i32 = &x as *const i32;

    unsafe {
        println!("The value of x is {}", *ptr);
    }
}

在上述代码示例中,我们使用 unsafe 块来解引用 ptr 指向的值,并将其打印输出。通过使用 unsafe 块,我们告诉 Rust 编译器这是一个不安全的操作,需要程序员自行承担安全风险和责任。

创建裸指针的更多方式

除了从引用转换,还有其他几种创建裸指针的方式:

  1. 从智能指针转换:例如,Box<T> 提供了 into_raw 方法,它会消耗 Box<T> 并返回一个裸指针,同时放弃对内存的管理。之后需要手动使用 Box::from_raw 来重新接管内存并正确释放。

    rust
    fn main() {
        let b = Box::new(5i32);
        let ptr: *mut i32 = Box::into_raw(b);
        // 此时 ptr 是一个裸指针,Box b 不再管理这块内存
        // ... 在 unsafe 块中使用 ptr ...
        unsafe {
            // 使用完毕后,需要将裸指针转换回 Box 以便 Rust 正确释放内存
            let _ = Box::from_raw(ptr);
        }
    }
  2. 创建空指针:可以使用 std::ptr::null()std::ptr::null_mut() 来创建不可变和可变的空指针。

    rust
    use std::ptr;
    
    fn main() {
        let const_null_ptr: *const i32 = ptr::null();
        let mut_null_ptr: *mut i32 = ptr::null_mut();
    
        assert!(const_null_ptr.is_null());
        assert!(mut_null_ptr.is_null());
    }

    解引用空指针是未定义行为。

  3. 从整数地址创建:可以将一个整数直接转换为裸指针。这是一种非常不安全的操作,通常只在与硬件交互或进行非常底层的编程时使用,因为无法保证该地址是有效的。

    rust
    fn main() {
        let address = 0x012345usize;
        let ptr = address as *const i32;
        // 对这个 ptr 的任何操作都极度不安全,因为我们不知道这个地址是否有效
    }

使用裸指针的常见操作

使用裸指针时,大部分操作都需要在 unsafe 块中进行:

  1. 解引用:如前所述,使用 * 操作符来访问指针指向的数据。

  2. 指针运算:可以使用 offset 方法进行指针的偏移计算。这个方法也是 unsafe 的,因为它不检查边界。

    rust
    fn main() {
        let arr = [10, 20, 30];
        let ptr: *const i32 = arr.as_ptr();
    
        unsafe {
            println!("First element: {}", *ptr);
            println!("Second element: {}", *ptr.offset(1)); // 移动到下一个元素
            println!("Third element: {}", *ptr.offset(2));  // 移动到再下一个元素
        }
    }
  3. 读写数据:可以使用 readwrite 方法从指针指向的内存位置读取或写入数据。这些方法也是 unsafe 的,因为它们不保证指针有效或对齐。

    • ptr.read(): 读取 *ptr 的值,但不会创建引用,适用于可能存在别名或未对齐的情况。
    • ptr.write(value): 将 value 写入 *ptr 指向的内存,不会调用 drop 清理旧值。
    rust
    fn main() {
        let mut x = 10;
        let ptr_mut: *mut i32 = &mut x;
    
        unsafe {
            println!("Original value: {}", ptr_mut.read()); // 读取值
            ptr_mut.write(20); // 写入新值
            println!("New value: {}", *ptr_mut); // 通过解引用读取
        }
    }
  4. 与 C 代码交互 (FFI):裸指针在与 C 语言或其他语言编写的库进行交互时非常关键,因为这些语言通常使用指针来传递数据。

在 Rust 中 一个指针所指向的内容与它本身的值有什么区别?

在 Rust 中,一个指针包含两个部分:指向的内容和指针本身的值。指针本身的值表示指针所指向的内容在内存中的地址,而指向的内容是存储在该地址上的数据。

指针本身的值通常是一个整数,用于表示内存地址。在 Rust 中,指针有两种类型:裸指针(raw pointer)和引用(reference)。 引用是 Rust 中更加安全和常用的指针类型,因为它们受到语言的安全检查和借用规则的保护,避免了悬垂指针和内存不安全的问题。

当我们在 Rust 中定义一个变量时,比如 let x = 10;,它会被存储在内存中的某个地址上。这个地址也就是 x 的值。如果我们想要访问这个值,就需要使用指针或引用。

下面是一个使用引用的示例:

假设我们有一个指向 x 的裸指针 ptr,它将指向 x 存储在内存中的地址。我们可以通过解引用 *ptr 来获取指针所指向的内容,即 x 的值。下面是一个简单的示例:

rust
fn main() {
    let x = 10;
    let ptr = &x as *const i32;  // 定义一个裸指针,指向 x 所在的内存地址

    println!("ptr value: {:?}", ptr);
    println!("ptr points to value: {:?}", unsafe { *ptr });
}

在上面的代码中,我们定义了变量 x 并将其初始化为整数10。然后,我们通过 &x 获取了一个指向 x 的引用,并将其转换为一个裸指针。最后,我们输出了裸指针的值和指针所指向的内容(对裸指针的操作都是不安全的操作,需要使用unsafe块)。

对于引用,我们可以像这样定义一个指向 x 的引用 ref_xlet ref_x = &x;与裸指针不同,引用受到 Rust 的安全检查和借用规则的保护,避免了悬垂指针和内存不安全的问题。 我们也可以通过解引用 *ref_x 来获取引用所指向的内容,即 x 的值。

下面是一个使用引用的示例代码:

rust
fn main() {
    let x = 10;         // 定义一个变量 x
    let ref_x = &x;     // 定义一个指向 x 的引用 ref_x

    println!("ref_x value: {:p}", ref_x);                // 输出引用 ref_x 的值,即内存地址
    println!("ref_x points to value: {:?}", *ref_x);     // 输出引用 ref_x 指向的内容,即 x 的值
}

在上面的例子中,变量ref_x是一个指向x的引用(即:一个指向x变量的指针),它的值就是 x 在内存中的地址。可以通过 *ref_x 来访问 ref_x 指向的内容,即变量 x 的值。

需要注意的是,由于 Rust 的所有权机制,一旦一个变量的所有权被转移了,它所对应的内存区域就会被释放,因此它的指针也就失效了。 因此,在 Rust 中需要非常注意指针的安全性和生命周期。如果想要在多个地方共享对数据的访问,可以使用引用(borrowing)。如果确实需要共享数据的所有权,可以使用像 Rc<T> (引用计数指针) 或 Arc<T> (原子引用计数指针,用于多线程) 这样的智能指针类型,而不是直接传递裸指针或试图通过简单引用转移所有权。

使用裸指针的规则和最佳实践

尽管裸指针提供了更大的灵活性,但也带来了巨大的风险。在使用它们时,应遵循一些基本规则:

  1. 最小化 unsafe 代码:将 unsafe 块限制在绝对必要且尽可能小的范围内。将不安全的操作封装在安全的抽象之后是一个好主意。
  2. 程序员的责任:编译器不会对裸指针的使用进行安全检查。因此,程序员必须自己确保:
    • 指针指向的是有效的、已初始化的内存。
    • 对于 *mut T,在写入时没有其他指针(无论是 *const T 还是 *mut T)同时访问或写入同一块内存(类似于 &mut T 的别名规则,但不由编译器强制)。*const T 则可以有多个别名。
    • 指针的生命周期是正确的,避免悬垂指针。
  3. 文档化不安全代码的假设:如果编写了 unsafe 代码,务必清晰地文档化其依赖的假设和不变量,以便他人(或未来的你)能够理解其正确性。

裸指针是 Rust 中必要的“后门”,允许开发者在需要时绕过编译器的安全检查,以实现一些底层操作或与其他语言交互。然而,它们的使用应该非常谨慎,并且尽可能地被安全的抽象所封装。

指针与借用的理解

指针和借用是 Rust 中非常核心且关键的概念,它们共同构成了 Rust 内存安全保证的基石。

指针 (Pointers)

  • 核心概念:指针本质上是一个存储了内存地址的变量。这个地址指向内存中另一块数据的位置。
  • Rust 中的类型
    • 引用 (References): &T (不可变引用) 和 &mut T (可变引用)。它们是 Rust 中最常用的指针类型,是安全的,受到借用检查器的严格管理。引用总是指向有效的、已初始化的内存。
    • 裸指针 (Raw Pointers): *const T (不可变裸指针) 和 *mut T (可变裸指针)。它们更接近 C/C++ 中的指针,不直接受借用检查器的管理,使用它们需要 unsafe 代码块。裸指针可以为 null,可以指向无效内存,也可能存在数据竞争的风险。

借用 (Borrowing)

  • 核心概念:借用是 Rust 允许代码访问数据而不获取其所有权的机制。当你创建一个引用时,你就是在“借用”数据。
  • 借用规则 (由借用检查器强制执行)
    1. 一个可变引用或任意数量的不可变引用:在任何给定时间,你要么只能有一个对特定数据的可变引用 (&mut T),要么可以有任意数量的不可变引用 (&T),但不能同时拥有两者。
    2. 引用必须始终有效:引用指向的数据在其生命周期内必须保持有效。Rust 通过生命周期系统来确保这一点,防止悬垂引用(dangling references)。

指针与借用的关系和区别

  1. 安全性

    • 借用 (通过引用实现):是 Rust 安全性的核心。编译器在编译时通过借用检查器强制执行借用规则,从而在编译阶段就消除许多常见的内存安全问题(如数据竞争、悬垂指针)。
    • 裸指针:本质上是 unsafe 的。编译器不会为裸指针提供相同的安全保证。程序员必须自己负责确保裸指针的有效性和正确使用。
  2. 所有权

    • 借用:不转移所有权。数据的所有者仍然是原始变量。
    • 裸指针:可以指向有所有权的数据(例如通过引用转换而来),也可以指向没有明确所有权概念的内存区域(例如通过 FFI 从 C 代码接收的指针,或手动分配的内存)。
  3. 可变性规则

    • 借用:严格执行“一个可变引用或多个不可变引用”的规则。
    • 裸指针:允许同时存在多个指向同一数据的 *mut T*const T。这意味着如果通过 *mut T 修改数据,程序员需要自己保证数据的一致性和线程安全,编译器不会提供帮助。
  4. 有效性与 Null 值

    • 引用:保证总是指向有效的、已初始化的数据,并且永远不会是 null。
    • 裸指针:可以是 null (std::ptr::null()std::ptr::null_mut()),也可以指向无效或未初始化的内存。解引用一个无效的裸指针是未定义行为。
  5. 使用场景

    • 借用 (引用):是 Rust 日常编程中传递和访问数据的标准、安全方式。
    • 裸指针
      • 与 C 语言或其他不支持 Rust 所有权和借用模型的语言进行互操作 (FFI)。
      • 构建不安全的代码抽象,例如实现某些特定的数据结构(如 Vec<T> 内部就使用了裸指针进行内存管理)。
      • 进行非常底层的系统编程,直接与硬件或操作系统交互。

总结来说

  • 可以将引用看作是 Rust 提供的“受管理的、安全的指针”。它们是实现借用机制的主要工具。
  • 裸指针则是更原始、更自由的指针,它们提供了 C/C++ 级别对内存的直接控制能力,但同时也放弃了 Rust 编译器的许多安全保障,需要开发者承担更多的安全责任。
  • 在安全 Rust 代码中,你几乎总是会使用引用和借用。只有在确实需要绕过 Rust 的安全保证,并且你确信能够手动管理内存安全时,才应该诉诸 unsafe 代码和裸指针。

大白话理解指针与借用

想象一下我们不是在编程,而是在管理一些真实世界的物品,比如一本书。

指针是什么?

指针,简单来说,就是一张“便签”,上面写着“书在哪里”。

  1. 引用 (&Book, &mut Book) - “官方借书卡”

    • 这就像是图书馆发给你的正式“借书卡”。图书管理员(Rust 编译器)会确保这张卡上写的书名和位置都是准确的,而且书也确实在那个架子上。
    • &Book(不可变引用):你拿到的是一张“阅览卡”,你可以去看这本书,但不能在上面写字或撕页。很多人可以同时用“阅览卡”看同一本书。
    • &mut Book(可变引用):你拿到的是一张“编辑卡”,你可以修改这本书(比如做笔记)。但为了避免混乱,当有人持有“编辑卡”时,其他人连“阅览卡”都不能用,必须等这个人用完归还。
    • 这种“借书卡”非常安全,因为图书管理员一直在检查,不会让你拿到一张指向不存在的书的卡。
  2. 裸指针 (*const Book, *mut Book) - “神秘的纸条”

    • 这更像是一张不知道从哪里来的“神秘纸条”,上面也写着一个书的位置。
    • *const Book:纸条上说“某地有本书,可以看看”。
    • *mut Book:纸条上说“某地有本书,也许可以改改”。
    • 问题在于,这张“神秘纸条”上的信息可能是过时的(书已经被移走了),可能是错误的(地址写错了),甚至可能指向一个空书架。如果你完全相信这张纸条去找书或修改书(这在 Rust 里对应 unsafe 操作),风险得自己承担。图书管理员不会帮你检查这张纸条的真伪。
    • 所以,只有在你非常清楚这张“神秘纸条”的来源和可靠性时,才会去用它。

借用是什么?

“借用”就是你拿着上面说的“官方借书卡”去使用书的过程。你并没有把书买下来(没有获得所有权),书还是图书馆的,你只是暂时用一下。

Rust 的核心规则(借用规则)就像图书馆的规定:

  • 规则一:要么多人看,要么一人改。
    • 你可以有很多张“阅览卡” (&Book) 同时发出去,大家一起看书。
    • 或者,你只能发出去一张“编辑卡” (&mut Book),这个人可以修改书。此时,不能有任何“阅览卡”在外面。
  • 规则二:借了就得能用,不能借到空气。
    • 你借书的时候,书肯定是在的。在你还书之前,图书馆保证这本书不会突然消失(引用在其生命周期内必须有效)。

如果用图来表示(想象一下):

假设有一块蛋糕(这是你的数据)。

  • 所有者:是你,你拥有这块蛋糕。

  • 不可变借用 (&Cake)

    • 你允许几个朋友你的蛋糕。你给他们每人一张“观察许可证”(不可变引用)。他们可以欣赏蛋糕,讨论它多漂亮,但谁也不能动手去叉一块吃。
    • 场景:几个人围着蛋糕,指指点点,赞不绝口。
  • 可变借用 (&mut Cake)

    • 你允许一个朋友来装饰这块蛋糕(比如在上面加点水果)。你给了他唯一的一张“操作许可证”(可变引用)。在他装饰的时候,为了避免他受到打扰或者其他人误操作,其他所有人都不能靠近蛋糕,连看都不行。
    • 场景:一个人小心翼翼地在蛋糕上放草莓,其他人在旁边等着。
  • 裸指针 (*const Cake)

    • 有张模糊的地图碎片,上面画着“可能这里有蛋糕”。你(在 unsafe 模式下)决定按图索骥。可能真的找到了蛋糕,也可能找到的是个空盘子,或者地图是错的,你找到的是别人的饼干。

希望这些大白话和比喻能帮助你更好地理解 Rust 中的指针和借用!它们是 Rust 保证内存安全的关键机制,虽然初看有些复杂,但理解后会发现其设计的巧妙之处。

Released under the MIT License