【Rust入门练中学】第4课:函数与所有权入门

🦀 目标:

掌握函数定义、参数、返回值、表达式与语句的区别,理解所有权的三条规则与 move 语义。


一、核心概念

  1. 函数定义
rust 复制代码
fn add(a: i32, b: i32) -> i32 {
    a + b
}

要点:

· fn 定义函数,参数必须标注类型

· -> 标注返回类型

· 函数体最后一个表达式就是返回值,不加分号

· 加了分号就变成语句,返回 (),类型不匹配会报错

调用:

rust 复制代码
fn main() {
    let result = add(3, 5);
    println!("{}", result);  // 8
}
  1. 表达式 vs 语句

这是 Rust 的核心概念之一。

语句:执行一个动作,不返回值。以分号结尾。

表达式:计算出一个值。不加分号。

rust 复制代码
fn main() {
    let x = 5;        // 语句

    let y = {
        let z = 3;
        z + 1         // 表达式,块的值是 4
    };                // 整个 let 是语句

    println!("{}", y);  // 4
}

注意 z + 1 没有分号,它是块的返回值。如果写成 z + 1;,块返回 (),y 就是 ()。

  1. 所有权的三条规则

这是 Rust 最核心、最独特的概念。

规则一:每个值都有一个所有者(owner)。

规则二:同一时刻,只有一个所有者。

规则三:所有者离开作用域时,值被自动释放(drop)。

rust 复制代码
fn main() {
    let s = String::from("hello");  // s 是所有者
    println!("{}", s);
}   // s 离开作用域,内存自动释放
  1. 移动(move)

当把一个堆上的值赋给另一个变量,或传给函数时,所有权会移动。

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;              // 所有权从 s1 移动到 s2
    // println!("{}", s1);    // ❌ s1 已失效
    println!("{}", s2);       // ✅
}

为什么?因为 String 在堆上分配内存。如果 s1 和 s2 都指向同一块内存,离开作用域时会被释放两次(double free)。Rust 直接让 s1 失效,避免这个问题。

  1. 克隆(clone)

想保留两份独立数据,用 .clone():

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();      // 深拷贝,堆上数据复制一份
    println!("{} {}", s1, s2); // 两个都可用
}
  1. 栈上类型:Copy

简单类型(整数、浮点、布尔、char、以及只含这些的元组)实现了 Copy trait,赋值时是复制而不是移动:

rust 复制代码
fn main() {
    let x = 5;
    let y = x;         // 复制,x 仍然可用
    println!("{} {}", x, y);  // 5 5
}
  1. 所有权与函数

传参和返回都会涉及移动:

rust 复制代码
fn takes_ownership(s: String) {
    println!("{}", s);
}   // s 在这里被 drop

fn gives_ownership() -> String {
    String::from("hello")   // 返回,所有权移出
}

fn main() {
    let s = String::from("world");
    takes_ownership(s);
    // println!("{}", s);   // ❌ s 已被移动

    let s2 = gives_ownership();  // 接收所有权
    println!("{}", s2);
}

二、多元化练习题(含答案与解读)

🟢 练习 1(基础):写一个加法函数

要求:定义 fn add(a: i32, b: i32) -> i32,在 main 中调用并打印 3 + 5 的结果。
参考答案

rust 复制代码
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    println!("{}", add(3, 5));  // 8
}

解读:a + b 后面不能加分号,否则返回 (),类型不匹配会报错 expected i32, found ()。


🟢 练习 2(表达式):预测输出

rust 复制代码
fn main() {
    let x = {
        let a = 2;
        let b = 3;
        a * b
    };
    println!("{}", x);
}

参考答案

输出 6。

解读:花括号块的值是最后一个表达式 a * b,等于 6。如果写成 a * b;(带分号),块返回 (),x 就是 (),编译报错。


🟡 练习 3(找出错误):语句 vs 表达式

以下代码无法编译,请指出原因并修复。

rust 复制代码
fn square(x: i32) -> i32 {
    x * x;
}

fn main() {
    println!("{}", square(4));
}

参考答案

rust 复制代码
fn square(x: i32) -> i32 {
    x * x      // 去掉分号
}

解读:x * x; 是语句,值为 ()。函数声明返回 i32,但实际返回 (),类型不匹配。编译器的报错会提示 "help: remove this semicolon"。这是新手最常犯的错误之一。


🟡 练习 4(move 语义):预测能否编译

rust 复制代码
fn main() {
    let s1 = String::from("hi");
    let s2 = s1;
    println!("{}", s1);
}

参考答案

无法编译。

解读:let s2 = s1; 发生移动,s1 失效。报错 borrow of moved value: s1。修复方式有两种:改成 s1.clone(),或者后面不再用 s1。


🟡 练习 5(Copy vs Move):判断对错

以下两段代码,哪段能编译?

A:

rust 复制代码
fn main() {
    let x = 5;
    let y = x;
    println!("{} {}", x, y);
}

B:

rust 复制代码
fn main() {
    let s = String::from("hi");
    let t = s;
    println!("{} {}", s, t);
}

参考答案

A 能编译,B 不能。

解读:i32 实现了 Copy,赋值是复制,x 仍可用。String 没有实现 Copy,赋值是移动,s 失效。判断标准:数据是否在堆上。栈上的简单类型通常 Copy,堆上的类型通常 Move。


🔴 练习 6(函数传参):修复代码

