【Rust入门知识点学与练】第26课:Unsafe Rust

知识点:什么是 unsafe

Rust 默认保证内存安全,但有 5 件事必须在 unsafe 块中才能做:

  1. 解引用裸指针
  2. 调用 unsafe 函数或方法
  3. 访问或修改可变静态变量
  4. 实现 unsafe trait
  5. 访问 union 的字段
rust 复制代码
fn main() {
    // unsafe 不会关闭借用检查器,其他安全规则仍然生效
    let mut x = 42;
    let r1 = &x;
    // let r2 = &mut x;  // ❌ 编译错误,即使 unsafe 也不行
    println!("{}", r1);

    // unsafe 只是解除了上面 5 种限制
    unsafe {
        // 这 5 种操作在 unsafe 中才允许
    }
}

知识点:裸指针 --- 不可变 *const T 和可变 *mut T

裸指针可以绕过借用规则,但需要手动保证安全:

rust 复制代码
fn main() {
    let mut num = 5;

    // 创建裸指针(这步是安全的)
    let r1 = &num as *const i32;   // 不可变裸指针
    let r2 = &mut num as *mut i32; // 可变裸指针

    // 创建裸指针是安全的,解引用才需要 unsafe
    println!("r1 地址: {:p}", r1);
    println!("r2 地址: {:p}", r2);

    unsafe {
        println!("r1 值: {}", *r1);  // 5
        println!("r2 值: {}", *r2);  // 5

        *r2 = 10;  // 通过裸指针修改
        println!("修改后 r1: {}", *r1);  // 10
        println!("修改后 r2: {}", *r2);  // 10
    }

    // 裸指针可以指向无效内存(危险!)
    let dangling = Box::new(42);
    let ptr = &*dangling as *const i32;
    drop(dangling);  // 内存已释放
    // unsafe { println!("{}", *ptr); }  // ❌ 未定义行为!

    // 裸指针可以是 null
    let null_ptr: *const i32 = std::ptr::null();
    println!("空指针: {:p}", null_ptr);
    // unsafe { println!("{}", *null_ptr); }  // ❌ 段错误!

    // 安全做法:先检查
    unsafe {
        if !null_ptr.is_null() {
            println!("{}", *null_ptr);
        } else {
            println!("指针为空,跳过");
        }
    }
}

知识点:裸指针与 FFI(外部函数接口)

裸指针最常见的用途是与 C 语言库交互:

rust 复制代码
// 声明外部 C 函数(安全的声明,调用时才需要 unsafe)
extern "C" {
    fn abs(input: i32) -> i32;        // C 标准库的 abs
    fn strlen(s: *const std::os::raw::c_char) -> usize;
}

// 导出函数给 C 调用(用 #[no_mangle] 防止编译器改名)
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    unsafe {
        // 调用 C 的 abs
        println!("abs(-5) = {}", abs(-5));

        // 调用 C 的 strlen
        let s = std::ffi::CString::new("hello").unwrap();
        println!("strlen = {}", strlen(s.as_ptr()));
    }
}

知识点:调用 unsafe 函数

rust 复制代码
// 定义 unsafe 函数
unsafe fn dangerous() {
    println!("危险操作!");
}

// 在安全函数中包装 unsafe,提供安全接口
fn safe_wrapper() {
    unsafe {
        dangerous();
    }
}

// 实际例子:分割切片(标准库的做法)
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        // 用裸指针创建两个独立的可变切片
        // 编译器无法验证这两个切片不重叠,所以必须 unsafe
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    // dangerous();  // ❌ 不能在安全代码中直接调用

    safe_wrapper();  // ✅ 通过安全函数调用

    let mut v = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut v, 3);
    println!("左边: {:?}", left);   // [1, 2, 3]
    println!("右边: {:?}", right);  // [4, 5, 6]

    // 修改互不影响
    left[0] = 10;
    right[0] = 40;
    println!("修改后: {:?}", v);  // [10, 2, 3, 40, 5, 6]
}

