1. 程式人生 > >【自己動手】實現簡單的C++ smart pointer

【自己動手】實現簡單的C++ smart pointer

Why Smart Pointer?

為什麼需要智慧指標?因為c++的記憶體管理一直是個令人頭疼的問題。

假如我們有如下person物件:每個person有自己的名字,並且可以告訴大家他叫什麼名字:

//
//a person who can tell us his/her name.
//
#include<iostream>
#include<string>
using namespace std;
class person{
public:
	person(string);
	void tell();
	~person();
private:
	string name;	  
};

person::person(string name):name(name){

}

void person::tell(){
	cout << "Hi! I am " << name << endl;
}

person::~person(){
	cout << "Bye!" << endl;
}
很多時候,我們並不知道自己要建立多少個物件,因此需要在程式執行中動態地建立和銷燬物件- -這時我們會使用new操作符在堆(heap)中為建立的物件分配記憶體,使用delete釋放分配的記憶體。一個簡單的示例:
#include "person.h"
int main(){
	person *p = new person("Cici");
	p -> tell();
	delete p;
}
程式的執行結果:

簡單的程式我們當然不會忘記釋放堆中分配的物件。但是當程式複雜時,成千上萬物件被動態的建立,而在不使用這些物件時,都需要及時的釋放,否則就會造成記憶體洩露(memory leak)。記憶體洩露是c++程式中最常見的問題之一,因為c++本身並沒有提供自動堆記憶體管理的功能,所以,所有這些任務都交給了程式設計師自己--程式設計師必須對自己動態建立的堆物件負責:在物件不需要被使用時及時地銷燬物件。然而,程式設計師不是萬能的,所以記憶體洩露,總是成為c++程式的常見bug。

這裡不得不提到的是java,java增加了垃圾回收機制(garbage collection),jvm會自己管理那些不被使用到的物件並及時銷燬他們,這大大減輕了程式設計師的負擔,程式設計師只需要new自己需要的物件而不必去釋放他們,因為,在物件不被使用(被引用)時,垃圾回收器會替我們清理殘骸。java也因為這一特性而廣受關注。

STL auto_ptr

c++程式設計師們當然也不甘寂寞,stl中有了“智慧指標”:stl::auto_ptr。使用智慧指標,我們就可以不必關心物件的釋放,因為智慧指標會幫助我們完成物件記憶體空間的釋放。有了auto_ptr,我們的程式可以這樣寫了:

#include "person.h"
#include<memory>
using namespace std;
int main(){
	auto_ptr<person> p(new person("Cici"));
	p -> tell();
	//we don't have to delete p because smart pointer will handle this
	//delete p;
}
執行程式,輸出如下:

可以看到輸出和我們的第一個版本一模一樣。雖然我們並沒有delete我們建立的物件,但是可以看到,它已經在程式退出之前被正確的析構了。

簡單的smart_ptr 通過上面的示例,看到stl的auto_ptr的用法,於是我們可以先總結下一個應該具備的基本功能: 1,對所指向的物件,能夠自動釋放 2,過載了“->”操作符,我們在使用smart_ptr時,能夠像普通指標一樣使用“->”訪問其所指向的物件的成員 3,過載了“*”解引用操作符,同上 那麼,如何實現物件的自動釋放呢? 我們知道,對於區域性物件,其生存週期是物件所在的區域性作用域(一般是程式中的“{ }”之間),而在程式跑出區域性物件的作用域之後,這些區域性物件就會被自動銷燬(這時物件的解構函式也會被呼叫)。所以我們的smart_ptr可以在其解構函式中顯式delete所指向的物件,這樣我們指向的物件也會在smart_ptr作用域之外被釋放。所以我們可以這樣實現我們自己的smart_ptr:
//
//our simple smart pointer
//
#include "person.h"
class smart_ptr{
public:
	smart_ptr(person* p);
	~smart_ptr();
	person& operator*();
	person* operator->();
private:
	person *ptr;
};

smart_ptr::smart_ptr(person* p):ptr(p){
	
}

smart_ptr::~smart_ptr(){
	delete ptr;
}

person&  smart_ptr::operator*(){
	return *ptr;
}

person* smart_ptr::operator->(){
	return ptr;
}
來測試一下這個簡單的smart_ptr:
#include "smart_ptr.h"
using namespace std;
int main(){
	smart_ptr p(new person("Cici"));
	p -> tell();
	//we don't have to delete p because smart pointer will handle this
	//delete p;
}
執行結果:
哈哈,我們的smart_ptr正常工作了。 但是可以看到,這樣的smart_ptr有很大的缺點,我們的智慧指標只能指向我們的person物件,我們需要他指向新的物件時豈不是又要依葫蘆畫瓢寫一個新的smart_ptr麼?顯然,這是c++,我們當然有更通用的方法:模板。我們可以使用模板使我們的smart_ptr在編譯時決定它指向的物件,所以改進的版本:
//
//our simple smart pointer
//
template <typename T>
class smart_ptr{
public:
	smart_ptr(T* p);
	~smart_ptr();
	T& operator*();
	T* operator->();
private:
	T* ptr;
};

template <typename T>
smart_ptr<T>::smart_ptr(T* p):ptr(p){
	
}

