問題描述
我剛從 Java 轉到使用 C++ 進行物件導向開發,我發現一個很讓我非常困惑的問題:C++ 中經常出現使用物件指標,而不是直接使用物件本身的程式碼,比如下面這個例子:
1 |
Object *myObject = new Object; |
而不是使用:
1 |
Object myObject; |
要不就是呼叫物件的方法(比如 testFunc())時不使用這種方式:
1 |
myObject.testFunc(); |
而是得寫成這樣:
1 |
myObject->testFunc(); |
我不明白程式碼為什麼要寫成這種形式,我能想到的是指標方式是直接訪問記憶體,這麼寫程式碼可以提高程式碼效率以及執行速度,是這樣的麼?
最佳回覆來自 Joseph Mansfield
非常不幸,你在程式碼中遇到這麼多的動態記憶體分配,但這個只能說明有現在有太多不合格的 C++ 程式設計師。
這麼說吧,你的兩個問題本質上是同個問題。第一個問題是,應該何時使用動態分配(使用 new 方法)?第二問題是,什麼時候該使用指標?
最先要牢記的重點是,你應該根據實際需求選擇合適的方法。 一般來說,使用定義物件的方式比起使用手工動態分配(或new指標)的方式會更加合理以及安全。
動態分配
你的提問中,所列出的兩種分配物件方式的主要區別在於物件的生存期。通過 Object myObject
方式定義物件,物件的生存期是在其作用域內自維護(automatic storage),這個意味著程式離開物件的作用域之後,物件將被自動銷燬。當通過 new Object()
方式分配物件時,物件的生存期是動態的,這個意味著若不顯式地 detete
物件,物件將一直存在。你應該只在必要的時候使用動態分配物件。換句話說,只要有可能,你應該首選定義可自維護的物件。
這裡是兩個常見需要動態分配物件的情況:
- 分配不限制作用域的物件,物件儲存在其特定的記憶體中,而不是在記憶體中儲存物件的拷貝。如果物件是可以拷貝/移動的,一般情況下你應該選擇使用定義物件的方式。
- 定義的物件會消耗大量記憶體,這時可能會耗盡棧空間。如果我們永遠不需要考慮這個問題那該多好(實際大部分情況下,我們真不需要考慮),因為這個本身已經超出 C++ 語言的範疇,但不幸的是,在我們實際的開發過程中卻不得不去處理這個問題。
當你確實需要動態分配物件時,應該將物件封裝在一個智慧指標(smart pointer)或其他提供RAII機制的型別中(類似標準的 container)。智慧指標提供動態物件的所有權語義(ownership),具體可以看一下std::unique_ptr
和 std::shared_ptr
這兩個例子。如果你使用得當,基本上可以避免自己管理記憶體(具參見 Rule of Zero)。
指標
當然,不使用動態分配而採取原始指標(raw pointer)的用法也很常見,但是大多數情況下動態分配可以取代指標,因此一般情況應該首選動態分配的方法,除非你遇到不得不用指標的情況。
1. 使用引用語義(reference semantics)的情況。有時你可能需要通過傳遞物件的指標(不管物件是如何分配的)以便你可以在函式中去訪問/修改這個物件的資料(而不是它的一份拷貝),但是在大多數情況下,你應該優先考慮使用引用方式,而不是指標,因為引用就是被設計出來實現這個需求的。注意,採用這種方式,物件生存期依舊在其作用域內自維護。當然,如果通過傳遞物件拷貝可以滿足要求的情況下是不需要使用引用語義。
2. 使用多型的情況。通過傳遞物件的指標或引用呼叫多型函式(根據入參型別不同,會呼叫不同處理函式)。如果你的設計就是可以傳遞指標或傳遞引用,顯然,應該優先考慮使用傳遞引用的方式。
3. 對於入參物件可選的情況,常見的通過傳遞空指標
表示忽略入參。如果只有一個引數的情況,應該優先考慮使用預設引數或是對函式進行過載。要不然,你應該優先考慮使用一種可封裝此行為的型別,比如 boost::optional
(或者std::optional
,已經在 C++ 14 草案 n3797 14 中釋出 )。
4. 通過解耦編譯型別依賴減少編譯時間的情況。使用指標的一個好處在於可以用於前向聲名(forward declaration)指向特定型別(如果使用物件型別,則需要定義物件),這種方式可以減少參與編譯的檔案,從而顯著地提高編譯效率,具體可以看 Pimpl idiom 用法。
5. 與C庫或C風格的庫互動的情況。此時只能夠使用指標,這種情況下,你要確保的是指標使用只限定在必要的程式碼段中。指標可以通過智慧指標的轉換得到,比如使用智慧指標的get
成員函式。如果C庫操作分配的記憶體需要你在程式碼中維護並顯式地釋放時,可以將指標封裝在智慧指標中,通過實現 deleter
從而可以有效的地釋放物件。