知识点:可变静态变量

rust 复制代码
// 静态变量:全局有效,类似全局变量
// 不可变静态变量是安全的
static LANGUAGE: &str = "Rust";
static MAX_SIZE: usize = 1024;

// 可变静态变量必须用 unsafe 访问
static mut COUNTER: u32 = 0;

fn increment() {
    unsafe {
        COUNTER += 1;
        println!("计数: {}", COUNTER);
    }
}

fn main() {
    println!("语言: {}", LANGUAGE);
    println!("最大尺寸: {}", MAX_SIZE);

    // 读取可变静态变量也需要 unsafe
    unsafe {
        println!("初始计数: {}", COUNTER);
    }

    increment();
    increment();
    increment();

    unsafe {
        println!("最终计数: {}", COUNTER);  // 3
    }

    // ⚠️ 可变静态变量在多线程中不安全!
    // 多线程同时修改会导致数据竞争
    // 多线程场景应使用 Arc<Mutex<T>> 或 AtomicU32
}

知识点:unsafe trait

只有当编译器无法验证安全性时,才标记 trait 为 unsafe:

rust 复制代码
// unsafe trait 要求实现者保证某些不变量
unsafe trait Sync {
    // 实现者必须保证:&Self 可以安全地在线程间共享
}

// 实现 unsafe trait 需要 unsafe 块
struct MyType;

unsafe impl Sync for MyType {
    // 我保证 MyType 可以安全地在线程间共享
}

// 实际例子:为裸指针包装类型实现 Send
struct RawPointer<T> {
    ptr: *mut T,
}

// 裸指针默认不是 Send/Sync
// 如果我们能保证使用场景的安全,可以手动实现
unsafe impl<T: Send> Send for RawPointer<T> {}
unsafe impl<T: Sync> Sync for RawPointer<T> {}

fn main() {
    let mut value = 42;
    let rp = RawPointer { ptr: &mut value as *mut i32 };

    unsafe {
        println!("值: {}", *rp.ptr);
    }
}

知识点:unsafe 与 std 库中的常见操作

rust 复制代码
use std::slice;

fn main() {
    // === 指针算术 ===
    let arr = [10, 20, 30, 40, 50];
    let ptr = arr.as_ptr();

    unsafe {
        // 指针偏移
        println!("第0个: {}", *ptr);          // 10
        println!("第1个: {}", *ptr.add(1));   // 20
        println!("第2个: {}", *ptr.add(2));   // 30
        println!("第4个: {}", *ptr.add(4));   // 50

        // offset(有符号偏移)
        let end = ptr.add(4);
        println!("倒数第2个: {}", *end.offset(-1));  // 40
    }

    // === from_raw_parts:从裸指针创建切片 ===
    let data = [1u8, 2, 3, 4, 5];
    let ptr = data.as_ptr();
    unsafe {
        let slice = slice::from_raw_parts(ptr, 3);
        println!("切片: {:?}", slice);  // [1, 2, 3]
    }

    // === MaybeUninit:处理未初始化的内存 ===
    use std::mem::MaybeUninit;

    let mut buf: [MaybeUninit<i32>; 5] = unsafe {
        MaybeUninit::uninit().assume_init()
    };

    // 逐个初始化
    for (i, slot) in buf.iter_mut().enumerate() {
        slot.write((i * 10) as i32);
    }

    // 全部初始化后,安全地转换为普通数组
    let initialized: [i32; 5] = unsafe {
        // 所有元素都已初始化,可以安全地 assume_init
        std::mem::transmute_copy(&buf)
    };
    println!("初始化数组: {:?}", initialized);  // [0, 10, 20, 30, 40]

    // === transmute:类型强转(极度危险)===
    // 把 u32 的位模式解释为 f32
    let bits: u32 = 0x42280000;
    let float: f32 = unsafe { std::mem::transmute(bits) };
    println!("位模式 {:#010x} => f32: {}", bits, float);  // 42.0

    // 更安全的替代方案
    let float2 = f32::from_bits(bits);
    println!("from_bits: {}", float2);  // 42.0
}

