当前位置:   article > 正文

rust实战系列八十五:内存不安全示例:悬空指针

rust实战系列八十五:内存不安全示例:悬空指针

我们再使用一个例子,来继续说明为什么Rust的“mutation+alias”规则是有必要
的。

我们这次通过制造一个悬空指针来解释。以下为一段合理的C++代码,它创建
了一个动态数组,然后使用了一个指针,指向了动态数组的内部元素,然后我们向
动态数组内添加内容,然后发现原先的指针“悬空”了,它指向了一个非法的地址:

// 以下仅仅为了示例而已,不代表推荐的C++编码风格
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v(100, 5);
// 指针指向内部第一个元素
int * p0 = &v[0];
cout << *p0 << endl;
// 为了确保v发生扩容,多插入一些数据
for (int i = 0; i<100; i++) {
v.push_back(10);
}
// 打印p0的内容
cout << *p0 << endl;
return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

编译通过,执行结果为:

5
-72140872
  • 1
  • 2

熟悉STL的朋友肯定知道这里究竟发生了什么。动态数组是自行管理内存空间
的,在向动态数组内部添加元素的时候,如果超过了当前的最大容量,这个动态数
组会申请一块更大的连续内存空间,将原来的元素移动过去,释放掉之前的内存空
间,然后继续往后面添加元素。
我们的指针一开始是指向动态数组的第一个元素的,但是当往动态数组内部添
加多个元素之后,之前的那块内存已经不够用了,动态数组在这个过程中已经将原
来的内存空间释放,并申请了新的内存空间。于是,原本应该指向数组第一个元素
的指针从一个合法的指针变成了指向已回收内存区域的悬空指针,它现在指向的数
据是与原来的意图不同的。而这种情况正是属于Rust希望解决的“内存安全”问题。
我们来看看用Rust写会发生什么。同样,使用动态数组类型,使用一个指针指
向它的第一个元素,然后在原来的动态数组中插入数据:

fn main() {
let mut arr : Vec<i32> = vec![1,2,3,4,5];
let p : &i32 = &arr[0];
for i in 1..100 {
arr.push(i);
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

编译不通过,错误信息为:

error: cannot borrow `arr` as mutable because it is also borrowed as immutable
  • 1

我们可以看到,“mutation+alias”规则再次起了作用。在存在一个不可变引用的
情况下,我们不能修改原来变量的值。写Rust代码的时候,经常会有这样的感觉:
Rust编译器极其严格,甚至到了“不近人情”的地步。但是大部分时候却又发现,它
指出来的问题的确是对我们编程有益的。对它使用越熟练,越觉得它是一个好帮
手。

总结

Rust在内存安全方面的设计方案的核心思想是“共享不可变,可变不共享”。
在可变性控制方面,如果说,C语言和函数式编程语言分属一个天平的两端,
那么Rust就处于这个天平的中央。C语言的思想是:尽量不对程序员做限制,尽量
接近机器底层,类型安全、可变性、共享性都由程序员自由掌控,语言本身不提供
太多的限制和规定。安全与否,也完全取决于程序员。而函数式编程的思想是:尽
量使用不可变绑定,在可变性上有严格限制,在共享性方面没有限制。

函数式编程特别强调无副作用的函数以及不可变类型,以此来达到提高安全性的目的。
而Rust则是选择了折中的方案,既允许可变性,也允许共享性,只要这两者不
是同时出现即可。“共享不可变,可变不共享”,是Rust保证内存安全和线程安全
的“法宝”。

而我们可以看到,Rust的这个设计并不是首鼠两端、和稀泥式的中庸之
道,而是经过了仔细的观察总结、严谨的设计之后的产物。

其一,相比函数式设计方式,Rust并没有本质上牺牲安全性。函数式编程强调
的“不可变”特性,极大地提升了安全性的同时,也极大地提高了学习门槛。而Rust
在“不可变”要求上的理性妥协,实现了在不损失安全性的同时,一定程度上也降低
了学习成本。从C/C++背景转为使用Rust无需做太大的思维转变。相比函数式的设
计方式,Rust的入门门槛更低。虽然对于习惯了无拘无束自由挥洒的C/C++编程语
言的朋友来说,还是有诸多不习惯,但毕竟比Haskell要容易得多。

其二Rust针对传统C/C++做了大幅改进,设计了一系列静态检查规则,来防
止一些潜在的bug。“共享不可变,可变不共享”就是其中一项重要的规则。在传统
的C/C++中,所有的指针都是同一个类型。从功能性来说,这样设计是非常强大
的,但它缺少的恰恰是一定程度的取舍,以提高安全性。相对来说,Rust对程序员
的限制更多,有所为、有所不为。鼓励用户使用的功能应当越容易越优雅越好;避
免用户滥用的功能应当越困难越复杂越好。二者不可偏废。

其三Rust的这套内存安全体系,不需要依赖GC。虽然现在GC的性能越来越
好,但是没有GC在某些场景下依然是很重要的。没有GC、编译型语言的特点,是
Rust执行性能的潜力保证。这就是为什么Rust设计组有底气说Rust的运行性能与C语
言处于同一个档次的原因。当然,目前的Rust还很年轻,许多优化还没有实现,但
这不要紧,单从技术层面上看,还有许多优化在可行性上是没问题的,唯一需要的
是时间和工作量。另外,没有GC就可以使得它只依赖一个非常轻量级的runtime。
理论上来说,它可以用于许多嵌入式平台,甚至可以在无操作系统的裸机上执行,
使用Rust编写操作系统也是完全可行的。这就使得Rust拥有与C/C++相似的系统级
编程特性,大幅扩展了Rust的应用场景。

其四Rust的核心思想“共享不可变,可变不共享”,具有极好的一致性和扩展
性。它不仅可以解决内存安全的问题,还是解决线程安全的基础。在后文中我们会
看到,所谓的线程安全,实质上就是内存安全在多线程情况下的自然延伸。反过
来,我们也可以把Rust的内存安全解决方案视为传统的线程安全机制Read Write
Locker的编译阶段执行的版本。大家应该都能联想到,在多线程环境下,数据竞争
问题是怎么出现的。如果多个线程对同一个共享变量都是只读的,它是安全的;如
果有一个线程对共享变量写操作,那它就必须是独占的,不可有其他线程继续读
写,否则就会出现数据竞争。在第四部分中我们还会发现,Rust里面的许多线程安
全的类型,与一些非线程安全的类型,具有非常有趣的对称性。

由此我们可以看出,Rust的这套设计方案的确是有创新性的。
它走出了一条前无古人的道路。Rust在其他方面的功能,都不能被称作原创设计,都是从其他编程
语言中学过来的。唯独安全性方面的设计是独一无二的。只要我们保证了“共享不
可变,可变不共享”,我们就可以保证内存安全。那么它这套设计方案,究竟能不
能被大众所接受呢?我们拭目以待。
另外,这个规定是否是过于严苛了呢?会不会大幅削弱代码的表达能力?后面
我们还需要进一步分析。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/177166
推荐阅读
相关标签
  

闽ICP备14008679号