2.4 移动语义

04-移动语义 — Comprehensive Rust

译文 · 基于 Comprehensive Rust

原文链接: https://google.github.io/comprehensive-rust/memory-management/move.html

2.4 移动语义

赋值会在变量之间转移_所有权_(ownership):

1
2
3
4
5
6
7
8
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
fn main() {
    let s1 = String::from("Hello!");
    let s2 = s1;
    dbg!(s2);
    // dbg!(s1);
}
  • 把 s1 赋给 s2 会转移所有权。
  • 当 s1 离开作用域时什么也不发生:它已不再拥有任何东西。
  • 当 s2 离开作用域时,字符串数据被释放。

移动到 s2 之前:

 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - - - - - - - - -.
:                           :     :                                     :
:    s1                     :     :                                     :
:   +-----------+-------+   :     :   +----+----+----+----+----+----+   :
:   | ptr       |   o---+---+-----+-->| H  | e  | l  | l  | o  | !  |   :
:   | len       |     6 |   :     :   +----+----+----+----+----+----+   :
:   | capacity  |     6 |   :     :                                     :
:   +-----------+-------+   :     :                                     :
:                           :     `- - - - - - - - - - - - - - - - - - -'
:                           :
`- - - - - - - - - - - - - -'

移动到 s2 之后:

 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - - - - - - - - -.
:                           :     :                                     :
:    s1 "(inaccessible)"    :     :                                     :
:   +-----------+-------+   :     :   +----+----+----+----+----+----+   :
:   | ptr       |   o---+---+--+--+-->| H  | e  | l  | l  | o  | !  |   :
:   | len       |     6 |   :  |  :   +----+----+----+----+----+----+   :
:   | capacity  |     6 |   :  |  :                                     :
:   +-----------+-------+   :  |  :                                     :
:                           :  |  `- - - - - - - - - - - - - - - - - - -'
:    s2                     :  |
:   +-----------+-------+   :  |
:   | ptr       |   o---+---+--'
:   | len       |     6 |   :
:   | capacity  |     6 |   :
:   +-----------+-------+   :
:                           :
`- - - - - - - - - - - - - -'

把值传给函数时,该值会赋给函数参数,从而转移所有权:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
fn say_hello(name: String) {
    println!("Hello {name}")
}

fn main() {
    let name = String::from("Alice");
    say_hello(name);
    // say_hello(name);
}
  • 可以指出:这与 C++ 的默认相反——C++ 默认按值拷贝,除非使用 std::move(且定义了移动构造函数)。

  • 移动的只是所有权。是否生成机器码去搬动数据本身属于优化问题,这类拷贝会被积极优化掉。

  • 简单值(如整数)可以标记为 Copy(见后续幻灯片)。

  • 在 Rust 中,克隆是显式的(使用 clone)。

在 say_hello 示例中:

  • 第一次调用 say_hello 时,main 放弃了 name 的所有权。之后在 main 中不能再使用 name。
  • 为 name 分配的堆内存会在 say_hello 函数结束时释放。
  • 若 main 传入 name 的引用(&name),且 say_hello 接受引用参数,则 main 可保留所有权。
  • 或者,main 可在第一次调用时传入 name 的克隆(name.clone())。
  • Rust 把移动语义设为默认,并强制程序员显式克隆,从而比 C++ 更难无意中产生拷贝。

延伸阅读

现代 C++ 中的防御性拷贝

现代 C++ 用不同方式解决这一问题:

1
2
std::string s1 = "Cpp";
std::string s2 = s1;  // Duplicate the data in s1.
  • s1 的堆数据被复制,s2 得到独立的一份拷贝。
  • 当 s1 与 s2 离开作用域时,各自释放自己的内存。

拷贝赋值之前:

 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - -.
:                           :     :                       :
:    s1                     :     :                       :
:   +-----------+-------+   :     :   +----+----+----+    :
:   | ptr       |   o---+---+--+--+-->| C  | p  | p  |    :
:   | len       |     3 |   :     :   +----+----+----+    :
:   | capacity  |     3 |   :     :                       :
:   +-----------+-------+   :     :                       :
:                           :     `- - - - - - - - - - - -'
`- - - - - - - - - - - - - -'

拷贝赋值之后:

 Stack                             Heap
.- - - - - - - - - - - - - -.     .- - - - - - - - - - - -.
:                           :     :                       :
:    s1                     :     :                       :
:   +-----------+-------+   :     :   +----+----+----+    :
:   | ptr       |   o---+---+--+--+-->| C  | p  | p  |    :
:   | len       |     3 |   :     :   +----+----+----+    :
:   | capacity  |     3 |   :     :                       :
:   +-----------+-------+   :     :                       :
:                           :     :                       :
:    s2                     :     :                       :
:   +-----------+-------+   :     :   +----+----+----+    :
:   | ptr       |   o---+---+-----+-->| C  | p  | p  |    :
:   | len       |     3 |   :     :   +----+----+----+    :
:   | capacity  |     3 |   :     :                       :
:   +-----------+-------+   :     :                       :
:                           :     `- - - - - - - - - - - -'
`- - - - - - - - - - - - - -'

要点:

  • C++ 与 Rust 做了略有不同的选择。因为 = 会拷贝数据,字符串数据必须被克隆,否则任一字符串离开作用域时会出现 double-free。

  • C++ 也有 std::move,用于标明何时可以从一个值移动。若写成 s2 = std::move(s1),则不会发生堆分配。移动后 s1 处于有效但未指定的状态。与 Rust 不同,程序员仍允许继续使用 s1。

  • 与 Rust 不同,C++ 中的 = 可以根据被拷贝或移动的类型运行任意代码。

最后修改 August 11, 2026: 更新 (70a5af133)