知识点:用 unsafe 实现安全的抽象

rust 复制代码
// 用 unsafe 实现一个高效的环形缓冲区
struct RingBuffer {
    buffer: *mut u8,
    capacity: usize,
    read_pos: usize,
    write_pos: usize,
    len: usize,
}

impl RingBuffer {
    fn new(capacity: usize) -> Self {
        let mut buf = Vec::with_capacity(capacity);
        let buffer = buf.as_mut_ptr();
        std::mem::forget(buf);  // 防止 Vec 释放内存,由我们自己管理

        RingBuffer {
            buffer,
            capacity,
            read_pos: 0,
            write_pos: 0,
            len: 0,
        }
    }

    fn push(&mut self, byte: u8) -> bool {
        if self.len >= self.capacity {
            return false;  // 满了
        }
        unsafe {
            *self.buffer.add(self.write_pos) = byte;
        }
        self.write_pos = (self.write_pos + 1) % self.capacity;
        self.len += 1;
        true
    }

    fn pop(&mut self) -> Option<u8> {
        if self.len == 0 {
            return None;
        }
        let byte = unsafe { *self.buffer.add(self.read_pos) };
        self.read_pos = (self.read_pos + 1) % self.capacity;
        self.len -= 1;
        Some(byte)
    }

    fn len(&self) -> usize {
        self.len
    }

    fn is_empty(&self) -> bool {
        self.len == 0
    }
}

impl Drop for RingBuffer {
    fn drop(&mut self) {
        unsafe {
            // 重新构建 Vec 来释放内存
            let _ = Vec::from_raw_parts(self.buffer, 0, self.capacity);
        }
    }
}

fn main() {
    let mut rb = RingBuffer::new(4);

    // 写入
    rb.push(10);
    rb.push(20);
    rb.push(30);
    println!("长度: {}", rb.len());  // 3

    // 读出
    println!("pop: {:?}", rb.pop());  // Some(10)
    println!("pop: {:?}", rb.pop());  // Some(20)
    println!("长度: {}", rb.len());   // 1

    // 环形:写入超过容量后自动覆盖
    rb.push(40);
    rb.push(50);
    rb.push(60);  // 满了,返回 false
    println!("写入60: {}", rb.push(70));  // false

    while let Some(val) = rb.pop() {
        print!("{} ", val);
    }
    println!();  // 30 40 50
    println!("空? {}", rb.is_empty());  // true
}

核心规则

概念 写法

unsafe 块 unsafe { ... }

不可变裸指针 *const T

可变裸指针 *mut T

从引用创建裸指针 &x as *const T / &mut x as *mut T

解引用裸指针 unsafe { *ptr }

指针偏移 unsafe { ptr.add(n) } / ptr.offset(n)

空指针检查 ptr.is_null()

unsafe 函数 unsafe fn name() { ... }

调用 unsafe 函数 unsafe { func() }

可变静态变量 static mut NAME: T = value;

unsafe trait unsafe trait Name { ... }

实现 unsafe trait unsafe impl Name for Type { ... }

从裸指针创建切片 slice::from_raw_parts(ptr, len)

未初始化内存 MaybeUninit::::uninit()

类型强转 std::mem::transmute(a)

动手试试

补全下面的代码:

rust 复制代码
// 补全:实现一个安全的动态数组 MyVec<T>
// 用 unsafe 管理裸指针和手动内存分配
// 对外提供安全的 API(用户不需要 unsafe)

struct MyVec<T> {
    ptr: *mut T,        // 指向堆上数据的裸指针
    len: usize,         // 当前元素数量
    capacity: usize,    // 分配的容量
}

