智能指針有可能會讓你的應用崩潰

智能指針是具有手動內存管理的編程語言 (如 C++ 和 Rust) 的一個特性。它們被稱爲 “智能” 是因爲它們提供了自動內存管理,所以當堆中的對象不再使用時,程序員不必手動刪除 / 釋放內存。然而,這些智能指針並非萬無一失,程序員仍然需要了解它們是如何工作的,以避免程序中出現致命錯誤。

在 C++ 語言中,典型的智能指針類型是 unique_ptr 和 shared_ptr,而在 Rust 中,智能指針的數據類型是 Box 和 Rc 等,它們對於存儲動態分配的對象 (如鏈表、樹或圖) 非常有用。引入智能指針是爲了減少程序員手動管理內存的負擔,最大限度地減少與內存相關的錯誤。

儘管智能指針很有用,但它們有兩個主要的限制,使它們不那麼智能。首先,它們不能自動解析循環引用,程序員必須用弱指針手動處理循環引用,以避免內存泄漏。其次,在某些情況下如果不手動處理,它們的自動內存管理會使程序崩潰。

這些限制在垃圾收集語言中不存在。垃圾收集器可以釋放具有循環引用的對象而不會出現問題,並且肯定不會使程序崩潰。可以用 C++ 或 Rust 編寫一個簡單的程序來演示。

考慮 C++ 程序 main.cc:

#include <iostream>
#include <memory>

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 中管理內存的有用工具,但在某些情況下,它們仍然需要了解相當多的知識去手動處理。否則,要爲意外崩潰做好準備!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/HB3RHJBhrhKCp4mToPC2nw