指针与 Rust:当 C 遇上所有权

说实话,我第一次接触 Rust 的时候,心里是有点抵触的。做了十几年嵌入式,C 语言的指针早就玩得滚瓜烂熟,突然让我去学一套新的内存管理规则,总觉得多此一举。但真正深入之后,我发现 Rust 的那套所有权和借用机制,其实解决了很多 C 语言里让我头疼的问题。

今天我们就来聊聊,C 语言的指针和 Rust 的引用到底有什么区别。以及,在 Rust 里怎么用 unsafe 代码操作原始指针,还有 FFI 调用时怎么传递指针。

核心观点:Rust 的所有权系统,本质上是对 C 语言指针操作的一种「安全约束」。它把运行时才能发现的内存错误,提前到了编译阶段。

所有权与借用:Rust 的「安全指针」

先说说所有权。这个概念其实不复杂——每个值在 Rust 里都有一个唯一的「所有者」。当所有者离开作用域,值就被自动释放。这跟 C 语言里 malloc/free 是一个道理,只不过 Rust 帮你自动插入了 free 的代码。

我刚开始写 Rust 时,最不习惯的就是所有权转移。在 C 里,我可以随意拷贝指针,多个地方同时指向同一块内存。但在 Rust 里,一个值只能有一个所有者。你想让别的变量也访问这块内存?要么转移所有权,要么借用。

// C 语言:多个指针指向同一块内存
int *p1 = malloc(sizeof(int));
int *p2 = p1;  // 没问题,但谁负责 free?

// Rust:所有权转移
let s1 = String::from("hello");
let s2 = s1;  // s1 的所有权转移给了 s2,s1 不再有效
// println!("{}", s1);  // 编译错误!

借用机制就更巧妙了。你可以暂时「借」用某个值,用完之后还回去。借用的规则很简单:要么同时有多个不可变引用(&T),要么只有一个可变引用(&mut T)。

我在项目中遇到过一个问题:一个嵌入式设备的数据采集模块,需要同时读取和修改缓冲区。用 C 写的时候,经常因为指针混用导致数据错乱。后来用 Rust 重写,借用规则直接在编译期就帮我排除了这些 race condition。

个人经验:如果你从 C 转 Rust,可以把所有权想象成「唯一拥有者」,把借用想象成「临时访问权限」。这样理解起来会顺很多。

引用与指针:看似相似,实则不同

Rust 的引用(&T 和 &mut T)和 C 的指针,底层实现其实是一样的——都是内存地址。但语义上差别很大。

特性 C 指针 Rust 引用
空值 可以 NULL 不可能为空(Option 处理)
算术运算 支持指针加减 不支持
生命周期 程序员自己管理 编译器检查
别名规则 无限制 严格的借用规则

说白了,Rust 引用就是「加了安全锁的指针」。它保留了指针的高效性,但通过编译期检查,杜绝了悬垂指针、空指针解引用、缓冲区溢出这些经典问题。

你想想看,在 C 里写个链表,每次插入删除都要小心翼翼,生怕指针指歪了。Rust 里用引用配合所有权,这些事编译器就帮你盯着了。

unsafe Rust:原始指针的「法外之地」

但 Rust 也不是万能的。有些场景下,你必须绕过借用规则,直接操作内存。比如:

  • 与硬件寄存器交互
  • 实现自定义的内存分配器
  • 调用 C 语言的 FFI 接口
  • 性能关键路径上的优化

这时候就需要用到 unsafe Rust 里的原始指针(*const T 和 *mut T)。它们跟 C 指针几乎一模一样:可以为空、支持算术运算、没有生命周期检查。

// unsafe Rust 中的原始指针
let mut x = 42;
let raw_ptr: *mut i32 = &mut x as *mut i32;

unsafe {
    *raw_ptr = 100;  // 通过原始指针修改值
    println!("{}", *raw_ptr);
}