template <typename T>
smart_ptr<T>::~smart_ptr(){
	delete ptr;
}

template <typename T>
T&  smart_ptr<T>::operator*(){
	return *ptr;
}

template <typename T>
T* smart_ptr<T>::operator->(){
	return ptr;
}

引用計數

我們的smart_ptr是不是完美了呢?看看下面這種情況:

#include "person.h"
#include "smart_ptr.h"
using namespace std;
int main(){
	smart_ptr<person> p(new person("Cici"));
	p -> tell();
	{
		smart_ptr<person> q = p;
		q -> tell();
	}
}
執行一下:

程式出錯了。分析一下很容易找到原因:我們的智慧指標q在程式跑出自己的作用域後就釋放了指向的person物件,而我們的指標p由於和q指向的同一物件,所以在程式退出時企圖再次釋放一個已經被釋放的物件,顯然會出現經典的segmentation fault異常了。所以,我們“簡單的”smart_ptr是過於“簡單”了。

有什麼辦法解決這個問題呢?想想作業系統中的檔案引用計數器。作業系統為每個開啟的檔案維護一個“引用計數”,當多個程序同時開啟一個檔案時系統會依次將引用計數加1。若某個程序關閉了某個檔案此時檔案不會立即被關閉,系統只是將引用計數減1,當引用計數為0時表示這時已經沒用人使用這個檔案了,系統才會把檔案資源釋放。所以這裡,我們也可以為smart_ptr所指向的物件維護一個引用計數,當有新的smart_ptr指向這個物件時我們將引用計數加1,smart_ptr被釋放時我們只是將引用計數減一;當引用計數減為0時,我們才真正的銷燬smart_ptr所指向的物件。

另外,我們之前的smart_ptr還缺少無參建構函式,拷貝建構函式和對“=”運算子的過載。我們之前的版本並沒有過載“=”運算子但程式依然可以正常執行,這是因為此時使用了編譯器的合成版本,這也是不安全的(儘管這裡沒有發現問題)。

好吧,下面是最終的修改版本(新增的部分我特意都添加了註釋):

//
//smart_ptr.h : our simple smart pointer
//
template <typename T>
class smart_ptr{
public:
	//add a default constructor
	smart_ptr();
	//
	smart_ptr(T* p);
	~smart_ptr();
	T& operator*();
	T* operator->();
	//add assignment operator and copy constructor
	smart_ptr(const smart_ptr<T>& sp);
	smart_ptr<T>& operator=(const smart_ptr<T>& sp);
	//
private:
	T* ptr;
	//add a pointer which points to our object's referenct counter
	int* ref_cnt;
	//
};

template <typename T>
smart_ptr<T>::smart_ptr():ptr(0),ref_cnt(0){
	//create a ref_cnt here though we don't have any object to point to
	ref_cnt = new int(0);
	(*ref_cnt)++;
}

template <typename T>
smart_ptr<T>::smart_ptr(T* p):ptr(p){
	//we create a reference counter in heap
	ref_cnt = new int(0);
	(*ref_cnt)++;
}

template <typename T>
smart_ptr<T>::~smart_ptr(){
	//delete only if our ref count is 0
	if(--(*ref_cnt) == 0){
		delete ref_cnt;
		delete ptr;
	}
}

template <typename T>
T&  smart_ptr<T>::operator*(){
	return *ptr;
}

template <typename T>
T* smart_ptr<T>::operator->(){
	return ptr;
}

template <typename T>
smart_ptr<T>::smart_ptr(const smart_ptr<T>& sp):ptr(sp.ptr),ref_cnt(sp.ref_cnt){
	(*ref_cnt)++;
}

template <typename T>
smart_ptr<T>& smart_ptr<T>::operator=(const smart_ptr<T>& sp){
	if(&sp != this){
		//we shouldn't forget to handle the ref_cnt our smart_ptr previously pointed to
		if(--(*ref_cnt) == 0){
			delete ref_cnt;
			delete ptr;
		}
		//copy the ptr and ref_cnt and increment the ref_cnt
		ptr = sp.ptr;
		ref_cnt = sp. ref_cnt;
		(*ref_cnt)++;
	}
	return *this;
}
為了測試我們的smart_ptr的全部功能,這裡測試用例也做一些增加:
#include "person.h"
#include "smart_ptr.h"
using namespace std;
int main(){
	smart_ptr<person> r;
	smart_ptr<person> p(new person("Cici"));
	p -> tell();
	{
		smart_ptr<person> q = p;
		q -> tell();
		r = q;
		smart_ptr<person> s(r);
		s -> tell();
	}
	r -> tell();
}
執行結果如下:

可以看到,我們的Cici物件只有在最後才被銷燬,我們的smart_ptr終於順利完成任務了。

ps:

寫到這裡,我們的smart_ptr是否完美了呢?No。

比如:我們的物件如果被多個執行緒中的smart_ptr引用,我們的smart_ptr就又有出問題的隱患了,因為這裡根本沒有考慮執行緒對ref_cnt訪問的互斥,所以我們的引用計數是有可能出現計數問題的。這裡就不再實現了,畢竟,我們這裡的smart_ptr只是是“簡單的”實現~