rust 复制代码
fn print_len(s: String) {
    println!("{}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_len(s);
    println!("{}", s);   // 这里想再用 s
}

参考答案

方案一:让函数借用而不是拿走所有权(第 5 课内容):

rust 复制代码
fn print_len(s: &String) {
    println!("{}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_len(&s);
    println!("{}", s);   // ✅
}

方案二:传 clone:

rust 复制代码
fn main() {
    let s = String::from("hello");
    print_len(s.clone());
    println!("{}", s);   // ✅
}

解读:方案一更好,不复制数据。这里提前接触 & 引用,下节课会详细讲。方案二有性能开销,只适合数据小或确实需要独立副本的场景。


🔴 练习 7(综合):计算并返回所有权

要求:

· 写函数 fn make_greeting(name: &str) -> String,返回 "Hello, {name}!"

· 在 main 中调用,接收返回的 String,打印它
参考答案

rust 复制代码
fn make_greeting(name: &str) -> String {
    let mut s = String::from("Hello, ");
    s.push_str(name);
    s.push('!');
    s
}

fn main() {
    let greeting = make_greeting("Rust");
    println!("{}", greeting);  // Hello, Rust!
}

解读:函数内创建 String,最后作为表达式返回,所有权移出给 main 中的 greeting。name: &str 是借用字符串切片,不获取所有权,也不复制数据。这是 Rust 中很常见的函数签名模式。


🔵 思考题 1:为什么 Rust 需要所有权?
参考答案

为了在不引入垃圾回收(GC)的前提下保证内存安全。

其他语言的做法:C/C++ 手动管理,容易内存泄漏、double free、悬垂指针;Java/Go 有 GC,运行时开销大、有停顿。

Rust 的所有权是编译期的静态检查:每个值有唯一所有者,离开作用域自动释放,移动后旧变量失效。这样既没有 GC 的运行时开销,又避免了手动管理的错误。

解读:这是 Rust "零成本抽象"和"内存安全"两大卖点的基石。所有权是 Rust 学习曲线的核心难点,但一旦理解,后面会越来越顺。


🔵 思考题 2:clone 和 Copy 有什么区别?
参考答案

Copy 是隐式的、按位复制,通常用于栈上的简单类型。赋值、传参时自动发生,不消耗额外心智。

clone 是显式的、可能深拷贝,用于堆上数据。需要你手动调用 .clone(),明确表示"这里要复制一份"。

rust 复制代码
let x = 5;
let y = x;          // Copy,隐式

let s = String::from("hi");
let t = s.clone();  // clone,显式

解读:Rust 用这种区分让你对"昂贵操作"保持警觉。复制一个整数很便宜,可以隐式;复制一个堆上的大字符串可能很贵,必须显式写出来,提醒你这里可能有性能开销。


🔵 思考题 3:函数返回 String 会不会有性能问题?
参考答案

不会。

Rust 有返回值优化(RVO)和移动语义。返回 String 时,堆上的数据不会被复制,只是把指向堆的指针、长度、容量这"三要素"移出函数。真正的堆内存在原地不动。

rust 复制代码
fn make() -> String {
    String::from("hello")   // 堆数据不复制,只移出所有权
}

解读:在很多语言里"返回大对象"是性能陷阱,Rust 里不用太担心。编译器会尽量消除拷贝。这也是为什么 Rust 里 String 可以放心地按值返回,而不会像 C++ 老代码那样到处传指针。


三、知识点总结(第4课)

函数:fn name(param: Type) -> RetType { body },参数必须标类型。

返回值:函数体最后一个表达式即返回值,不加分号。加分号变语句,返回 ()。

表达式 vs 语句:表达式有值,语句没有。块 { } 的值是最后一个表达式。

所有权三规则:每个值有唯一所有者;同一时刻只有一个所有者;所有者离开作用域时值被释放。

移动:堆上类型赋值或传参时所有权转移,旧变量失效。

克隆:.clone() 显式深拷贝,保留两份独立数据。

Copy:栈上简单类型(整数、浮点、bool、char)赋值是复制,不移动。

函数与所有权:传参可能移动,返回时所有权移出。&str 借用不获取所有权。


四、下一课预告

第5课:引用与借用

· 引用 &T 与可变引用 &mut T

· 借用规则:同一时刻要么一个可变引用,要么多个不可变引用

· 悬垂引用与生命周期入门

· 切片 &T 与 &str


📌 本课心法:

所有权不是限制,是契约。它让编译器在编译期就知道每块内存的归宿,从而不需要 GC,也不会 double free。理解移动,就理解了 Rust 的一半。

相关推荐
sunoo-2291 小时前
【ARM嵌入式学习笔记Day5】一文打通GPIO中断全链路:从硬件引脚→GIC→汇编入口→C语言处理
arm开发·笔记·学习·中断·gic·裸机开发
右耳朵猫AI1 小时前
Rust周刊2026W38 | mold重写Rust、认证级Rust裸机、Slint 1.18发布、lint提速3133倍
后端·rust·系统编程
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(76):SEEM——从碎片检索到完整事件重建
论文阅读·人工智能·学习·开源·github
Source.Liu1 小时前
【A11】时间单位与精度设计笔记
rust
我命由我123451 小时前
Mac 操作系统 - 一些使用记录
运维·windows·学习·系统安全·运维开发·mac·学习方法
重生之小比特1 小时前
【初阶C++】stack和queue
开发语言·c++·vs·queue
爱喝水的鱼丶1 小时前
SAP-ABAP:MM 模块供应商主数据开发:字段扩展、分级管控与同步接口开发
开发语言·性能优化·接口·sap·abap·增强·经验交流
小小数媒成员1 小时前
UGUI 性能优化(详细,全,深入)
开发语言·unity·游戏引擎
大黄说说2 小时前
告别卡顿:Android UI 性能优化,布局层级过度绘制实战排查
开发语言