Rust 入门:编译器是你最严格的朋友 书架

第 04 章 / 共 12 章

第四章 · 所有权:每块内存只有一个主人

先看一段「诡异」的代码

let s1 = String::from("你好");
let s2 = s1;
println!("{}", s1);   // 编译错误!

来自任何其他语言的程序员看到第三行报错都会愣一下:我就赋了个值而已,怎么 s1 就不能用了?这一章我们就把这份「诡异」拆解开。它是 Rust 所有安全保证的地基,也是这门语言和你之前学过的每一门语言真正不同的地方。

先补一分钟硬件常识:栈与堆

程序用的内存分两块。(stack)像一摞盘子:大小固定、按顺序放取、极快,装着大小在编译期就确定的小数据(整数、布尔值)。(heap)像一个大仓库:想放多大的东西都行,但要先「申请」一块、拿到它的地址,用完了得有人「归还」。字符串的内容就存在堆里——因为它可长可短,栈上只放一张写着仓库地址和长度的「提货单」。

第一章说的那些内存事故,几乎全出在堆上:忘了归还(泄漏)、还了两次(重复释放)、还了还在用(悬垂指针)。问题归结为一句话:堆上的每块内存,到底谁负责归还?

所有权三规则

Rust 的回答,精确到只有三句话:

第三条就是 Rust 不要垃圾回收器的秘密:内存归还是编译期就排定的——编译器在主人离开作用域的那一行,自动插入了归还内存的代码(这个机制叫 Drop,大白话是「离场自动清扫」)。不用你手动 free,所以没有「忘了还」和「还两次」;不用运行时管家盯着,所以没有停顿。确定性极强:哪一行还内存,你看着代码就能指出来。

回到那段诡异代码:移动,不是复制

现在能解释开头了。let s2 = s1; 发生了什么?栈上那张「提货单」(地址+长度)被复制了一份给 s2,但仓库里的货物只有一份。如果两张提货单都有效,两个主人都离场时就会归还同一批货物两次——重复释放,正是第一章说的事故。所以 Rust 的做法是:赋值即过户s2 = s1 之后,s1 当场作废,主人换成了 s2。这个操作叫移动(move)。

类比:房子过户。把房产证交给新房主的那一刻,旧房主的证就失效了——世上绝不能同时存在两张指向同一套房子的有效房证,否则两家人都以为房子是自己的,拆迁款就得赔两次。Rust 的编译器就是那个绝不允许一房两证的登记处。

函数传参也一样过户:

fn main() {
    let s = String::from("你好");
    greet(s);                 // s 的所有权过户进了函数
    // println!("{}", s);     // 这里再用 s 就报错了
}

fn greet(msg: String) {
    println!("{}", msg);
}                             // msg 在这里离场,内存自动归还

如果你真的想要两份独立的货物,就明说:let s2 = s1.clone();clone 会把堆里的内容完整复制一份,两张提货单指向两批货,互不干扰。代价是真花了复制的开销——又一次,Rust 的风格:贵操作必须写在脸上,不许偷偷发生。

那为什么整数可以随便复制?

细心的你会发现:

let a = 5;
let b = a;
println!("{} {}", a, b);   // 好好的,没报错

因为整数整个儿就在栈上,没有「提货单+仓库」两层结构,复制它就是全量复制,便宜且无歧义。这类类型实现了 Copy 标记——大白话是「我这类东西复制起来就是顺手一抄,不存在过户问题」。整数、浮点、布尔、字符都属于此类。凡是要碰堆的(String、Vec 等),都不可能是 Copy,一律走移动。

这一章你带走了什么

一个看似苛刻的规则——「一个值一个主人,赋值即过户」——直接把内存泄漏、重复释放这两大类事故从语言层面删掉了,而且不需要任何运行时开销。但你一定已经感到了不方便:难道每次把数据交给函数用一下,就得过户、从此别见了?当然不是。下一章的「借用」就是为这个不方便设计的——而它的规矩,同样由编译器强制执行。

Rust 入门:编译器是你最严格的朋友 · 王建硕
费曼式写法 · 由多个 AI 代理撰写与互相审校