c++ - tutorial - unique_ptr用法




為什麼我應該使用指針而不是對象本身? (15)

我來自Java背景,並開始使用C ++中的對象。 但有一件事發生在我身上的是人們經常使用指向對象的指針而不是對象本身,例如這個聲明:

Object *myObject = new Object;

而不是:

Object myObject;

或者用一個函數,比如說testFunc() ,就像這樣:

myObject.testFunc();

我們必須寫:

myObject->testFunc();

但我無法弄清楚為什麼我們應該這樣做。 因為我們可以直接訪問內存地址,所以我會假設它與效率和速度有關。 我對嗎?


但我不明白為什麼我們應該這樣使用它?

我將比較它在函數體內的工作原理,如果你使用:

Object myObject;

在函數內部,一旦這個函數返回,你的myObject就會被銷毀。 所以這是有用的,如果你不需要你的功能之外的對象。 這個對象將被放在當前線程堆棧上。

如果你在函數體內寫入:

 Object *myObject = new Object;

那麼一旦函數結束, myObject指向的Object類實例將不會被銷毀,並且分配在堆上。

現在,如果你是java程序員,那麼第二個例子更接近java下的對象分配。 這一行: Object *myObject = new Object; 相當於java: Object myObject = new Object(); 。 不同的是,在java下myObject會得到垃圾回收,而在c ++下它不會被釋放,你必須在某處明確調用`delete myObject;' 否則你會引入內存洩漏。

由於c ++ 11,您可以使用安全的動態分配方式:通過將值存儲在shared_ptr / unique_ptr中的new Object

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

同樣,對象通常存儲在容器中,如map-s或vector-s,它們將自動管理對象的生命週期。


前言

Java不像C ++,與炒作相反。 Java炒作機器會讓你相信,因為Java具有類似C ++的語法,所以語言是相似的。 沒有什麼比事實更能說明問題。 這種錯誤信息是Java程序員轉向C ++並在不理解代碼含義的情況下使用類Java的語法的原因之一。

我們開始

但我無法弄清楚為什麼我們應該這樣做。 因為我們可以直接訪問內存地址,所以我會假設它與效率和速度有關。 我對嗎?

實際上,相反。 比堆棧得多,因為與堆相比,堆棧非常簡單。 自動存儲變量(也稱為堆棧變量)在析構函數超出範圍時調用它們的析構函數。 例如:

{
    std::string s;
}
// s is destroyed here

另一方面,如果使用動態分配的指針,則必須手動調用其析構函數。 delete為你調用這個析構函數。

{
    std::string* s = new std::string;
}
delete s; // destructor called

這與C#和Java中流行的new語法無關。 它們用於完全不同的目的。

動態分配的好處

1.你不必事先知道數組的大小

許多C ++程序員遇到的第一個問題是,當他們接受用戶的任意輸入時,只能為堆棧變量分配一個固定的大小。 你也不能改變數組的大小。 例如:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

當然,如果你使用std::string來替代, std::string內部調整自己的大小,這應該不是問題。 但本質上解決這個問題的辦法是動態分配。 您可以根據用戶的輸入分配動態內存,例如:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

附註 :許多初學者犯的一個錯誤是使用可變長度數組。 這是一個GNU擴展,也是Clang中的一個擴展,因為它們反映了許多GCC的擴展。 所以不應該依賴以下int arr[n]

由於堆比堆棧大得多,因此可以隨意分配/重新分配盡可能多的內存,而堆棧卻有其局限性。

數組不是指針

這是你問的好處? 一旦你理解了數組和指針背後的混淆/神話,答案就會變得清晰。 通常認為它們是相同的,但它們不是。 這個神話來自這樣的事實,即指針可以像數組一樣下標,因為數組會衰減到函數聲明頂層的指針。 但是,一旦數組衰減到指針,指針就會丟失其信息的sizeof 。 所以sizeof(pointer)將以字節為單位給出sizeof(pointer)的大小,這通常是64位系統上的8個字節。

您不能分配給數組,只能初始化它們。 例如:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

另一方面,你可以用指針做任何你想做的事情。 不幸的是,因為指針和數組之間的區別在Java和C#中是手動的,所以初學者不了解它們的區別。

3.多態性

Java和C#具有允許您將對象視為另一個對象的功能,例如使用as關鍵字。 所以如果有人想把一個Entity對象當作一個Player對象,可以將Player player = Entity as Player; 如果打算在僅適用於特定類型的同質容器上調用函數,這非常有用。 該功能可以通過以下類似的方式實現:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

所以說如果只有Triangles有一個旋轉函數,如果你試圖在類的所有對像上調用它,那將是一個編譯器錯誤。 使用dynamic_cast ,您可以模擬as關鍵字。 要清楚的是,如果轉換失敗,它將返回一個無效的指針。 所以!test本質上是檢查test是NULL還是一個無效指針的簡寫,這意味著cast失敗。

自動變量的好處

在看到動態分配可以完成的所有優點之後,您可能會想知道為什麼沒有人會一直使用動態分配? 我已經告訴過你一個原因,堆很慢。 如果你不需要所有的記憶,你就不應該濫用它。 所以這裡有一些缺點,沒有特別的順序:

  • 這是容易出錯的。 手動內存分配是危險的,你很容易洩漏。 如果您不熟練使用調試器或valgrind (內存洩漏工具),您可能會將頭髮拉出頭部。 幸運的是,RAII成語和智能指針緩解了這一點,但您必須熟悉“三定律”和“五法則”等做法。 這是很多的信息,並且不知道或不在乎的初學者會陷入這個陷阱。

  • 沒有必要。 與Java和C#不同的是,在C ++中,每個地方都使用new關鍵字,您只能在需要時使用它。 常見的一句話是,如果你有錘子,一切看起來都像釘子。 以C ++開始的初學者害怕使用指針,並習慣習慣使用堆棧變量,但Java和C#程序員開始使用指針而不理解它! 這實際上是走錯了路。 你必須放棄你所知道的一切,因為語法是一回事,學習語言是另一回事。

1.(N)RVO - 又名,(命名)返回值優化

很多編譯器做的一個優化就是所謂的elision返回值優化 。 這些東西可以避免不必要的複制對於非常大的對像很有用,例如包含很多元素的矢量。 通常的做法是使用指針來轉移所有權,而不是複制大對象來移動它們。 這導致了移動語義智能指針的出現

如果您使用指針,則(N)RVO不會發生。 如果您擔心優化,那麼利用(N)RVO而不是返回或傳遞指針會更有利且更不容易出錯。 如果函數的調用者負責delete動態分配的對像等,則可能發生錯誤洩漏。 如果指針像熱土豆一樣被傳遞,那麼追踪對象的所有權可能會很困難。 只需使用堆棧變量,因為它更簡單,更好。


"Necessity is the mother of invention." The most of important difference that I would like to point out is the outcome of my own experience of coding. Sometimes you need to pass objects to functions . In that case if your object is of a very big class then passing it as an object will copy its state (which you might not want ..AND CAN BE BIG OVERHEAD) thus resulting in overhead of copying object .while pointer is fixed 4 byte size (assuming 32 bit).Other reasons are already mentioned above...


With pointers ,

  • can directly talk to the memory.

  • can prevent lot of memory leaks of a program by manipulating pointers.


I will include one important use case of pointer. When you are storing some object in the base class, but it could be polymorphic.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

So in this case you can't declare bObj as an direct object, you have to have pointer.


In areas where memory utilization is at its premium , pointers comes handy. For example consider a minimax algorithm, where thousands of nodes will be generated using recursive routine, and later use them to evaluate the next best move in game, ability to deallocate or reset (as in smart pointers) significantly reduces memory consumption. Whereas the non-pointer variable continues to occupy space till it's recursive call returns a value.


One reason for using pointers is to interface with C functions. Another reason is to save memory; for example: instead of passing an object which contains a lot of data and has a processor-intensive copy-constructor to a function, just pass a pointer to the object, saving memory and speed especially if you're in a loop, however a reference would be better in that case, unless you're using an C-style array.


There are many benefits of using pointers to object -

  1. Efficiency (as you already pointed out). Passing objects to functions mean creating new copies of object.
  2. 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).
  3. if the object owns a resource and you want that the ownership should not be sahred with other objects.

This is has been discussed at length, but in Java everything is a pointer. It makes no distinction between stack and heap allocations (all objects are allocated on the heap), so you don't realize you're using pointers. In C++, you can mix the two, depending on your memory requirements. Performance and memory usage is more deterministic in C++ (duh).


Well the main question is Why should I use a pointer rather than the object itself? And my answer, you should (almost) never use pointer instead of object, because C++ has references , it is safer then pointers and guarantees the same performance as pointers.

Another thing you mentioned in your question:

Object *myObject = new Object;

它是如何工作的? It creates pointer of Object type, allocates memory to fit one object and calls default constructor, sounds good, right? But actually it isn't so good, if you dynamically allocated memory (used keyword new ), you also have to free memory manually, that means in code you should have:

delete myObject;

This calls destructor and frees memory, looks easy, however in big projects may be difficult to detect if one thread freed memory or not, but for that purpose you can try shared pointers , these slightly decreases performance, but it is much easier to work with them.

And now some introduction is over and go back to question.

You can use pointers instead of objects to get better performance while transferring data between function.

Take a look, you have std::string (it is also object) and it contains really much data, for example big XML, now you need to parse it, but for that you have function void foo(...) which can be declarated in different ways:

  1. void foo(std::string xml); In this case you will copy all data from your variable to function stack, it takes some time, so your performance will be low.
  2. void foo(std::string* xml); In this case you will pass pointer to object, same speed as passing size_t variable, however this declaration has error prone, because you can pass NULL pointer or invalid pointer. Pointers usually used in C because it doesn't have references.
  3. void foo(std::string& xml); Here you pass reference, basically it is the same as passing pointer, but compiler does some stuff and you cannot pass invalid reference (actually it is possible to create situation with invalid reference, but it is tricking compiler).
  4. void foo(const std::string* xml); Here is the same as second, just pointer value cannot be changed.
  5. void foo(const std::string& xml); Here is the same as third, but object value cannot be changed.

What more I want to mention, you can use these 5 ways to pass data no matter which allocation way you have chosen (with new or regular ).

Another thing to mention, when you create object in regular way, you allocate memory in stack, but while you create it with new you allocate heap. It is much faster to allocate stack, but it is kind a small for really big arrays of data, so if you need big object you should use heap, because you may get , but usually this issue is solved using STL containers and remember std::string is also container, some guys forgot it :)


使用指針的另一個很好的理由是前向聲明 。 在一個足夠大的項目中,他們確實可以加快編譯時間。


在C ++中,在堆棧中分配的Object object; (使用Object object;塊內的語句)將只存在於它們聲明的範圍內。當代碼塊完成執行時,聲明的對像被銷毀。 而如果你在堆上分配內存,使用Object* obj = new Object() ,它們將繼續存在於堆中,直到你調用delete obj為止。

當我喜歡不僅在聲明/分配它的代碼塊中使用對象時,我會在堆上創建一個對象。


指針有很多用例。

多態行為 。 對於多態類型,使用指針(或引用)來避免切片:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

引用語義並避免複製 。 對於非多態類型,指針(或引用)將避免複製潛在的昂貴對象

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

請注意,C ++ 11具有移動語義,可以避免將許多昂貴的對象複製到函數參數和返回值中。 但是使用指針肯定會避免這些,並且會允許同一對像上有多個指針(而對像只能從一次移動)。

資源獲取 。 使用new運算符創建指向資源的指針是現代C ++中的反模式 。 使用特殊資源類(標準容器之一)或智能指針std::unique_ptr<>std::shared_ptr<> )。 考慮:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

原始指針只能用作“視圖”,並不以任何方式參與所有權,無論是通過直接創建還是隱式地通過返回值。 另請參閱C ++ FAQ中的Q&A

更細粒度的生命期控制每次共享指針被複製時(例如作為函數參數),指向的資源都會保持活動狀態。 常規對象(不是由new創建的,無論是直接由您還是由資源類內部創建的)在超出範圍時被銷毀。


這個問題有很多優秀的答案,包括前向聲明,多態性等重要的用例,但我覺得你的問題的“靈魂”的一部分沒有得到回答 - 即不同的語法在Java和C ++中意味著什麼。

我們來看看比較兩種語言的情況:

Java的:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

與此最接近的是:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

讓我們看看另一種C ++方式:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

最好的辦法就是 - 或多或少 - Java(隱式地)處理指向對象的指針,而C ++可以處理指向對象的指針或對象本身。 有一些例外情況 - 例如,如果聲明Java“原始”類型,則它們是被複製的實際值,而不是指針。 所以,

Java的:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

也就是說,使用指針不一定是處理事物的正確或錯誤的方式; 但其他答案已經令人滿意地報導了這一點。 然而,總體思路是,在C ++中,對對象的生命週期以及它們的生活位置有更多的控制權。

回到頂端 - Object * object = new Object()構造實際上是最接近典型Java(或C#)的語義。


Object *myObject = new Object;

Doing this will create a reference to an Object (on the heap) which has to be deleted explicitly to avoid memory leak .

Object myObject;

Doing this will create an object(myObject) of the automatic type (on the stack) that will be automatically deleted when the object(myObject) goes out of scope.





c++11