一、前言
指针在前面的篇幅中已经介绍过许多,但主要是智能指针。
智能指针管理堆上的数据,并且受到rust的所有权和借用规则管理(注意,这里的所有权和借用有时候不同于最原始的那种)。
智能指针好歹能管着这些数据,但是rust中存在一些不能使用所有权管理的数据,它们需要利用原始指针来管理。
本文简要讨论原始指针(raw pointer)。
注:<<Rust程序设计语言>>翻译为裸指针,个人觉得不太贴却,那样容易让人联想到没有元数据的智能指针,其次“裸”并没有关联到raw的核心意思:
未经加工的;生的;未经处理的;真实的;原始的;寒冷的;自然状态的;未经训练的;未经分析的;工作生疏的;未烹制的;未煮的;红肿疼痛的;
二、定义
在那本书中,并没有给出原始指针的定义,找了一圈,大体可以如下定义:
- 原始指针类似c,c++中的原始指针
- 原始指针可以手动、直接操作内存,但是需要手动释放
- 原始指针不遵守所谓所有权和借用原则,不保证操作安全性
在代码上,如下定义一个原始指针:
- *const T -- 不可变原始指针
- *mut T -- 可变原始指针
注意:这里的✳号不是表示解引用,就是和C中的指针前的*差不多的意思。
以下是C语言中的指针定义示例:
int *ptr;
char *str = "Hello, World!";
三、作用
如前定义,原始指针的主要作用是直接操作内存,同时超凡脱俗(不用管所有权和借用原则)的特性导致可以用于以下几种业务场景:
- 调用其它语言的函数,目前主要是C语言
- 执行低级别的内存操作-所谓低级别,就是不用管rust的一些规则,显得初级生猛
- 实现高级数据结构和算法,这里主要指内存、并发数据、硬件接口等一些方面
- 优化性能-用于极端环境下的性能优化。总之快是顶级快,只是不保障安全。这是少数环境下的要求
- 提供编程灵活性-简而言之就是rust规则之外的补充。 要只知道世界至少有两面,一个面是复合rust规则,一面是不复合的。
毫无疑问,即使使用rust编码,使用到原始指针的机会也不会很多,否则不如用C,C++之类的语言。
四、示例
毫无疑问,现在对于原始指针以及内存操作并不熟悉,本文的例子基本上是来自<<rust程序设计语言>>,只不过部分稍作了一些调整。
此处示例主要关于:原始指针的创建和销毁
use std::ffi::CString; use std::os::raw::c_char; use std::slice;fn main() {//1.0 演示原始指针的创建和访问,以及可以同时拥有多个指向相同位置的原始指针(不论是可变还是不可变的原始指针)// 以及如何解引用原始指针let mut num = 5; //这里必须定义为mut 。但也发现了rust编译器的一个问题:会错误体提示要移除这个mut,但其实不能移除。let r1 = &num as *const i32; // 不可变原始指针let r2 = &mut num as *mut i32; // 可变原始指针println!("r1: {}, r2: {}", unsafe { *r1 }, unsafe { *r2 });//让r2+1unsafe {*r2 += 1;}println!("r1: {}, r2: {}", unsafe { *r1 }, unsafe { *r2 });//2.0 创建指向任意内存地址的裸指针// 此处代码需要注释掉,否则后续的代码不会执行。因为原始指针指向的内存地址是不合法的,所以会引发运行时错误。//create_raw_pointer_from_address(); println!("---------------------------------------------------------------");//3.0 使用Box创建原始指针 create_raw_pointer_use_box();//4.0 如何创建一个空的原始指针 create_empty_raw_pointer();//5.0 演示如果使用原始指针把一个数组切成两半,且每一半都是可变的切片 demo_split_slice_to_two_half(); }/*** 创建一个指向任意内存地址的原始指针*/ #[allow(dead_code)] fn create_raw_pointer_from_address() {let address = 0x012345usize;let r = address as *const i32;unsafe {println!("r: {}", *r);} } /*** 使用Box创建原始指针*/ fn create_raw_pointer_use_box() {//回忆下Box指针,我们知道Box指针是一个堆上分配的智能指针。let boxed = Box::new(5);let five = *boxed;println!("five: {}", five);let brave = Box::new(String::from("Rust"));let raw_brave = Box::into_raw(brave);unsafe{println!("raw_brave: {:?}", *raw_brave);}//现在需要手动释放内存,否则会造成内存泄露println!("释放raw_brave");unsafe{drop(Box::from_raw(raw_brave));}println!("释放raw_brave完成");// unsafe{// println!("raw_brave依然存在,但这个时候它应该是一个空的: {:?}", *raw_brave);// } }/*** 创建一个空的原始指针-利用std::ptr::null*/ fn create_empty_raw_pointer() {let mut ptr = std::ptr::null::<i32>();if ptr.is_null() {println!("指针ptr是空的");} else {println!("指针ptr不是空的");}let mut value = 5;unsafe {// 将指针指向一个有效的内存地址ptr = &value as *const i32;// 读取指针的值if !ptr.is_null() {println!("指针ptr现在的值是: {}", *ptr);} else {println!("指针ptr现在是空的");}} }/*** 演示如果使用原始指针把一个数组切成两半,且每一半都是可变的切片*/ fn demo_split_slice_to_two_half() {let mut scores=[10,20,30,40,50];println!("原始scores: {:?}", scores);let (left, right) = unsafe_slice(&mut scores, 3);println!("scores-left: {:?}, scores-right: {:?}", left, right);//现在改改左边第一个,看是不是发生了效果left[0] = 1024;right[0] = 1975;println!("修改后scores: {:?}", scores); //事实证明了这点:左边改了,所以可变切片可以的let mut poems=["独怜幽草涧边生","上有黄丽深树鸣","春潮带雨晚来急"];let (left, right) = unsafe_slice(&mut poems, 2);println!("left: {:?}, right: {:?}", left, right); }/*** 使用原始指针创建切片* 这里有非常关键的说明:(来自书本)* Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。* 本质上借用 slice 的不同部分是可以的,因为结果两个 slice 不会重叠,不过 Rust 还没有智能到能够理解这些* * 这一段至少说明了:rust的编译器也不是万能的,要是万能就太慢了。* 在rust书中,此例子属于:创建不安全代码的安全抽象 。* 即把不安全的操作封装起来,对外提供安全的接口(函数)* @param values 实际是一个数组的完整切片* @param mid 切片中间的索引位置*/ fn unsafe_slice<T>(values: &mut [T], mid: usize)-> (&mut [T], &mut [T]) {//let mut data = [1, 2, 3];//let ptr = &data as *const _;let len = values.len();let ptr = values.as_mut_ptr(); // 切片转为原始指针,且指向切片的起始位置。//方法from_raw_parts_mut用于从原始指针和长度创建一个可变切片。//注意:这里的add方法,是原始指针的一个方法,用于计算偏移后的新地址。//如果切片中保存的是汉字,那么应该加多少了? unsafe {(slice::from_raw_parts_mut(ptr, mid),slice::from_raw_parts_mut(ptr.add(mid), len - mid),)} }
输出如下:
有几点值得说:
- rust的编译器有时候会有问题
- 原始指针原则上需要自己手动释放,但也不是所有的都需要。如果是借用来的,可以不要;如果是Box创建的,可以释放
- 随意访问内存一块区域,可能会导致程序终止
五、小结
- 作为一个重要的补充,原始指针为rust编程提供了灵活性,能够做安全指针所不能完成的一些工作,主要是内存操作和外部函数接口、高级数据结构等
- rust有多种方式可以创建原始指针,包括使用引用、Box指针创建、空指针创建
- 部分原始指针需要手动释放,部分不需要,主要看如何定义,这个需要特别注意
- 原始指针的存在,再一次证明了个性化的、简单的东西能够更加高效,能力也更强,只是对于使用者的水平要求较高