OSG中的智能指針


在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將再次把內存計數器的數值加一,保證返回的數據有效,且主函數中仍然可以交由智能指針進行內存管理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM