学习网站:
【官方文档】[https://doc.rust-lang.org/book/title-page.html]
【HOH共学营】[https://github.com/gpteth/lets-rust/blob/main/tutorial/02_memory_management.md]
开始
先搭建环境,太久远了,这里不赘述了,可去官网下载。
以下命令最好在GitBash里面执行。
创建新项目:
有可能:
编译并运行:
基础编程概念
关于变量
一旦将值绑定到名称,就无法更改该值。
1 2 3 4 5 6 7
| fn main() { let x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); }
|
可以添加mut关键字,表示“可变”。
1 2 3 4 5 6
| fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); }
|
Rust 的常量声明使用const关键字,命名约定是全部大写。
Rust 允许使用相同名称的声明。
1 2 3 4 5 6 7 8 9 10
| fn main() { let x = 5; let x = x + 1;
{ let x = x * 2; println!("内部作用域中 x 的值是: {}", x); } println!("外部作用域中 x 的值是: {}", x); }
|
数据类型
Rust 是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。
标量类型
标量类型表示单个值。Rust 有四种主要的标量类型:整数、浮点数、布尔值和字符。(整数类型默认为i32。)
Rust 的浮点类型是f32和f64。(默认类型是 f64)
1 2 3 4 5
| fn main() { let x = 2.0;
let y: f32 = 3.0; }
|
如果你尝试将该变量更改为该范围之外的值,就会发生整数溢出。如果发生这种情况,程序会在运行时崩溃。
Rust 的char类型是该语言最原始的字母类型。它可以表示的内容远不止 ASCII。重音字母、中文、日文和韩文字符、表情符号以及零宽度空格char在 Rust 中都是有效值。
1 2 3 4 5
| fn main() { let c = 'z'; let z: char = 'ℤ'; let heart_eyed_cat = '😻'; }
|
单引号指定字面值,而不是使用双引号的字符串字面值。
复合类型
元组(Tuple)可以将多个不同类型的值组合在一起
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; println!("y 的值是: {}", y); let five_hundred = tup.0; let six_point_four = tup.1; let one = tup.2; }
|
数组
1 2 3 4 5 6 7 8 9 10
| fn main(){ let a = [1,2,3,4,5]; let a:[i32;5] = [1,2,3,4,5]; let a = [3,5]; let first = a[0]; let second = a[1]; }
|
函数
Rust 是基于表达式的语言,理解语句和表达式的区别非常重要:
- 语句:执行操作但不返回值
- 表达式:计算并产生一个值
函数使用->指定返回类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| fn five() -> i32 { 5 }
fn plus_one(x: i32) -> i32 { x + 1 }
fn main() { let x = five(); let y = plus_one(5); println!("x = {}, y = {}", x, y); }
|
程序控制流
1 2 3 4 5 6 7 8 9 10 11 12 13
| fn main() { let number = 6; if number % 4 == 0 { println!("number 能被 4 整除"); } else if number % 3 == 0 { println!("number 能被 3 整除"); } else if number % 2 == 0 { println!("number 能被 2 整除"); } else { println!("number 不能被 4、3 或 2 整除"); } }
|
在let语句中使用if:
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { let condition = true; let number = if condition { 5 } else { 6 };
println!("number 的值是: {}", number); }
|
代码块的计算结果取决于其中最后一个表达式,数字本身也是一种表达式。在这种情况下,整个if表达式的值取决于执行哪个代码块。这意味着每个分支可能返回的值必须是同一类型。
循环
Rust 有三种循环:loop、while和for。
◆loop关键字告诉Rust永远重复执行一个代码块,或者直到你明确告诉它停止。
1 2 3 4 5 6 7 8 9 10 11 12 13
| fn main() { let mut counter = 0;
let result = loop { counter += 1;
if counter == 10 { break counter * 2; } };
println!("The result is {result}"); }
|
双层循环:
'counting_up是外层循环的名称,可以自己取其他名字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10;
loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; }
count += 1; } println!("End count = {count}"); }
|
运行结果:
1 2 3 4 5 6 7 8 9
| count = 0 remaining = 10 remaining = 9 count = 1 remaining = 10 remaining = 9 count = 2 remaining = 10 End count = 2
|
1 2 3 4 5 6 7 8 9 10
| fn main() { let mut number = 3; while number != 0 { println!("{}!", number); number -= 1; } println!("发射!"); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("值是: {}", element); } for number in (1..4).rev() { println!("{}!", number); } println!("发射!"); for xixi in arr.iter().rev(){ println!("反转迭代{xixi}"); } }
|
(1..4)创建了一个从 1 开始到 4 之前结束的范围(不包含 4);
.rev() 方法将这个范围反转,变成 3、2、1 的顺序;
for number in ... 是一个循环,依次取出范围中的每个数字并赋值给变量 number;
运行结果:
1 2 3 4 5 6 7 8 9
| 值是: 10 值是: 20 值是: 30 值是: 40 值是: 50 3! 2! 1! 发射!
|
程序与内存管理
程序的组成
一个 Rust 程序在运行时,内存主要分为以下几个区域:
- 代码段(Text Segment):存储程序的机器码
- 数据段(Data Segment):存储全局变量和静态变量
- 栈(Stack):存储函数调用信息和局部变量
- 堆(Heap):动态分配的内存区域
1 2 3 4 5 6 7 8 9 10 11 12 13
| static GLOBAL_COUNTER: i32 = 0; const MAX_SIZE: usize = 100;
fn main() { let x = 42; let y = "hello"; let v = vec![1, 2, 3, 4, 5]; let s = String::from("world"); }
|
程序执行流程
- 程序启动:操作系统加载程序,分配内存空间
- 初始化:设置栈指针,初始化全局变量
- 执行 main 函数:程序从 main 函数开始执行
- 函数调用:通过栈来管理函数调用和返回
- 程序结束:清理资源,返回操作系统
栈与堆
栈(Stack)
栈是一种后进先出(LIFO)的数据结构,具有以下特点:
- 快速:分配和释放都很快,只需移动栈指针
- 有序:数据按照固定顺序存储
- 大小限制:栈空间有限(通常几 MB)
- 自动管理:函数结束时自动清理
堆(Heap)
堆是用于动态内存分配的区域,特点如下:
- 灵活:可以存储大小可变的数据
- 较慢:分配需要搜索可用空间
- 无序:数据可能分散存储
- 手动管理:需要明确的分配和释放(Rust 通过所有权系统自动管理)
对比示例
这里可以去看:关于所有权 -> 借用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| fn main() { let x = 5; let y = x; let s1 = String::from("hello"); let s2 = s1; let s3 = String::from("world"); let s4 = s3.clone(); println!("s4 = {}", s4); println!("s3 = {}", s3); }
|
指针类型
引用是 Rust 中最常见的指针类型,使用&符号。
Rust中的原始指针(Raw Pointers) 是对标C/C++裸指针的底层特性,核心是绕开Rust的所有权/借用规则,直接操作内存地址 —— 但代价是失去Rust编译器的安全检查,属于 “不安全” 特性,仅用于底层开发、FFI(外部函数接口)或性能极致优化场景。
Rust 提供两种原始指针类型,语法上以*开头(区别于引用&):
| 指针类型 |
语法 |
含义 |
类比 C/C++ |
| 不可变原始指针 |
*const T |
只读指针,不能修改指向的值 |
const T* |
| 可变原始指针 |
*mut T |
可写指针,能修改指向的值 |
T* |
◆无所有权/借用检查:可同时存在多个*mut T指针指向同一内存,也可出现悬垂指针; |
|
|
|
◆无自动解引用:需手动解引用(*ptr),且解引用必须在unsafe块中; |
|
|
|
◆可空:原始指针可以指向null(Rust引用 &T 绝对不能为null)。 |
|
|
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| fn main() { let mut num = 42;
let ptr_const: *const i32 = # let ptr_mut: *mut i32 = &mut num;
unsafe { println!("*ptr_const = {}", *ptr_const); *ptr_mut = 100; println!("*ptr_mut = {}", *ptr_mut); } }
|
原始指针是不安全的,需要在unsafe块中使用。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { let x = 5; let raw_ptr = &x as *const i32; let mut y = 10; let raw_mut_ptr = &mut y as *mut i32; unsafe{ println!("*raw_ptr = {}", *raw_ptr); *raw_mut_ptr = 20; println!("y = {}", y); println!("*raw_mut_ptr = {}", *raw_mut_ptr); } }
|
关于所有权
概述
- 每个值在 Rust 中都有一个唯一的所有者(Owner);
- 所有者离开自己的作用域(Scope)时,这个值会被自动销毁(调用
drop 函数释放内存);
- 一个值同一时间,要么被多次只读借用(&T),要么被一次可变借用(&mut T)。
所有者与所有域
作用域就是值的生命周期范围,所有者是持有这个值的变量,生命周期结束 → 所有者消失 → 值被销毁。
1 2 3 4 5 6
| fn main() { let s = String::from("Hello, owner!"); println!("{s}"); }
|
对比其他语言):
◆Java:靠GC后台扫描释放内存,运行期有开销;
◆C++:需手动写 delete,容易遗漏(内存泄漏)或重复删除(double free);
◆Rust:编译期绑定所有者-作用域,自动释放,无开销、无风险。
所有权的转移:
非copy值在赋值 / 传参时会转移所有权,原所有者会失效,无法再使用(避免 double free)。
那么哪些是非copy值?
◆非 Copy 类型(复杂值,栈存指针、堆存数据):String、Vec、自定义结构体等。
◆Copy 类型(简单值,存在栈上):整数(i32)、bool、浮点数、字符(char)等。
为什么要转移所有权???
因为像String这种数据存在堆上,栈上只存一个指向数据的指针。如果不转移所有权,s1和s2 会同时指向堆上的同一个数据。当s1、s2都出作用域时,会两次调用drop→重复释放堆数据(double free,内存安全漏洞)。所以Rust用「转移所有权」来避免这个问题。同一时间,只有一个所有者能操作堆数据。
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { let s1 = String::from("Hello, owner!"); let s2 = s1; println!("{s2}");
let x =10; let y = x; println!("{x}, {y}"); }
|
借用
如果我们想使用某个值,但不想拿走它的所有权(比如函数传值,用完还能自己用),这是就需要借用—— 本质是临时引用。
分类:
(1)不可变借用(&T):可以有多个。(多人同时读,不冲突)
(2)可变借用(&mut T):可以修改值,但是!只能有一个可变借用。(避免多人同时修改,数据混乱)
代码示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| fn main() { let s1 = String::from("Hello, Borrow1!"); let s2 = &s1; let s3 = &s1; println!("{}, {}", s2, s3);
let mut t1 = String::from("Hello, Borrow2!"); let t2 = &mut t1; println!("{}", t2); }
|
代码示例2:函数传参。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| fn take_ownership(s: String) { println!("{}", s); }
fn borrow_ownership(s: &String) { println!("{}", s); }
fn return_ownership() -> String { let s = String::from("hello"); s }
fn main() { let s1 = String::from("hello"); take_ownership(s1);
let s2 = String::from("world"); borrow_ownership(&s2); println!("{}", s2);
let s3 = return_ownership(); println!("{}", s3); }
|
补充:
1 2
| let s1 = String::from("hello"); let s2 = s1.clone();
|
上面这个方法中s1、s2的引用相同吗?
不相同。
◆String的内存结构:栈上有指向堆数据的指针、字符串长度、容量。而实际的字符串内容存储在堆上。
◆clone() 的不仅复制栈上的指针、长度、容量(浅拷贝部分),还会在堆上重新分配一块内存,复制原字符串的内容(深拷贝核心)。
切片
Rust 中的切片(Slice) 是一种无所有权的引用类型,创建切片不会转移 / 剥夺原集合的所有权,核心作用是 “指向集合(如数组、String、Vec)中连续的一段元素”,让你无需拷贝数据就能操作部分内容 —— 可以把它理解为 “带长度的引用”,而非完整的容器。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| fn main() { let arr = [1, 2, 3, 4, 5]; let slice1 = &arr[1..4]; let slice2 = &arr[..3]; let slice3 = &arr[2..]; let slice4 = &arr[..];
println!("slice1: {:?}", slice1); println!("切片长度:{}", slice1.len()); println!("slice2: {:?}", slice2); println!("slice4: {:?}", slice4);
println!("数组元素访问:arr[2] = {}", arr[2]); }
|
运行结果:
1 2 3 4 5
| slice1: [2, 3, 4] 切片长度:3 slice2: [1, 2, 3] slice4: [1, 2, 3, 4, 5] 数组元素访问:arr[2] = 3
|
切片不就是借用吗?
◆借用是 “如何安全使用别人的数据” 的通用规则(比如 “不可变借用只读、可变借用独占”),所有引用(&T/&mut T)都必须遵守;
◆切片是 “针对连续元素的引用” 这个具体类型,它只是 “借用规则” 的一个应用场景—— 切片必然是借用,但借用不一定是切片。
悬垂引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
fn no_dangle() -> String { let s = String::from("hello"); s }
fn main() { let string = no_dangle(); println!("{}", string); }
|
结构体
自定义复合数据类型,将多个不同类型的数据组合成一个整体,用于封装有逻辑关联的数据。与前面的元组很像,不同的是结构体一般为每个数据项命名,比元组更灵活,无需依赖数据的顺序来指定或访问实例的值。
关键字:struct。
结构体的三种类型:
单元结构体。
元组结构体:元组结构体有结构体名称,但没有具体的字段名。
具体字段结构体。
代码示例:创建一个简单的结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| struct User { active: bool, username: String, email: String, sign_in_count: u64, }
fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } }
fn main() { let user = build_user( String::from("sut@gmail.com"), String::from("March") ); println!("{} 已创建", user.username); println!("邮箱: {}", user.email); }
|
运行结果:
1 2
| March 已创建 邮箱: sut@gmail.com
|
基于一个结构体实例创建新实例时,可通过 .. 复用原有字段,仅修改需要变更的部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct User { username: String, age: u8, is_active: bool, }
fn main() { let user1 = User { username: String::from("bob"), age: 28, is_active: true, };
let user2 = User { username: String::from("charlie"), ..user1 };
println!("User2: {}, {}, {}", user2.username, user2.age, user2.is_active); }
|
◆self 是Rust的关键字,代表调用该方法的结构体实例本身。
◆impl 是implementation的缩写,核心作用是为某个类型(如结构体、枚举、基础类型等)绑定方法、关联函数,或实现特质,简单说,impl 是 “给类型添加功能” 的语法块。
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct Rectangle { width: u32, height: u32, }
impl Rectangle { fn area(&self) -> u32 { self.width * self.height }
fn set_width(&mut self, new_width: u32) { self.width = new_width; } }
fn main() { let mut rect = Rectangle { width: 10, height: 20 }; println!("面积:{}", rect.area()); rect.set_width(15); println!("修改后面积:{}", rect.area()); }
|
如何打印结构体?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #[derive(Debug)] struct Rectangle { width: u32, height: u32, }
fn main() { let rect = Rectangle { width: 30, height: 50, }; println!("rect is {:?}", rect); println!("rect is {:#?}", rect); dbg!(&rect); let scale = 2; let rect2 = Rectangle { width: dbg!(30 * scale), height: 50, }; }
|
运行结果:
1 2 3 4 5 6 7 8 9 10
| rect is Rectangle { width: 30, height: 50 } rect is Rectangle { width: 30, height: 50, } [src\main.rs:21:5] &rect = Rectangle { width: 30, height: 50, } [src\main.rs:25:16] 30 * scale = 60
|
dbg!(&rect) 是Rust中用于调试打印的内置宏,核心作用是 “输出变量的详细信息(值 + 代码位置),且不获取变量所有权”。
枚举
基础
上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| enum TrafficLight { Red, Yellow, Green, }
fn main() { let light = TrafficLight::Red; match light { TrafficLight::Red => println!("红灯停"), TrafficLight::Yellow => println!("黄灯警示"), TrafficLight::Green => println!("绿灯行"), } }
|
运行结果:红灯停
携带数据
Rust的枚举的一个很大的特点是可以携带不同类型、不同数量的数据,甚至是结构体 / 另一个枚举。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct Point(i32, i32);
enum Shape { Circle(Point, f64), Rectangle(Point, Point), }
fn main() { let circle = Shape::Circle(Point(5, 5), 3.0); let rect = Shape::Rectangle(Point(0, 0), Point(10, 20));
match circle { Shape::Circle(Point(x, y), r) => println!("圆形:中心({},{}),半径{}", x, y, r), Shape::Rectangle(Point(x1, y1), Point(x2, y2)) => println!("矩形:({},{})-({},{})", x1, y1, x2, y2), } match rect { Shape::Circle(Point(x, y), r) => println!("圆形:中心({},{}),半径{}", x, y, r), Shape::Rectangle(Point(x1, y1), Point(x2, y2)) => println!("矩形:({},{})-({},{})", x1, y1, x2, y2), } }
|
和结构体一样,枚举也能通过impl块添加方法。
Option
Rust里面没有null,但有一个枚举类型可以表示值存在或不存在。
1 2 3 4
| enum Option<T> { None, Some(T), }
|
Option<T>有什么用?显式表达 “值可能缺失” 的场景。
Option<T> 是Rust预导入的(不用手动 use)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| fn main() { let maybe_num: Option<i32> = Some(5);
match maybe_num { Some(num) => println!("有值:{},加 10 等于 {}", num, num + 10), None => println!("无值"), }
let no_num: Option<i32> = None; match no_num { Some(num) => println!("有值:{}", num), None => println!("确实没值"), } }
|
简化版:
1 2 3 4 5 6 7 8 9 10 11
| fn main() { let maybe_str = Some(String::from("hello"));
if let Some(s) = maybe_str { println!("提取到字符串:{}", s); } else { println!("无字符串"); } }
|
代码实现:写一个“除法函数”,避免除以0崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| fn divide(a: i32, b: i32) -> Option<i32>{ if b == 0 { None } else { Some(a / b) } } fn main(){ let result1 = divide(8, 8); let result2 = divide(9, 0); match result1 { Some(num) => println!("Result1: {}", num), None => println!("Error: Division by zero in result1"), } match result2 { Some(num) => println!("Result2: {}", num), None => println!("Error: Division by zero in result2"), } }
|
运行结果:
1 2
| Result1: 1 Error: Division by zero in result2
|
常用类
::
:: 被称为 “路径解析运算符”,核心作用是 “访问某个命名空间下的项(类型、函数、常量等)。是 Rust 通用的语法规则,可以类比其他语言的 .。
| 符号 |
作用 |
适用场景 |
示例 |
:: |
路径解析,访问 “类型 / 模块” 的项 |
关联函数、枚举变体、常量、模块 |
Vec::new()、Option::Some(5) |
. |
访问 “实例” 的方法 / 字段 |
实例方法、结构体字段 |
v.push(1)、rect.width |
◆调用类型的关联函数:关联函数是属于类型本身,而非类型实例的函数(不需要先创建实例就能调用),必须用 类型::函数名() 调用。
例如Vec::new():new() 是 Vec 类型的关联函数。String::from("hello"):from() 是 String 类型的关联函数。
1 2 3 4 5
| let s = String::from("hello"); s.len();
let v = Vec::new(); v.push(1);
|
◆访问模块/命名空间下的项:Rust 用模块组织代码,:: 用于从模块中取出类型 / 函数 / 常量。
1 2 3 4 5
| use std::fs::File;
let result = std::io::read_to_string("file.txt");
|
◆访问枚举的变体:枚举的变体也需要用 :: 访问,因为变体属于枚举类型的命名空间。
1 2 3 4 5 6 7
| enum Option<T> { None, Some(T), }
let x: Option<i32> = Option::Some(5); let y = Option::None;
|
◆访问常量:类型/模块下的常量也通过 :: 访问。
1 2 3
| let max_i32 = i32::MAX; println!("i32 最大值:{}", max_i32);
|
Vector
概述
Rust 中的 Vector(简称 Vec<T>)是动态可变的连续内存数组,也是 Rust 最核心的集合类型 —— 可以把它理解为 “能自动扩容的数组”,支持在运行时添加 / 删除元素,且严格遵循 Rust 的所有权规则,保证内存安全。
◆动态长度:编译时无需确定长度,运行时可按需扩容(区别于固定长度的数组 [T; N]);
◆连续内存:元素在堆上连续存储,访问速度快(随机访问时间复杂度 O (1));
◆所有权管理:Vec<T> 拥有其所有元素的所有权,销毁时会自动释放所有元素的内存;
◆泛型支持:Vec<T> 是泛型类型,<T> 可以是任意 Rust 类型(如 i32、String、自定义结构体等),但一个 Vec 只能存储同类型元素。
创建Vector:
1 2 3 4 5 6 7 8 9 10 11
| ```rust fn main() { let mut v1: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3, 4];
let v3 = vec![0; 5]; }
|
基本方法