聊聊智能指針和所有權的問題
在編程語言中,對堆對象的內存管理是一個麻煩又復雜的問題。一不小心就會帶來問題,比如JS里一直引用一個已經不使用的對象導致gc無法回收,或者C++里多個變量指向同一塊內存導致重復釋放。本文簡單探討一下關于對象所有權的問題。
對象的所有權意味著當我們分配一個對象的時候,誰持有這個對象的所有權,比如下面代碼。
- Object *obj = new Object();
那么obj就持有了對象的所有權。但是現實往往比較復雜,比如我們看看下面代碼。
- #include<stdio.h>
- using namespace std;
- class Demo {
- public:
- ~Demo(){
- printf("執(zhí)行析構函數");
- }};void test() {
- Demo *d = new Demo();
- }
- int main(){
- test();
- return 0;
- }
執(zhí)行上面的代碼,我們在test函數里分配一個堆對象,執(zhí)行完test后我們發(fā)現Demo對象的析構函數并沒有執(zhí)行,這就造成了內存泄漏。那我們需要怎么做呢?我們需要收到釋放對象對應的內存。修改一下test函數的代碼。
- void test() {
- Demo *d = new Demo();
- delete d;
- }
這時候我們發(fā)現就會輸出執(zhí)行析構函數幾個字了,說明析構函數被執(zhí)行,對象的內存也被釋放了。手動管理內存不僅麻煩,而且往往容易出錯,比如我們往往會忘了釋放,尤其是代碼邏輯復雜的時候。這時候,我們可以使用智能指針解決這個問題。
- #include <iostream>
- #include<stdio.h>
- using namespace std;
- class Demo {
- public:
- ~Demo(){
- printf("執(zhí)行析構函數");
- }
- };
- template<class T>
- class SmartPoint
- {
- T* point;
- public:
- SmartPoint(T *ptr = nullptr) :point(ptr) {}
- ~SmartPoint() {
- if (point) {
- // 會調用point指向對象的的析構函數
- delete point;
- }
- }
- // 使用智能指針就像使用內部包裹的的對象一樣
- T& operator*() {
- return *point;
- }
- T* operator->() {
- return point;
- }
- };
- void test() {
- SmartPoint<Demo> p(new Demo());
- }
- int main(){
- test();
- return 0;
- }
智能指針的原理比較簡單,因為智能指針對象是在棧上面分配的,離開作用域的時候會被自動釋放,然后在智能指針的析構函數里釋放包裹的內部對象。看起來是很完美的解決方案。但是智能指針也帶來了一些問題,那就是在復制或賦值的時候。我們看看代碼。
- int main(){
- SmartPoint<Demo> p(new Demo());
- SmartPoint<Demo> p2 = p;
- return 0;
- }
執(zhí)行下面代碼會導致core dump,為什么呢?我們來看看這個過程。當執(zhí)行p2=p的時候會導致p2和p的內部指針point都指向了Demo對象的地址,最后代碼執(zhí)行完畢后,兩個智能指針都執(zhí)行了釋放內存的操作,重復釋放內存導致了core dump。那如何解決這個問題呢?一種方式是復制一份point指向的內存,但是我們可能不知道這個內存多大,無法復制,另一種方式就是所有權轉移。我們繼續(xù)看代碼。
- #include <iostream>
- #include<stdio.h>
- using namespace std;
- class Demo {
- public:
- ~Demo(){
- printf("執(zhí)行析構函數");
- }
- };
- template<class T>
- class SmartPoint
- {
- T* point;
- public:
- SmartPoint(T *ptr = nullptr) :point(ptr) {}
- // 實現復制構造函數
- SmartPoint(SmartPoint & p) {
- // 指向p.point對應的內存
- point = p.point;
- // p.point置null
- p.point = nullptr;
- }
- ~SmartPoint() {
- if (point) {
- // 會調用point指向對象的的析構函數
- delete point;
- }
- }
- // 使用智能指針就像使用內部包裹的的對象一樣
- T& operator*() {
- return *point;
- }
- T* operator->() {
- return point;
- }
- };
- int main(){
- SmartPoint<Demo> p(new Demo());
- SmartPoint<Demo> p2 = p;
- return 0;
- }
我們實現了一個復制構造函數,在main里執(zhí)行p2=p時會被執(zhí)行,在復制構造函數中,我們實現了所有權轉移,這時候p2時Demo對象的持有者,而p指向null,這時候不能再對p進行操作。這時候我們可以在SmartPoint中實現一個isNull函數用于判斷智能指針的有效性。
- bool isNull() {
- return point == nullptr;
- }
然后在使用的地方加一下判斷。
- if (p.isNull()) {
- //
- }
這顯然很麻煩。我們看看Rust怎么做。
- struct Demo(u32);
- fn main() {
- let _box1 = Box::new(Demo(1));
- // 所有權轉移
- let _box2 = _box1;
- // 報錯
- println!("{}", _box1.0);
- }
編譯上面代碼會報錯,是編譯而不是運行,這就是Rust,在編譯期就解決了這個問題。Box是智能指針,以上代碼和剛才C++中的代碼類似,當執(zhí)行_box2=_box1的時候,堆對象的所有權就轉移到了_box2,_box1相當于包裹了一個空指針,而Rust不允許你再訪問_box1管理里的內存。