impl<T> MyVec<T> {
    // 补全:创建空的 MyVec(容量为0)
    fn new() -> Self {
        // 提示:用 std::ptr::null_mut() 创建空指针
        todo!()
    }

    // 补全:返回当前元素数量
    fn len(&self) -> usize {
        todo!()
    }

    // 补全:是否为空
    fn is_empty(&self) -> bool {
        todo!()
    }

    // 补全:返回容量
    fn capacity(&self) -> usize {
        todo!()
    }

    // 补全:在末尾添加元素
    // 如果容量不够,先扩容为原来的2倍(最少为4)
    // 提示:用 std::alloc::Layout 和 std::alloc::alloc / realloc
    fn push(&mut self, value: T) {
        // 补全
        todo!()
    }

    // 补全:移除并返回末尾元素
    fn pop(&mut self) -> Option<T> {
        // 补全
        todo!()
    }

    // 补全:获取指定位置的引用
    fn get(&self, index: usize) -> Option<&T> {
        // 补全
        todo!()
    }

    // 补全:获取指定位置的可变引用
    fn get_mut(&mut self, index: usize) -> Option<&mut T> {
        // 补全
        todo!()
    }

    // 补全:扩容到 new_capacity
    // 如果 new_capacity == 0,释放内存
    // 提示:
    // 首次分配用 std::alloc::alloc(layout)
    // 扩容用 std::alloc::realloc(ptr, layout, new_size)
    // 释放用 std::alloc::dealloc(ptr, layout)
    fn grow(&mut self, new_capacity: usize) {
        // 补全
        todo!()
    }
}

// 补全:为 MyVec<T> 实现 Drop trait
// 1. 逐个 drop 元素(用 std::ptr::drop_in_place)
// 2. 释放堆内存(用 grow(0) 或 dealloc)

// 补全:为 MyVec<T> 实现 Debug trait(T: Debug)
// 格式和 Vec 一样:[elem1, elem2, ...]

fn main() {
    let mut v = MyVec::new();
    println!("空: is_empty={}, len={}, cap={}", v.is_empty(), v.len(), v.capacity());
    // 期望: 空: is_empty=true, len=0, cap=0

    // 补全:push 1 到 10
    // 补全:打印 len 和 capacity
    // 期望: len=10, cap>=10

    // 补全:通过 get 打印第0个和第9个元素
    // 期望: 第0个: Some(1), 第9个: Some(10)

    // 补全:通过 get_mut 修改第0个元素为 100
    // 补全:打印修改后的第0个元素
    // 期望: 修改后第0个: Some(100)

    // 补全:pop 3次,打印结果
    // 期望: pop: Some(10), Some(9), Some(8)

    // 补全:打印 Debug 格式
    // 期望: [100, 2, 3, 4, 5, 6, 7]
}

期望输出:

空: is_empty=true, len=0, cap=0

len=10, cap=16

第0个: Some(1)

第9个: Some(10)

修改后第0个: Some(100)

pop: Some(10)

pop: Some(9)

pop: Some(8)

100, 2, 3, 4, 5, 6, 7

写好了贴过来,没问题就进入第27课:异步编程基础 Async/Await 👋

贴代码

MyVec 完整实现(unsafe裸指针、手动内存管理,教学版)

⚠️ 重要说明:这是学习版本,用于理解标准库Vec底层原理。生产环境直接用std::vec::Vec,不要自己造轮子;内存分配器API需要 #!feature(alloc_layout_extra) ?不需要,稳定版即可,但是裸指针操作全部包裹在unsafe块中,对外API完全安全。

完整代码,可以直接复制运行:

rust 复制代码
  
use std::alloc::{alloc, dealloc, realloc, Layout};
use std::fmt;
use std::ptr;

struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    capacity: usize,
}

impl<T> MyVec<T> {
    /// 创建空MyVec,无堆内存分配
    fn new() -> Self {
        MyVec {
            ptr: ptr::null_mut(),
            len: 0,
            capacity: 0,
        }
    }

