get方法报空指针_智能指针shared_ptr踩坑笔记
平時寫代碼一直避免使用指針,但在某些場景下指針的使用還是有必要的。最近在項目中簡單使用了一下智能指針(shared_ptr),結果踩了不少坑,差點就爬不出來了。痛定思痛抱著《Cpp Primer》啃了兩天,看書的時候才發現自己的理解和實踐很淺薄,真的是有種后背發涼的感覺。。。特地記錄下這些坑點,且警后人(指后來的自己=。=).
寫在前面……
本次實驗基于的數據結構定義如下:
基類Polygon的成員_points是一個shared_ptr,指向動態分配的vector<Point>,這樣實現了在Polygon對象的多個拷貝之間共享相同的vector<Point>。基于Polygon實現了Rect和Circle兩個子類。
#include <vector> #include <string> #include <memory> #include <cassert>using namespace std;static constexpr double PI = 3.14;using coord_t = double;struct Point { coord_t x, y; };class Polygon { public:Polygon(const vector<Point> &points) :_points(make_shared<const vector<Point>>(points)) {}virtual string shape() const = 0;virtual coord_t area() const = 0;public:const shared_ptr<const vector<Point>> _points; };class Rect final : public Polygon { public:Rect(const vector<Point> &points, coord_t width, coord_t height) :Polygon(points), _width(width), _height(height) {assert(points.size() == 4);}string shape() const { return "Rect"; }coord_t area() const { return _width * _height; }private:const coord_t _width;const coord_t _height; };class Circle final : public Polygon { public:Circle(const vector<Point> &points, coord_t radius) :Polygon(points), _center(points.front()), _radius(radius) {assert(points.size() == 1);}string shape() const { return "Circle"; }coord_t area() const { return PI * _radius * _radius; }private:const Point _center;const coord_t _radius; };using polygon_ptr = shared_ptr<Polygon>;using rect_ptr = shared_ptr<Rect>;using circle_ptr = shared_ptr<Circle>;// 定義一個邊長為5的矩形和一個半徑為5的圓. static vector<Point> r_points{ {0,0},{0,5},{5,5},{5,0} }; static coord_t r_width = 5, r_height = 5; static vector<Point> c_points{ {0,0} }; static coord_t c_radius = 5;從正確定義智能指針開始……
在項目中采用智能指針的初衷是為了實現多個對象之間共享數據,避免拷貝造成的開銷。然而在使用的時候,我竟然連定義一個智能指針都能制造出五花八門的錯誤。。。下面分別整理了正確和錯誤的用法。
1. make_shared函數:最安全的分配和使用動態內存的方法
類似順序容器的emplace成員,make_shared用其參數來構造給定類型的對象。可以是一般的構造函數:
shared_ptr<Rect> p1 = make_shared<Rect>(r_points, r_width, r_height);也可以是拷貝構造函數:
Rect rect_2(r_points, r_width, r_height); shared_ptr<Rect> p2 = make_shared<Rect>(rect_2); Ps:需要說明的一點是,由于p2指向的對象(即*p2)是rect_2的拷貝,所以它們的_points成員指向相同的內存,共享相同的vector<Point>。這個vector<Point>是r_points的一份拷貝,保存在動態內存中。2. shared_ptr和new結合使用
可以用new返回的指針來初始化智能指針:
shared_ptr<Rect> p3(new Rect(r_points, r_width, r_height));或者將一個shared_ptr綁定到一個已經定義的普通指針:
Rect *x = new Rect(r_points, r_width, r_height); shared_ptr<Rect> p4(x); x = nullptr; Ps:這是一種不建議的寫法。原則上當p4綁定到x時,內存管理的責任就交給了p4,就不應該再使用x來訪問p4指向的內存了。因此建議在完成綁定之后立刻將x置為空指針nullptr,避免在后續代碼中使用delete x釋放p4所指的內存,或者又將其他智能指針綁定到x上,這都會造成同一塊內存多次釋放的錯誤。但這就出現一個尷尬的情況:程序員要時刻記得一個已經存在的變量不能使用,這要求實在是高了點。。。最理想的還是不要制造出x,或者說x的存在就沒有意義。
3. 【錯誤1】試圖從raw指針隱式轉換到智能指針
shared_ptr<Rect> p5 = new Rect(r_points, r_width, r_height); // !!!【修改】接受指針參數的智能指針構造函數是explicit的,必須使用直接初始化形式:
shared_ptr<Rect> p5(new Rect(r_points, r_width, r_height));4. 【錯誤2】將非動態分配的內存托管給智能指針
Rect rect_6(r_points, r_width, r_height); shared_ptr<Rect> p6(&rect_6); // !!!這種寫法將p6指向一塊棧內存,相當于局部變量rect_6和p6管理了同一內存空間,而棧內存中的對象是編譯器負責創建和銷毀的,而且不能析構一個指向非動態分配的內存的智能指針,因此是不合理的。
【修改】創建智能指針時傳遞一個空的刪除器函數或者直接使用raw指針,詳見stackoverflow。正如回答中說的:There is not much point in using a shared_ptr for an automatically allocated object.
Rect rect_6(r_points, r_width, r_height); shared_ptr<Rect> p6(&rect_6, [](Rect*) {});5. 【錯誤3】將同一份動態內存托管給多個智能指針
Rect *xx = new Rect(r_points, r_width, r_height); shared_ptr<Rect> p7(xx); {shared_ptr<Rect> p8(xx); // !!!shared_ptr<Rect> p9(p7.get()); // !!! } xx = nullptr; Rect rect_7 = *p7;p7、p8和p9指向了相同的動態內存,但由于它們是相互獨立創建的,因此各自的引用計數都是1,即相互不知道對方的存在,認為自己是這塊內存的唯一管理者。當p8、p9所在程序塊結束時,內存被釋放,從而導致p7變為空懸指針,意味著當試圖使用p7時將發生未定義的行為;而且也存在同一內存多次釋放的危險。
Ps:在測試中還發現這種多個智能指針托管同一動態內存的情況與上文智能指針指向棧內存的情況,二者報錯信息并不相同。
【修改】與錯誤用法2類似,在創建智能指針時傳遞一個空的刪除器函數即可。
Rect *xx = new Rect(r_points, r_width, r_height); shared_ptr<Rect> p7(xx); {shared_ptr<Rect> p8(xx, [](Rect*) {});shared_ptr<Rect> p9(p7.get(), [](Rect*) {}); } xx = nullptr; Rect rect_7 = *p7; 小結:本質上4和5屬于同一類型的錯誤,即同一塊內存由多個管理者托管,但它們彼此之間又不知道對方的存在,這樣就導致在它們各自生命周期結束時都會釋放這塊內存的錯誤。個人認為,5的正確寫法在某種程度上還是可以接受的,但4是一種完全不合理的智能指針使用方式,這種情況就應該直接使用raw指針,“只有將指向動態分配的對象的指針交給shared_ptr托管才是有意義的”。往往這種錯誤在編譯期間沒有問題,但運行時會報錯,因此不易排查。為了避免這種錯誤,應該養成良好的編程意識,《Cpp Primer》中提到幾條基本規范,建議嚴格遵循:
1. 不使用相同的raw指針初始化(或reset)多個智能指針。
2. 不delete get()返回的指針。
3. 不使用get()初始化或reset另一個智能指針。
4. 如果你使用get()返回的指針,記住當最后一個對應的智能指針銷毀后,你的指針就變為無效了。
5. 如果你使用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器。
智能指針的使用場景
《Cpp Primer》中提到程序使用動態內存出于以下三種原因之一:
1. 程序不知道自己需要使用多少對象2. 程序不知道所需對象的準確類型
3. 程序需要在多個對象間共享數據
容器類是出于第一種原因而使用動態內存的典型例子,而2和3的需求可以使用(智能)指針很好地滿足。
智能指針成員
基類Polygon中的_points成員是一個shared_ptr智能指針,依靠它實現了Polygon對象的不同拷貝之間共享相同的vector<Point>,并且此成員將記錄有多少個對象共享了相同的vector<Point>,并且能在最后一個使用者被銷毀時釋放該內存。
Rect rect_1(r_points, r_width, r_height); cout << "rect_1 points成員地址: " << rect_1._points.get() << endl; cout << "rect_1 points引用計數: " << rect_1._points.use_count() << endl;Rect rect_2 = rect_1; cout << "rect_2 points成員地址: " << rect_2._points.get() << endl; cout << "rect_2 points引用計數: " << rect_2._points.use_count() << endl;上述代碼的運行結果:
程序需要在多個對象間共享數據 →(智能)指針成員容器與繼承
當我們使用容器存放繼承體系中的對象時,因為不允許直接在容器中保存不同類型的元素,通常必須采取間接存儲的方式,即我們實際上存放的是基類的(智能)指針,這些指針所指的對象可以是基類對象,也可以是派生類對象。當要使用具體的對象時,要利用多態性將基類指針下行轉換為派生類指針。
vector<polygon_ptr> polygon_ptrs; polygon_ptrs.push_back(make_shared<Rect>(r_points, r_width, r_height)); polygon_ptrs.push_back(make_shared<Circle>(c_points, c_radius));//auto rect = dynamic_cast<Rect*>(polygon_ptrs.front()); // compile error //auto rect = dynamic_cast<rect_ptr>(polygon_ptrs.front()); // compile error auto rect = dynamic_pointer_cast<Rect>(polygon_ptrs.front()); // compile success cout << "polygon_ptrs.front() shape: " << rect->shape() << " area: " << rect->area() << endl; auto circle = dynamic_pointer_cast<Circle>(polygon_ptrs.back()); cout << "polygon_ptrs.back() shape: " << circle->shape() << " area: " << circle->area() << endl;上述代碼的運行結果:
程序不知道所需對象的準確類型 → 容器中放置(智能)指針而非對象智能指針的下行轉換1. 必須使用dynamic_pointer_cast,而不是dynamic_cast。這是因為父子兩種智能指針并非繼承關系,而是完全不同的類型。
2. 基類必須是多態類型(包含虛函數)。
[Github] 代碼
項目實例均在vs2017上測試,并上傳至GitHub。
[Reference] 參考
Stack Overflow: Set shared_ptr to point existing object?stackoverflow.comC++11 shared_ptr(智能指針)詳解?www.cnblogs.com總結
以上是生活随笔為你收集整理的get方法报空指针_智能指针shared_ptr踩坑笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dropout层_DNN,CNN和RNN
- 下一篇: 服务器内存一般多大_性能调优第一步,搞定