在OpenSceneGraph中,智能指針(Smart pointer)的概念指的是一種類的模板,它針對某一特定類型的對象(即Referenced類及其派生類)構建,提供了自己的管理模式,以避免因為用戶使用new運算符創建對象實例之后,沒有及時用delete運算符釋放對象,而造成部分內存空間被浪費的后果,也就是所謂的內存泄露錯誤。
由於OSG中與場景圖形有關的大多數類均派生自Referenced類,因此OSG大量使用了智能指針來實現場景圖形節點的管理。智能指針的使用為用戶提供了一種自動內存釋放的機制,即,場景圖形中的每一個節點均關聯一個內存計數器,當計數器的計數減到零時,該對象將被自動釋放。而用戶如果希望釋放整個場景圖形的節點的話,則只需要刪除根節點,根節點以下的所有分支節點均會因此被自動刪除,不用擔心內存泄露的問題。
要使用OSG的智能指針,需要滿足以下兩個條件:
1、用戶的類必須派生自Referenced類,這樣才能使用與其自身關聯的內存計數器;
2、使用智能指針模板osg::ref_ptr<class T>來定義類的實例,當用戶使用該模板定義實例時,內存計數器即被啟用並加一;同理,當ref_ptr模板超出其生命范圍時,類實例的內存計數器將被減一,如果減到零則對象自動被釋放。
此外,要使用智能指針,程序中應當引用以下的頭文件:
#include <osg/ref_ptr>
一個使用智能指針的例子如下:
void exampleFunc(){ osg::ref_ptr<osg::Group> root = new osg::Group; osg::ref_ptr<osg::Geode> node1 = new osg::Geode; osg::ref_ptr<osg::Geometry> geo1 = new osg::Geometry; printf("%d, %d, %d\n", root->referenceCount(), node1->referenceCount(), geo1->referenceCount()); root->addChild(node1.get()); node1->addDrawable(geo1.get()); printf("%d, %d, %d\n", root->referenceCount(), node1->referenceCount(), geo1->referenceCount()); }
這個例子本身並沒有什么意義,但是可以通過它了解智能指針的運作流程。
在解讀這個例子之前,首先了解一下與ref_ptr和Referenced類相關的主要成員和運算符:
Referenced類
void ref()
這個公共函數使得Referenced類實例的內存計數器值加一。
void unref()
這個公共函數使得Referenced類實例的內存計數器值減一,如果計數器值為零,那么它自動嘗試將類的實例刪除,釋放相應的內存。
int referenceCount()
返回當前內存計數器的數值。
ref_ptr
ref_ptr()
構造函數,不過它什么也不做。用例為:
osg::ref_ptr<osg::Node> node1;
ref_ptr(T* ptr)
構造函數,並為其對象分配新的內存空間,同時對象的內存計數器值加一。用例為:
osg::ref_ptr<osg::Node> node1 = new osg::Node; ref_ptr(const ref_ptr& rp)
構造函數,其對象將指向一個已有的智能指針對象,同時對象的內存計數器值加一。用例為:
osg::ref_ptr<osg::Node> node2 = node1;
~ref_ptr()
析構函數,執行時對象的內存計數器值減一。
ref_ptr& operator = (const ref_ptr& rp)
重載的賦值運算符,用例為:
node2 = node1;
將node2指向node1,同時將node2(也就是node1)的內存計數器值加一。
ref_ptr& operator = (T* ptr)
重載的賦值運算符,用例為:
node2 = new osg::Node;
將node2指向一個類的實例,同時將node2的內存計數器值加一。
T& operator*() const
返回類實例的值。例如:
osg::ref_ptr<osg::Node> node1 = new osg::Node;
則*node1表示osg::Node
T* operator->() const
返回類的實例。例如:
osg::ref_ptr<osg::Node> node1 = new osg::Node;
則node1->…表示(osg::Node*)->…
T* get() const
返回類的實例。例如:
osg::ref_ptr<osg::Node> node1 = new osg::Node;
則node1.get()表示osg::Node*
bool valid()
返回指針是否有效的標志。
void swap(ref_ptr& rp)
將目前指針所指向的內容與用戶輸入的數據進行交換。用例為:
node2.swap(node1); //交換兩個指針的位置
再看剛才的例子程序,它主要完成了這樣的功能:
1、 新建兩個節點root和node1,以及一個幾何圖形geo1;
2、 調用智能指針的get方法,將node1作為root的子節點加入(addChild);
3、 調用智能指針的get方法,將geo1作為node1的繪圖數據加入(addDrawable)。
此外,程序還調用referenceCount方法,觀察內存計數器的數值。
在主函數中調用此子函數,編譯並運行,觀察顯示的結果,應為:
1, 1, 1
1, 2, 2
可見,當節點和幾何圖形第一次被創建時,它們的內存計數器自動加一;而將node1作為子節點加入以及將geo1作為圖形元件加入的操作,則分別使得這兩者的內存計數器再次加一。
使用new運算符使得內存計數器加一,是因為在ref_ptr構造函數中執行了ref()方法。此方法自動為當前實例的內存計數器加一。
函數addChild和addDrawable會使得內存計數器加一,是因為程序中將node1或者geo1加入到一個ref_ptr的向量表中。參照源代碼可知,用於保存子節點的向量表為NodeList,其定義為:
typedef std::vector< ref_ptr<Node> > NodeList;
而用於保存Geometry幾何數據的向量表為DrawableList,其定義為:
typedef std::vector< ref_ptr<Drawable> > DrawableList;
在執行函數addChild和addDrawable時,使用了向量表模板的push_back方法,將帶有智能指針的數據壓入向量表中,這一步將使得內存計數器自動加一。具體的執行過程可以參見VC目錄下的vector頭文件,其中有類同以下的語句段:
…… _Ty _Tmp = _Val; ……
對於NodeList,上文中的_Ty即表示ref_ptr<Node>,而_Val則是壓入向量表的數據,因此有:
ref_ptr<Node> _Tmp = node1.get();
參考ref_ptr中第二種構造函數的形式可知,此時系統將調用ref()函數,使得內存計數器再次加一,顯然,這一操作對node1也會產生影響。
同理,當執行向量表的pop_back或erase函數時,因為調用了ref_ptr的析構函數,也會使得內存計數器自動減一。執行函數removeChild和removeDrawable即可實現這樣的效果。
再看一種常見的情況,代碼如下:
for (int i = 0; i < 100; i++){ osg::Node* node = new osg::Node; …… }
一般情況下,在循環中使用new運算符開辟新的內存空間,如果沒有及時釋放的話,將產生內存泄露的問題。對於上述的程序段,在運行時如果打開任務管理器,則可以看到程序所占的內存值不斷上漲,如果不加以制止的話,甚至可能造成計算機崩潰。
現在將該程序段中使用new運算符的語句行改寫如下:
//osg::Node* node = new osg::Node; osg::ref_ptr<osg::Node> node = new osg::Node;
再次運行該程序,可以發現內存增長的現象消失了,智能指針在這里發揮了不可忽視的作用。分析這一段程序的流程,可見:
1、 進入循環后,首先為node分配一塊新的內存區域,同時內存計數器自動加一;
2、 執行其余的代碼,如果不對node使用addChild等操作,那么計數器的值始終為1;
3、 到達for循環的結束位置,此時臨時變量的生命周期已經結束,則執行~ref_ptr(),在其中自動執行unref()對計數器的值減一,則計數器的值為0,系統將自動釋放內存區域。
4、新的循環開始,此時原有的內存區域已被釋放,沒有出現內存泄露的情況。
綜上所述,使用智能指針ref_ptr來包裝用戶的節點類,幾何體類等數據,可以有效地進行內存管理,很大程度上避免了內存泄露現象的發生。而在智能指針的使用過程中,還應當注意以下幾點:
1、智能指針模板的應用對象必須派生自Referenced類,否則模板將無法使用。例如:
osg::ref_ptr<osg::Vec3> v;
這樣的聲明是無法編譯通過的,因為Vec3類並不是派生自Referenced類,因此也不具有ref()和unref()這樣的成員函數,無法與計數器相關聯。
2、不可以直接使用delete運算符刪除應用智能指針的對象。事實上這樣的語句也無法編譯通過,閱讀Referenced類的源代碼可以發現,Referenced類的析構函數~Referenced()為保護函數(protected類型),直接使用delete運算符調用它是不允許的。
3、不要隨意使用ref()和unref()函數來改變內存計數器的值。由於這兩個函數都是公共函數,因此這樣的操作不會在編譯中報錯,但是如果內存計數器的數值在程序運行時減為零,以致其對象被釋放,那么下面所有針對此對象的操作均可能導致程序崩潰。而這樣的變故在正常使用的情況下是決不會出現的,因此,除非用戶有特殊需要,否則盡量不要直接使用ref()和unref()來改變內存計數器的值。
4、在OSG中,不使用智能指針而是用形如osg::Node * node的聲明方式也是可以的。但是在大型程序中,應當盡量統一使用智能指針來進行內存的管理。此外,有的時候沒有統一使用ref_ptr的話,程序也可能出現問題。比如這個例子:
osg::Group* exampleFunc(){ osg::ref_ptr<osg::Group> root = new osg::Group; osg::ref_ptr<osg::Geode> node1 = new osg::Geode; root->addChild(node1.get()); …… return root->get(); } int main(int argc, char** argv){ …… osg::Node* a = exampleFunc()->getChild(0); …… }
由於ref_ptr的生命周期在函數的末尾即告結束,導致函數返回時返回的Group指針其實已經被釋放掉了,這樣程序編譯和鏈接都不會與錯誤,但運行時會出現錯誤,而且這種錯誤往往難以檢測到。為了解決問題,將程序統一修改為ref_ptr的命名方式如下:
osg::ref_ptr<osg::Group> exampleFunc(){ osg::ref_ptr<osg::Group> root = new osg::Group; osg::ref_ptr<osg::Geode> node1 = new osg::Geode; root->addChild(node1.get()); …… return root; } int main(int argc, char** argv){ …… ref_ptr<osg::Node> a = exampleFunc()->getChild(0); …… }
就可以運行通過了。函數返回時,ref_ptr將再次把內存計數器的數值加一,保證返回的數據有效,且主函數中仍然可以交由智能指針進行內存管理。