    fn len(&self) -> usize {
        self.len
    }

    fn is_empty(&self) -> bool {
        self.len == 0
    }

    fn capacity(&self) -> usize {
        self.capacity
    }

    /// 扩容函数;new_capacity=0 代表释放全部堆内存
    fn grow(&mut self, new_capacity: usize) {
        let old_layout = Layout::array::<T>(self.capacity).unwrap();

        if new_capacity == 0 {
            // 释放内存
            if self.capacity > 0 {
                unsafe {
                    dealloc(self.ptr as *mut u8, old_layout);
                }
            }
            self.ptr = ptr::null_mut();
            self.capacity = 0;
            return;
        }

        let new_layout = Layout::array::<T>(new_capacity).unwrap();

        if self.capacity == 0 {
            // 首次分配
            unsafe {
                self.ptr = alloc(new_layout) as *mut T;
            }
        } else {
            // realloc重新分配
            unsafe {
                self.ptr = realloc(self.ptr as *mut u8, old_layout, new_layout.size()) as *mut T;
            }
        }
        self.capacity = new_capacity;
    }

    /// 尾部追加元素
    fn push(&mut self, value: T) {
        // 判断是否需要扩容
        if self.len == self.capacity {
            let new_cap = if self.capacity == 0 {
                4
            } else {
                self.capacity * 2
            };
            self.grow(new_cap);
        }
        // 在len位置写入元素
        unsafe {
            ptr::write(self.ptr.add(self.len), value);
        }
        self.len += 1;
    }

    /// 弹出尾部元素
    fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            return None;
        }
        self.len -= 1;
        unsafe { Some(ptr::read(self.ptr.add(self.len))) }
    }

    /// 获取不可变引用,边界检查
    fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len {
            return None;
        }
        unsafe { Some(&*self.ptr.add(index)) }
    }

    /// 获取可变引用,边界检查
    fn get_mut(&mut self, index: usize) -> Option<&mut T> {
        if index >= self.len {
            return None;
        }
        unsafe { Some(&mut *self.ptr.add(index)) }
    }
}

/// Drop实现:析构所有元素,释放堆内存
impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        // 逐个drop已经存在的元素
        for i in 0..self.len {
            unsafe {
                ptr::drop_in_place(self.ptr.add(i));
            }
        }
        // 释放堆内存
        self.grow(0);
    }
}

/// Debug输出,和标准Vec格式保持一致 [1,2,3]
impl<T: fmt::Debug> fmt::Debug for MyVec<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_list()
            .entries((0..self.len).map(|i| unsafe { &*self.ptr.add(i) }))
            .finish()
    }
}

fn main() {
    let mut v = MyVec::new();
    println!(
        "空: is_empty={}, len={}, cap={}",
        v.is_empty(),
        v.len(),
        v.capacity()
    );

    // push 1~10
    for i in 1..=10 {
        v.push(i);
    }
    println!("len={}, cap={}", v.len(), v.capacity());

    // get访问第0、第9项
    println!("第0个: {:?}", v.get(0));
    println!("第9个: {:?}", v.get(9));

    // get_mut 修改第一个元素
    if let Some(x) = v.get_mut(0) {
        *x = 100;
    }
    println!("修改后第0个: {:?}", v.get(0));

    // pop三次
    println!("pop: {:?}", v.pop());
    println!("pop: {:?}", v.pop());
    println!("pop: {:?}", v.pop());

    // Debug打印
    println!("{:?}", v);
}
 

运行输出

plaintext

空: is_empty=true, len=0, cap=0

len=10, cap=16

第0个: Some(1)

第9个: Some(10)

修改后第0个: Some(100)

pop: Some(10)

pop: Some(9)

pop: Some(8)

100, 2, 3, 4, 5, 6, 7

核心知识点逐条解析

1. MyVec三大字段

a. ptr: *mut T :裸指针,指向堆区连续T数组;null_mut()代表没有分配内存。