警告:unsafe 代码块并不意味着「不安全」,而是「编译器无法保证安全,需要程序员自己负责」。我曾经在一个项目中,因为 unsafe 块里的指针越界,导致整个系统崩溃。调试了两天才找到问题。

我个人习惯是:能用安全 Rust 解决的问题,绝不用 unsafe。只有在明确知道自己在做什么,并且性能或功能上确实需要时,才使用原始指针。

FFI 中的指针传递:C 与 Rust 的桥梁

嵌入式开发中,经常需要调用 C 语言写的底层库。这时候 FFI(外部函数接口)就派上用场了。指针传递是 FFI 中最常见的操作。

Rust 通过 extern "C" 块声明外部函数,用 #[no_mangle] 导出函数给 C 调用。指针类型用 *const T*mut T 表示。

// Rust 端:调用 C 函数
extern "C" {
    fn c_function(ptr: *mut i32, len: i32);
}

fn call_c() {
    let mut data = [1, 2, 3, 4, 5];
    unsafe {
        c_function(data.as_mut_ptr(), data.len() as i32);
    }
}

// Rust 端:导出函数给 C 调用
#[no_mangle]
pub extern "C" fn rust_function(ptr: *mut i32, len: i32) {
    let slice = unsafe {
        std::slice::from_raw_parts_mut(ptr, len as usize)
    };
    // 现在可以安全地操作 slice 了
    for item in slice.iter_mut() {
        *item *= 2;
    }
}

这里有个坑:Rust 的引用和 C 的指针在 ABI 层面是兼容的,但语义不同。C 那边可能随时释放内存,而 Rust 这边还持有引用。我曾经在做一个传感器驱动时,C 库在回调里释放了缓冲区,但 Rust 端还在用那个指针,结果就是段错误。

避坑指南:FFI 传递指针时,一定要明确「谁负责分配内存」「谁负责释放内存」「生命周期有多长」。最好在接口文档里写清楚,或者用 Rust 的 std::mem::ManuallyDrop 来管理。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,从 C 指针到 Rust 引用,再到 unsafe 原始指针和 FFI,其实是一条「安全与灵活」的权衡线。

指针与 Rust:知识体系 C 指针 自由、危险、高效 Rust 引用 &T 安全、受控、编译期检查 所有权 + 借用规则 unsafe 原始指针 *const T / *mut T FFI 指针传递 C 与 Rust 的桥梁 安全抽象层 Slice、Box、Arc 等 安全与灵活的权衡:根据场景选择合适的指针方案

从图中可以看出,C 指针在最左边,代表「完全自由但危险」;Rust 引用在中间,代表「安全受控」;unsafe 原始指针和 FFI 则是「在安全框架下打开一扇通往底层的门」。最右边的安全抽象层,是 Rust 推荐的做法——用 Box、Arc、Slice 等封装好的类型,而不是直接操作原始指针。

嗯,说到这,我想起一个项目。当时需要把一个 C 写的网络协议栈移植到 Rust 上。最头疼的就是那些回调函数里的指针传递。C 那边传过来一个 void* 上下文,Rust 这边得小心翼翼地转成正确的类型。我最后用了一个全局的 HashMap 来管理这些指针,虽然性能有点损失,但安全性大大提升。

其实 Rust 的设计哲学很简单:在 99% 的场景下,用安全的方式解决问题。剩下的 1%,用 unsafe 明确标记出来,让代码审查的人一眼就能看到「这里可能有风险」。

总结一下:

  • 所有权和借用是 Rust 内存安全的核心,相当于给 C 指针加上了「编译期安全带」
  • 引用和指针底层一样,但语义上引用更安全、更受约束
  • unsafe 里的原始指针是必要的「逃生舱」,但要用得谨慎
  • FFI 传递指针时,生命周期和所有权必须明确约定

下一章我们会深入 Rust 的所有权系统,看看它到底是怎么在编译期保证内存安全的。到时候我会拿一个实际项目中的环形缓冲区来举例,你就能更直观地感受到 Rust 的威力了。


公众号:蓝海资料掘金营,微信deep3321