Essential Qt 第九章 內存管理

              前面連續幾章製作了一個記事本(ReadMe)程序,當然並沒有完成所有的功能。作爲一名C++程序員,不知道你有沒有注意到一個問題,在這個程序中有個類ReadMe,在類的構造函數中使用了大量的new來創建對象,然而這些構造函數中的new卻找不到對應的delete,事實上這個類根本就沒有析構函數。如果你有這樣的疑惑,很好。。。。因爲這對於C++程序員來說很正常。
              Qt提供了一套內存管理機制,這裏將會介紹這套機制使用,或者說讓你知道哪些必須delete,哪些不必delete.事實上大多數C++有關內存的問題都可以寫成長篇論文或者一本大頭書,這裏則只是簡單的介紹下Qt內存管理上的一些原理,並不討論Qt管理內存的實現細節。
               在正式開始之前,我們先看一個簡單的C++的類定義,以及類構造函數
class Student
{
  private:
    string Name;
    int Age;
  public:
    Student();
};


他的構造函數有兩個寫法
寫法1
Student::Student()
{
  Name = "Jack";
  Age = 20;
}
或者這樣
寫法2
Student::Student():Name("Jack"),Age(20)
{
}
這兩種寫法有個最大的區別在於,第一種寫法中Name = "Jack",他這裏並不是使用了string類的構造函數,而是使用了複製函數,C++規定,對象的成員變量的初始化動作發生在進入構造函數之前,也就是說,第一種寫法事實上先調用string類的默認構造函數創建了一個空的對象Name,然後再調用複製函數來給這個對象Name賦值,相對於寫法2只調用一次構造函數,寫法1調用了兩次string類的函數,這種寫法必然會降低程序的效率。
          如果從程序效率來講,寫法2是最佳選擇,但這裏也帶來一個問題,如果類成員比較多呢?以前面完成的ReadMe程序爲例,他的成員就有滿滿一頁,而這只是我們完成的“小玩具”,如果是工業化的大型程序,一個構造函數如果要使用寫法2的話可能是另一回事了,因爲出於模塊化的設計初衷,我們在ReadMe程序中就通過:就把變量放在不同的函數中,然後在構造函數中調用這些函數,這樣的方法來實現,如果採用寫法2,那就必須把長長的一串變量寫成滿滿一頁的列表,而如果以後需要修改程序的話會變成一場災難了。
           我們可以看出寫法1和寫法2其實算上各有利弊,寫法1使得編碼較爲容易,寫法2就更加考慮程序的性能。這裏就需要一種比較折中的辦法了,寫法1之所以效率較低,因爲需要調用2次函數,但如果有些類“複製表現的像初始化一樣好”,換句話說,有些類成員的默認初始函數和複製函數的開銷非常小,那這樣的類成員可以採用寫法1,如果類成員複製構造函數開銷較大,則採用寫法2

           在明確了這點原則後再來看ReadMe的構造函數,他有長長一串類變量,但很欣慰的是這些變量都是指針,無論一個指針指向一個多大的內存,指針本身很小,很顯然指針的賦值開銷非常小,完全可以採用寫法1,事實上我也是這麼做的。這裏也許有人會追問,爲什麼要用指針,而不是一個類的對象呢?這裏原因有兩個,首先,這裏源自圖形界面“龐大”的內存開銷,一個很普通的按鈕QPushButton,這樣一個窗體控件需要多大內存呢,這是一個無法回答的問題,如果你用一張1Mb的圖片作爲這個按鈕的背景和用一張5Mb的圖片做背景是完全不一樣的,所以面對這種情況,在運行時決定內存大小遠優於在編譯時決定內存大小。其次,使用指針的另一個重要的原因就是Qt的大多數類(其實我遇到的所有類都是這樣)都不能使用複製構造函數和賦值符號=,這兩者在Qt的類中將相應的成員函數聲明爲private,並且不予實現,這樣避免了用會直接使用複製構造函數或者通過繼承來實現複製功能,所以如果不使用指針的話,在構造函數中根本無法給類對象賦值,應爲在構造函數體內使用的是複製構造函數,而Qt這樣設計的原因在於Qt本身的特點,例如信號與槽,如果複製的對象裏有個槽,一個信號連接到兩個完全一樣的槽裏,另外對於界面編程來說複製本身就沒有太大意義,界面中極少會出現連個完全相同的窗體部件。

         另外需要說明的是在ReadMe程序中,雖然ReadMe類成員都是“運行時決定內存”,但最後的ReadMe類對象確實創建在棧中,即在編譯時就決定了內存大小,那這樣會不會佔用過大的內存呢?其實仔細的觀察下ReadMe的類成員話就會發現,這個類成員都是一些指針,換言之,雖然最後創建了一個在棧中的對象,但僅僅是一個包含了很多指針的對象,這個對象本身所需要的內存是非常小的。

          然後就是大家最關心的delete了,在ReadMe的構造函數中,有一串的new而沒有一個對應的delete,那在ReadMe對象銷燬的時候,這些成員變量有被銷燬嗎?答案是肯定,Qt以窗體部件中的父/子關係來進行內存管理,一個類對象A在調用析構函數之前,會先行調用子對象B的析構函數,如果子對象B也有子對象C,則對象B在析構函數調用前先調用C的析構函數,即A是B的父對象,B是C的父對象,關閉A的時候會按照C->B->A的順序來逐個析構,通過這樣的方式來保證內存不會泄漏,以ReadMe程序爲例,關閉這個程序的時候系統會調用他的析構函數,在這之前,會優先調用類成員的析構函數,這些類成員都是他的子對象或者孫對象(其實Qt文檔裏沒有孫對象這個概念,只是爲了描述問題臨時起的。。)。這樣我們可以確保ReadMe類對象在析構前他的成員已經被析構了,那這個對象也就沒有其他內存可以釋放,所以ReadMe程序就不需要顯示的寫一個析構函數,這裏也可以看出父/子關係的重要性,對於一個Qt程序,必須且只能有一個父對象,其他Qt類的對象都必須是這個對象的(直接或間接)子對象。
          但假設ReadMe函數類成員裏有個string* names;在他的構造函數裏有這樣一行代碼
names = new string;
這不是一個Qt的類,顯然Qt通過父/子對象來管理回收內存的方法不適合他,如果遇到這樣的情況就必須實現ReadMe的析構函數了
ReadMe::~ReadMe()
{
  delete names;
}


PS:本章引用了<Effective C++>的內容
發佈了92 篇原創文章 · 獲贊 19 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章