b. len :已经初始化、有效的元素个数。

c. capacity :堆上一共分配了多少T的空间,不一定全部初始化。

len ≤ capacity,len之后的内存只是原始字节,没有有效的T对象,绝对不能直接解引用!

2. grow扩容函数关键点

  • Layout::array::(n):创建存放n个T的内存布局,自动处理对齐(alignment);
  • capacity=0,首次分配:调用 alloc ;
  • capacity>0:调用 realloc ,内存块重新分配,旧内存的数据自动拷贝;
  • new_capacity=0:dealloc释放堆内存,ptr置空,capacity归零。

realloc只是拷贝字节,不会调用T的构造/析构,只是原始内存搬运。

3. push原理

  • len == capacity → 需要扩容;初始容量4,后续每次*2;
  • ptr::write(ptr.add(len), value) :直接在指定地址写入T,不会调用drop旧值(这块内存是未初始化的);
  • len +=1,标记新元素有效。

4. pop原理

  • len先−1;
  • ptr::read:从指定地址把T移出,内存位置变成未初始化状态;返回T所有权,由调用方负责drop。

⚠️ push用write、pop用read,这两个函数专门操作未初始化内存,是实现动态数组的核心原语。

5. get / get_mut(安全边界检查)

先判断 index < len;超出范围返回None;

成功后,裸指针转成安全引用 &T / &mut T ,返回给调用者。

对外API完全安全,用户不可能越界访问,所有unsafe操作都封装在MyVec内部。

6. Drop析构逻辑(重中之重)

两步:

a. 循环 ptr::drop_in_place(ptr.add(i)) :逐个调用每个有效元素T的drop方法;

drop_in_place:原地析构一个已经初始化的T,不会释放内存,只是运行T的析构函数;

b. grow(0),调用dealloc释放整块堆内存。

如果漏掉drop_in_place,会产生内存泄漏(堆上T没有执行析构)。

7. Debug实现

f.debug_list() 标准调试列表输出;循环遍历全部有效下标,裸指针转引用,交给debug_list格式化,输出和Vec一模一样 a,b,c

当前实现的已知缺陷(教学版,非标准库完整版本)

  1. 没有实现Clone;
  2. 没有实现Index(\[\]下标语法);
  3. 没有处理内存分配失败(alloc/realloc内存耗尽直接panic,标准库Vec也是如此);
  4. 不支持remove、insert、clear、extend等其它方法;
  5. 没有实现Send/Sync自动推导(裸指针默认不自动实现Send/Sync;标准库Vec根据T自动推导)。

Rust内存安全进阶路线

MyVec 属于不安全Rust(Unsafe Rust) 的入门天花板练习题。

完成这道题,所有权、生命周期、内存布局、裸指针、内存分配、drop、未初始化内存、边界检查就全部打通了。

相关推荐
bug总结1 小时前
uniapp总结
开发语言·前端·javascript
zyeyeye1 小时前
C++入门:从HelloWorld到命名空间揭秘
c语言·开发语言·c++·算法
励志不掉头发的内向程序员1 小时前
【LibreCAD 2D架构】从 addHistory 到 RS_UndoCycle:LibreCAD 里的两套 History 与撤销机制
开发语言·c++·qt·学习·系统架构
Escalating_xu1 小时前
【mmap 进程间通信】从共享内存到跨进程唤醒:用互斥锁、条件变量控制多个进程
java·linux·开发语言·jvm
`流年づ1 小时前
人工智能学习笔记 - 自动微分
人工智能·笔记·学习
走到天涯海角1 小时前
pinia和Vuex的区别以及使用pinia
开发语言·前端·javascript
泡海椒1 小时前
脚本包导入实战:JQuick-Java自定义包引入与别名配置教程
java·开发语言·python
fbbqt1 小时前
Python模块与包
开发语言·python
雪落漂泊1 小时前
C++智能指针
开发语言·jvm·c++