• Rust编程-小心智能指针导致的程序奔溃
  • 发布于 2个月前
  • 578 热度
    0 评论
  • 我曾经
  • 1 粉丝 32 篇博客
  •   
智能指针是具有手动内存管理的编程语言(如C++和Rust)的一个特性。它们被称为“智能”是因为它们提供了自动内存管理,所以当堆中的对象不再使用时,程序员不必手动删除/释放内存。然而,这些智能指针并非万无一失,程序员仍然需要了解它们是如何工作的,以避免程序中出现致命错误。

在C++语言中,典型的智能指针类型是unique_ptr和shared_ptr,而在Rust中,智能指针的数据类型是Box和Rc等,它们对于存储动态分配的对象(如链表、树或图)非常有用。引入智能指针是为了减少程序员手动管理内存的负担,最大限度地减少与内存相关的错误。

尽管智能指针很有用,但它们有两个主要的限制,使它们不那么智能。首先,它们不能自动解析循环引用,程序员必须用弱指针手动处理循环引用,以避免内存泄漏。其次,在某些情况下如果不手动处理,它们的自动内存管理会使程序崩溃。

这些限制在垃圾收集语言中不存在。垃圾收集器可以释放具有循环引用的对象而不会出现问题,并且肯定不会使程序崩溃。可以用C++或Rust编写一个简单的程序来演示。

考虑C++程序main.cc:
#include <iostream>
#include <memory>
// 堆代码 duidaima.com
template<typename T>
struct Node {
  std::unique_ptr<Node<T>> next;
  T val;

  Node(T val, std::unique_ptr<Node<T>> next) : val{std::move(val)}, next{std::move(next)} {}
};

template<typename T>
struct LinkedList {
  std::unique_ptr<Node<T>> head;

  void push_front(T val) {
    auto node = std::make_unique<Node<T>>(std::move(val), std::move(head));
    head = std::move(node);
  }
};

int main(int argc, const char** argv) {
  auto n = std::stoi(argv[1]);
  LinkedList<int> list;
  for (int i = 0; i < n; ++i)
    list.push_front(i);

  return 0;
}
然后编译并运行
g++ -std=c++17 -O3 main.cc
./a.out 1000000
Segmentation fault (core dumped)
在我的机器上,我在800000左右出现了段故障。我可以向你保证,错误不是来自链表实现本身,而是来自智能指针。下面是Rust的等效版本:
struct Node<T> {
    val: T,
    next: Option<Box<Node<T>>>,
}

struct LinkedList<T> {
    head: Option<Box<Node<T>>>
}

impl<T> LinkedList<T> {
    fn new() -> Self {
        Self{ head: None }
    }

    fn push_front(&mut self, val: T) {
        let next = self.head.take();
        self.head = Some(Box::new(Node{val, next}));
    }
}

fn main() {
    let mut list = LinkedList::new();
    for i in 0..1000000 {
        list.push_front(i);
    }
}
下面是我得到的结果:
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Aborted                 timeout --signal=KILL ${timeout} "$@"
正如你所看到的,即使使用正确的智能指针,也很容易使程序崩溃。显然,如果用Java或c#实现链表,程序就会正常运行,不会崩溃。那么,到底发生了什么?

程序崩溃是因为LinkedList的智能指针头部的默认释放导致对下一个节点的递归调用,这不是尾递归的,无法优化。修复方法是手动覆盖LinkedList数据结构的析构函数方法,迭代地释放每个节点,而不需要递归。从某种意义上说,这违背了智能指针的目的——它们无法从程序员那里解放手动内存管理的负担。

总之,智能指针是C++和Rust中管理内存的有用工具,但在某些情况下,它们仍然需要了解相当多的知识去手动处理。否则,要为意外崩溃做好准备!
用户评论