
以这段Go代码为例作为开场
func createInt() *int {
i := 42 // int 类型分配在栈上
return &i // 这里由于返回引用类型,分配到堆上
}
func main() {
num := createInt()
println(*num) // 程序结束时,num指向的堆内存会被释放
}
这是一段Go程序,是健康可运行的,createInt函数返回指针,main函数调用,这里叫做 内存逃逸
Go语言中的Gc回收器+逃逸分析,这两个东西组合保障了程序能够正常运行,且不必要担心安全问题
当这段逃逸的堆内存没有地方再引用时,会被回收掉
悬垂引用
一个引用(或指针)仍然指向一块内存地址,但这块内存已经被释放或不再有效。此时再继续操作程序就会异常
下面是一个C语言例子
#include <stdio.h>
int* createInt() {
int i = 42; // int 类型分配在栈上
return &i; // 返回 i 的地址
} // ⚠️ 函数结束,栈的内存会被回收
int main() {
int* num = createInt(); // 接收了已释放内存的地址(
printf("%d\n", *num); // ⚠️ 未定义行为!可能打印 42,也可能打印垃圾值,或者崩溃
return 0;
}
C语言没Go那一套逃逸分析的自动堆内存分配机制,这里的代码实现就称为 悬垂引用,或野指针
如果再C语言中实现该逻辑,需要手动将函数createInt返回内容分配到堆上,为什么要分配到堆上?
因为栈的东西在函数执行完就被回收了,注意哈,C语言没有Gc回收机制,但栈上的内存操作系统是会自动管理的
改良后的完整代码
#include <stdio.h>
#include <stdlib.h> // 包含malloc/free的头文件
int* createInt() {
int* i = (int*)malloc(sizeof(int)); // 分配堆内存
if (i == NULL) { // 检查内存分配是否成功(必做)
printf("内存分配失败\n");
exit(1);
}
*i = 42; // 给堆内存赋值
return i; // 返回堆内存地址(安全)
}
int main() {
int* num = createInt();
printf("%d\n", *num); // 输出 42(安全)
free(num); // 释放堆内存(避免内存泄漏)
num = NULL; // 置空指针(避免野指针)
return 0;
}
rust中悬垂引用
概念都是一样的,rust中没有C语言的malloc/free,没有Go语言的Gc/逃逸分析
fn main() {
let i = create_int();
println!("{}", i);
}
fn create_int() -> &i32 {
let s = 32;
&s
}
rust的编译器不仅仅用来转换二进制代码,还提供了代码解决方案,这是完整的报错信息,里面已经提示你正确的代码书写方式
error[E0106]: missing lifetime specifier
--> src/main.rs:12:20
|
12 | fn create_int() -> &i32 {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
12 | fn create_int() -> &'static i32 {
| +++++++
help: instead, you are more likely to want to return an owned value
|
12 - fn create_int() -> &i32 {
12 + fn create_int() -> i32 {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `naive` (bin "naive") due to 1 previous error
rust中解决悬垂引用的方法为,-> 生命周期
rust 两大数据类型
在搞清楚生命周期之前,要先区分两大类型
- 所有权类型:你是数据的“主人”,数据存在你的变量里
- 引用类型:你只是数据的“借阅者”,数据属于别人,你只能看或改,不能销毁它
fn main() {
let i = create_int();
let j = create_intptr(); // 会报错
}
// 32 这个值是变量 s 的,该函数返回的是 s,谁接收谁能随便用
// 不管你是堆还是栈分配,所有权会自动管理
fn create_int() -> i32 {
let s = 32;
s
}
// 32 这个值是变量 s 的,该函数返回的是 s 的引用指针,接收方只能看/改
// 但是,函数create_intptr执行结束了,s 释放销毁了,main函数中的 j 将变成悬垂引用
fn create_intptr() -> &i32 {
let s = 32;
&s
}
换句话来说,
在局部块儿(函数、结构体..)返回引用类型,会产生悬空指针;返回所有权类型,可以随便玩
rust 中解决悬空指针的方法为 -> 生命周期
这里整理了一份 所有权类型 和 引用类型 的具体体现
所有权类型
特征:变量直接包含数据(或在堆上拥有数据),当变量离开作用域时,数据会被自动释放(Drop)。
符号:通常没有 & 符号。
| 类别 | 具体类型示例 | 说明 |
|---|---|---|
| 基本标量类型 |
i32, f64, bool, char
|
这些类型大小固定,直接存储在栈上。赋值时会复制一份新数据。 |
| 元组 (Tuple) |
(i32, bool), (String, i32)
|
如果元组内包含拥有所有权的类型,元组本身也拥有它们。 |
| 数组 (Array) |
[i32; 5], ["a", "b"]
|
大小固定,直接存储在栈上(除非很大)。赋值时复制。 |
| 结构体 (Struct) | struct User { name: String } |
如果结构体字段是 String 等拥有所有权的类型,结构体实例就拥有这些数据。 |
| 枚举 (Enum) | enum Option<T> { Some(T), None } |
同上,拥有内部数据的所有权。 |
| 字符串 (String) | String::from("hello") |
重点:这是堆分配的字符串,变量拥有堆上数据的所有权。 |
| 集合 (Collections) |
Vec<T>, HashMap<K, V>, Box<T>
|
这些都在堆上分配数据,变量拥有堆数据的句柄(指针+容量+长度),负责释放内存。 |
| 闭包 (Closure) |
|x| x + 1 (捕获所有权时) |
闭包可以捕获变量的所有权。 |
代码示例
let a = 10; // a 拥有 10
let s = String::from("hi"); // s 拥有堆上的 "hi"
let v = vec![1, 2, 3]; // v 拥有堆上的数组
let my_struct = User { name: s }; // my_struct 现在拥有了 name (s 的所有权转移了)
引用类型
特征:变量不包含数据,只包含数据的地址。它们不负责释放内存。必须依附于某个拥有所有权的变量存在。
符号:必须带有 & (不可变引用) 或 &mut (可变引用)。
| 类别 | 具体类型示例 | 说明 |
|---|---|---|
| 不可变引用 |
&i32, &String, &str
|
只能读数据,不能改。可以有多个同时存在。 |
| 可变引用 |
&mut i32, &mut Vec<i32>
|
可以修改数据。同一时间只能有一个。 |
| 切片 (Slices) |
&[i32], &str
|
重点:切片是对连续内存部分的引用。&str 是对字符串数据的引用(通常指向 String 内部或字面量)。 |
| 原始指针 |
*const T, *mut T
|
类似 C 的指针,不安全(unsafe),但也属于引用语义(不拥有所有权)。 |
代码示例
let a = 10;
let r = &a; // r 是引用,借用 a,r 的类型是 &i32
let st = "hello"; // 注意:st是引用类型,引用的一个 "hello" 的串,"hello" 这个字符并不属于变量 st
let s = String::from("hello");
let slice: &str = &s; // slice 是引用,借用 s 的一部分,类型是 &str
let mut x = 5;
let m = &mut x; // m 是可变引用,类型是 &mut i32
*m = 10; // 通过引用修改数据
rust 生命周期
生命周期是编译器用来确保所有引用都是有效的机制。它们的主要目的是防止悬垂引用,即引用指向了已经被释放的内存
语法规则
生命周期标注以 ' 开头,通常使用小写字母,如 'a, 'b, 'c, 'static 等。通常会搭配泛型符号使用。
rust中,泛型不仅仅能用来定义自定义类型,也可用于自定义生命周期类型
static是一个特殊的关键字,下面会单独说
不使用生命周期,会报错
fn main() {
let st = longest("hello", "world");
println!("{}", st);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
加上生命周期
fn main() {
let st = longest("hello", "world");
println!("{}", st);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
这里声明了一个生命周期为'a的泛型,返回的引用也使用了'a生命周期,这样就保证了返回的引用指向的数据和传入的引用指向的数据是同一个生命周期,不会出现悬垂引用
拓展
结构体类型使用
struct Excerpt<'a> {
part: &'a str,
}
// impl 方法实现中也需声明生命周期: impl<'a> xxx<'a>
impl<'a> Excerpt<'a> {
fn return_str(&self, announcement: &str) -> &str {
println!("Announcement: {}", announcement);
self.part // 返回的是 self 的一部分,生命周期与 self 绑定
}
}
fn main() {
let st = Excerpt { part: "hello" };
println!("{}", st.return_str("world"));
}
多个生命周期
fn get_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
y // 明确返回的是 x 的切片,与 y 的生命周期无关 返回x可编译成功,返回y会编译失败
}
fn main() {
let st = get_first("hello", "world");
println!("{}", st);
}
'static 生命周期
系统预设的一个特殊的生命周期,表示该引用在整个程序运行期间都有效。(那怕是声明方的程序已经栈回收了)
以字面量字符引用为例let s = "world";,这个类型编辑器显示是&str类型,这里其实给省略了,它应当是一个定义了全局生命周期的&'static str类型。
案例_1
// 报错
fn longest_1() -> &str {
let s = "world";
s
}
// 正常
fn longest_2(x: &str) -> &str {
let y = "world";
y
}
这是因为rust编译器存在一套生命周期省略规则,有一条规则为 如果函数有一个输入引用参数(如 &str),且返回值是引用类型,编译器会自动将返回值的生命周期推断为与这个输入参数的生命周期相同。
longest_1 手动声明返回类型为 -> &'static str 可以编译通过
这个除外,编译器根据逻辑,不确定使用x还是y 的生命周期
fn longest_6(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
案例_2
// 没显示声明,任何生命周期都可以传 'static 'a 'b
fn partial(s: &str) {
println!("Static string: {}", s);
}
// 显示声明了 static 周期
fn global(s: &'static str) {
println!("Static string: {}", s);
}
总结
各语言解决悬空指针的方法
- Go 逃逸分析+Gc垃圾回收
- C 手动分配堆内存,手动回收
- rust 生命周期