為什麼使用物件指標而不是使用物件本身
引子:
class A{};class B{ public: B(); ~B(); private: A object_a; A * ptr_a;};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在上面的程式碼中,是使用類A的物件好,還是物件指標比較好呢?
很多時候,我們都沒有深刻考慮過這兩種情況,似乎在一個類中 都可以滿足我們的需求。
這種問題在stackoverflow上當然有回覆: t’s very unfortunate that you see dynamic allocation so often. That just shows how many bad C++ programmers there are. 非常不幸,你在程式碼中遇到這麼多的動態記憶體分配,但這個只能說明有現在有太多不合格的 C++ 程式設計師。 哭死
In a sense, you have two questions bundled up into one. The first is when should we use dynamic allocation (using new)? The second is when should we use pointers? 這麼說吧,你的兩個問題本質上是同個問題。第一個問題是,應該何時使用動態分配(使用 new 方法)?第二問題是,什麼時候該使用指標?
The important take-home message is that you should always use the appropriate tool for the job. In almost all situations, there is something more appropriate and safer than performing manual dynamic allocation and/or using raw pointers. 最先要牢記的重點是,你應該根據實際需求選擇合適的方法。 一般來說,使用定義物件的方式比起使用手工動態分配(或new指標)的方式會更加合理以及安全。
It’s very unfortunate that you see dynamic allocation so often. That just shows how many bad C++ programmers there are.
In a sense, you have two questions bundled up into one. The first is when should we use dynamic allocation (using new)? The second is when should we use pointers?
The important take-home message is that you should always use the appropriate tool for the job. In almost all situations, there is something more appropriate and safer than performing manual dynamic allocation and/or using raw pointers.
Dynamic allocation
In your question, you’ve demonstrated two ways of creating an object. The main difference is the storage duration of the object. When doing Object myObject; within a block, the object is created with automatic storage duration, which means it will be destroyed automatically when it goes out of scope. When you do new Object(), the object has dynamic storage duration, which means it stays alive until you explicitly delete it. You should only use dynamic storage duration when you need it. That is, you should always prefer creating objects with automatic storage duration when you can.
The main two situations in which you might require dynamic allocation:
You need the object to outlive the current scope - that specific object at that specific memory location, not a copy of it. If you’re okay with copying/moving the object (most of the time you should be), you should prefer an automatic object. You need to allocate a lot of memory, which may easily fill up the stack. It would be nice if we didn’t have to concern ourselves with this (most of the time you shouldn’t have to), as it’s really outside the purview of C++, but unfortunately we have to deal with the reality of the systems we’re developing for. When you do absolutely require dynamic allocation, you should encapsulate it in a smart pointer or some other type that performs RAII (like the standard containers). Smart pointers provide ownership semantics of dynamically allocated objects. Take a look at std::unique_ptr and std::shared_ptr, for example. If you use them appropriately, you can almost entirely avoid performing your own memory management (see the Rule of Zero).
動態分配 你的提問中,所列出的兩種分配物件方式的主要區別在於物件的生存期。通過 Object myObject 方式定義物件,物件的生存期是在其作用域內自維護(automatic storage),這個意味著程式離開物件的作用域之後,物件將被自動銷燬。當通過 new Object() 方式分配物件時,物件的生存期是動態的,這個意味著若不顯式地 detete 物件,物件將一直存在。你應該只在必要的時候使用動態分配物件。換句話說,只要有可能,你應該首選定義可自維護的物件。
這裡是兩個常見需要動態分配物件的情況:
分配不限制作用域的物件,物件儲存在其特定的記憶體中,而不是在記憶體中儲存物件的拷貝。如果物件是可以拷貝/移動的,一般情況下你應該選擇使用定義物件的方式。 定義的物件會消耗大量記憶體,這時可能會耗盡棧空間。如果我們永遠不需要考慮這個問題那該多好(實際大部分情況下,我們真不需要考慮),因為這個本身已經超出 C++ 語言的範疇,但不幸的是,在我們實際的開發過程中卻不得不去處理這個問題。 當你確實需要動態分配物件時,應該將物件封裝在一個智慧指標(smart pointer)或其他提供RAII機制的型別中(類似標準的 container)。智慧指標提供動態物件的所有權語義(ownership),具體可以看一下std::unique_ptr 和 std::shared_ptr 這兩個例子。如果你使用得當,基本上可以避免自己管理記憶體(具參見 Rule of Zero)。
Pointers
However, there are other more general uses for raw pointers beyond dynamic allocation, but most have alternatives that you should prefer. As before, always prefer the alternatives unless you really need pointers.
You need reference semantics. Sometimes you want to pass an object using a pointer (regardless of how it was allocated) because you want the function to which you’re passing it to have access that that specific object (not a copy of it). However, in most situations, you should prefer reference types to pointers, because this is specifically what they’re designed for. Note this is not necessarily about extending the lifetime of the object beyond the current scope, as in situation 1 above. As before, if you’re okay with passing a copy of the object, you don’t need reference semantics. You need polymorphism. You can only call functions polymorphically (that is, according to the dynamic type of an object) through a pointer or reference to the object. If that’s the behaviour you need, then you need to use pointers or references. Again, references should be preferred. You want to represent that an object is optional by allowing a nullptr to be passed when the object is being omitted. If it’s an argument, you should prefer to use default arguments or function overloads. Otherwise, you should prefer use a type that encapsulates this behaviour, such as boost::optional (or perhaps soon, std::optional - Edit std::optional is voted out of the current C++14 draft n3797). You want to decouple compilation units to improve compilation time. The useful property of a pointer is that you only require a forward declaration of the pointed-to type (to actually use the object, you’ll need a definition). This allows you to decouple parts of your compilation process, which may significantly improve compilation time. See the Pimpl idiom. You need to interface with a C library or a C-style library. At this point, you’re forced to use raw pointers. The best thing you can do is make sure you only let your raw pointers loose at the last possible moment. You can get a raw pointer from a smart pointer, for example, by using its get member function. If a library performs some allocation for you which it expects you to deallocate via a handle, you can often wrap the handle up in a smart pointer with a custom deleter that will deallocate the object appropriately.
指標
當然,不使用動態分配而採取原始指標(raw pointer)的用法也很常見,但是大多數情況下動態分配可以取代指標,因此一般情況應該首選動態分配的方法,除非你遇到不得不用指標的情況。
使用引用語義(reference semantics)的情況。有時你可能需要通過傳遞物件的指標(不管物件是如何分配的)以便你可以在函式中去訪問/修改這個物件的資料(而不是它的一份拷貝),但是在大多數情況下,你應該優先考慮使用引用方式,而不是指標,因為引用就是被設計出來實現這個需求的。注意,採用這種方式,物件生存期依舊在其作用域內自維護。當然,如果通過傳遞物件拷貝可以滿足要求的情況下是不需要使用引用語義。
使用多型的情況。通過傳遞物件的指標或引用呼叫多型函式(根據入參型別不同,會呼叫不同處理函式)。如果你的設計就是可以傳遞指標或傳遞引用,顯然,應該優先考慮使用傳遞引用的方式。
對於入參物件可選的情況,常見的通過傳遞空指標表示忽略入參。如果只有一個引數的情況,應該優先考慮使用預設引數或是對函式進行過載。要不然,你應該優先考慮使用一種可封裝此行為的型別,比如 boost::optional (或者std::optional,已經在 C++ 14 草案 n3797 14 中釋出 )。
通過解耦編譯型別依賴減少編譯時間的情況。使用指標的一個好處在於可以用於前向聲名(forward declaration)指向特定型別(如果使用物件型別,則需要定義物件),這種方式可以減少參與編譯的檔案,從而顯著地提高編譯效率,具體可以看 Pimpl idiom 用法。
與C庫或C風格的庫互動的情況。此時只能夠使用指標,這種情況下,你要確保的是指標使用只限定在必要的程式碼段中。指標可以通過智慧指標的轉換得到,比如使用智慧指標的get成員函式。如果C庫操作分配的記憶體需要你在程式碼中維護並顯式地釋放時,可以將指標封裝在智慧指標中,通過實現 deleter 從而可以有效的地釋放物件。
在函式形參中使用物件指標更好 再看一個例子:
void fun(Base b) { ... }void gun(Base* b) { ... }void hun(Base& b) { ... }Base b;fun(b); // copies b, potentially expensive gun(&b); // takes a pointer to b, no copyinghun(b); // regular syntax, behaves as a pointer
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
Efficiency Passing objects to functions mean creating new copies of object.
Working with objects from third party libraries. If your object belongs to a third party code and the authors intend the usage of their objects through pointers only (no copy constructors etc) the only way you can pass around this object is using pointers. Passing by value may cause issues. (Deep copy / shallow copy issues).
if the object owns a resource and you want that the ownership should not be sahred with other objects.