get方法报空指针_智能指针shared_ptr踩坑笔记
平時寫代碼一直避免使用指針,但在某些場景下指針的使用還是有必要的。最近在項目中簡單使用了一下智能指針(shared_ptr),結(jié)果踩了不少坑,差點(diǎn)就爬不出來了。痛定思痛抱著《Cpp Primer》啃了兩天,看書的時候才發(fā)現(xiàn)自己的理解和實(shí)踐很淺薄,真的是有種后背發(fā)涼的感覺。。。特地記錄下這些坑點(diǎn),且警后人(指后來的自己=。=).
寫在前面……
本次實(shí)驗(yàn)基于的數(shù)據(jù)結(jié)構(gòu)定義如下:
基類Polygon的成員_points是一個shared_ptr,指向動態(tài)分配的vector<Point>,這樣實(shí)現(xiàn)了在Polygon對象的多個拷貝之間共享相同的vector<Point>。基于Polygon實(shí)現(xiàn)了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;從正確定義智能指針開始……
在項目中采用智能指針的初衷是為了實(shí)現(xiàn)多個對象之間共享數(shù)據(jù),避免拷貝造成的開銷。然而在使用的時候,我竟然連定義一個智能指針都能制造出五花八門的錯誤。。。下面分別整理了正確和錯誤的用法。
1. make_shared函數(shù):最安全的分配和使用動態(tài)內(nèi)存的方法
類似順序容器的emplace成員,make_shared用其參數(shù)來構(gòu)造給定類型的對象。可以是一般的構(gòu)造函數(shù):
shared_ptr<Rect> p1 = make_shared<Rect>(r_points, r_width, r_height);也可以是拷貝構(gòu)造函數(shù):
Rect rect_2(r_points, r_width, r_height); shared_ptr<Rect> p2 = make_shared<Rect>(rect_2); Ps:需要說明的一點(diǎn)是,由于p2指向的對象(即*p2)是rect_2的拷貝,所以它們的_points成員指向相同的內(nèi)存,共享相同的vector<Point>。這個vector<Point>是r_points的一份拷貝,保存在動態(tài)內(nèi)存中。2. shared_ptr和new結(jié)合使用
可以用new返回的指針來初始化智能指針:
shared_ptr<Rect> p3(new Rect(r_points, r_width, r_height));或者將一個shared_ptr綁定到一個已經(jīng)定義的普通指針:
Rect *x = new Rect(r_points, r_width, r_height); shared_ptr<Rect> p4(x); x = nullptr; Ps:這是一種不建議的寫法。原則上當(dāng)p4綁定到x時,內(nèi)存管理的責(zé)任就交給了p4,就不應(yīng)該再使用x來訪問p4指向的內(nèi)存了。因此建議在完成綁定之后立刻將x置為空指針nullptr,避免在后續(xù)代碼中使用delete x釋放p4所指的內(nèi)存,或者又將其他智能指針綁定到x上,這都會造成同一塊內(nèi)存多次釋放的錯誤。但這就出現(xiàn)一個尷尬的情況:程序員要時刻記得一個已經(jīng)存在的變量不能使用,這要求實(shí)在是高了點(diǎn)。。。最理想的還是不要制造出x,或者說x的存在就沒有意義。
3. 【錯誤1】試圖從raw指針隱式轉(zhuǎn)換到智能指針
shared_ptr<Rect> p5 = new Rect(r_points, r_width, r_height); // !!!【修改】接受指針參數(shù)的智能指針構(gòu)造函數(shù)是explicit的,必須使用直接初始化形式:
shared_ptr<Rect> p5(new Rect(r_points, r_width, r_height));4. 【錯誤2】將非動態(tài)分配的內(nèi)存托管給智能指針
Rect rect_6(r_points, r_width, r_height); shared_ptr<Rect> p6(&rect_6); // !!!這種寫法將p6指向一塊棧內(nèi)存,相當(dāng)于局部變量rect_6和p6管理了同一內(nèi)存空間,而棧內(nèi)存中的對象是編譯器負(fù)責(zé)創(chuàng)建和銷毀的,而且不能析構(gòu)一個指向非動態(tài)分配的內(nèi)存的智能指針,因此是不合理的。
【修改】創(chuàng)建智能指針時傳遞一個空的刪除器函數(shù)或者直接使用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】將同一份動態(tài)內(nèi)存托管給多個智能指針
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指向了相同的動態(tài)內(nèi)存,但由于它們是相互獨(dú)立創(chuàng)建的,因此各自的引用計數(shù)都是1,即相互不知道對方的存在,認(rèn)為自己是這塊內(nèi)存的唯一管理者。當(dāng)p8、p9所在程序塊結(jié)束時,內(nèi)存被釋放,從而導(dǎo)致p7變?yōu)榭諔抑羔?#xff0c;意味著當(dāng)試圖使用p7時將發(fā)生未定義的行為;而且也存在同一內(nèi)存多次釋放的危險。
Ps:在測試中還發(fā)現(xiàn)這種多個智能指針托管同一動態(tài)內(nèi)存的情況與上文智能指針指向棧內(nèi)存的情況,二者報錯信息并不相同。
【修改】與錯誤用法2類似,在創(chuàng)建智能指針時傳遞一個空的刪除器函數(shù)即可。
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; 小結(jié):本質(zhì)上4和5屬于同一類型的錯誤,即同一塊內(nèi)存由多個管理者托管,但它們彼此之間又不知道對方的存在,這樣就導(dǎo)致在它們各自生命周期結(jié)束時都會釋放這塊內(nèi)存的錯誤。個人認(rèn)為,5的正確寫法在某種程度上還是可以接受的,但4是一種完全不合理的智能指針使用方式,這種情況就應(yīng)該直接使用raw指針,“只有將指向動態(tài)分配的對象的指針交給shared_ptr托管才是有意義的”。往往這種錯誤在編譯期間沒有問題,但運(yùn)行時會報錯,因此不易排查。為了避免這種錯誤,應(yīng)該養(yǎng)成良好的編程意識,《Cpp Primer》中提到幾條基本規(guī)范,建議嚴(yán)格遵循:
1. 不使用相同的raw指針初始化(或reset)多個智能指針。
2. 不delete get()返回的指針。
3. 不使用get()初始化或reset另一個智能指針。
4. 如果你使用get()返回的指針,記住當(dāng)最后一個對應(yīng)的智能指針銷毀后,你的指針就變?yōu)闊o效了。
5. 如果你使用智能指針管理的資源不是new分配的內(nèi)存,記住傳遞給它一個刪除器。
智能指針的使用場景
《Cpp Primer》中提到程序使用動態(tài)內(nèi)存出于以下三種原因之一:
1. 程序不知道自己需要使用多少對象2. 程序不知道所需對象的準(zhǔn)確類型
3. 程序需要在多個對象間共享數(shù)據(jù)
容器類是出于第一種原因而使用動態(tài)內(nèi)存的典型例子,而2和3的需求可以使用(智能)指針很好地滿足。
智能指針成員
基類Polygon中的_points成員是一個shared_ptr智能指針,依靠它實(shí)現(xiàn)了Polygon對象的不同拷貝之間共享相同的vector<Point>,并且此成員將記錄有多少個對象共享了相同的vector<Point>,并且能在最后一個使用者被銷毀時釋放該內(nèi)存。
Rect rect_1(r_points, r_width, r_height); cout << "rect_1 points成員地址: " << rect_1._points.get() << endl; cout << "rect_1 points引用計數(shù): " << rect_1._points.use_count() << endl;Rect rect_2 = rect_1; cout << "rect_2 points成員地址: " << rect_2._points.get() << endl; cout << "rect_2 points引用計數(shù): " << rect_2._points.use_count() << endl;上述代碼的運(yùn)行結(jié)果:
程序需要在多個對象間共享數(shù)據(jù) →(智能)指針成員容器與繼承
當(dāng)我們使用容器存放繼承體系中的對象時,因?yàn)椴辉试S直接在容器中保存不同類型的元素,通常必須采取間接存儲的方式,即我們實(shí)際上存放的是基類的(智能)指針,這些指針?biāo)傅膶ο罂梢允腔悓ο?#xff0c;也可以是派生類對象。當(dāng)要使用具體的對象時,要利用多態(tài)性將基類指針下行轉(zhuǎn)換為派生類指針。
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;上述代碼的運(yùn)行結(jié)果:
程序不知道所需對象的準(zhǔn)確類型 → 容器中放置(智能)指針而非對象智能指針的下行轉(zhuǎn)換1. 必須使用dynamic_pointer_cast,而不是dynamic_cast。這是因?yàn)楦缸觾煞N智能指針并非繼承關(guān)系,而是完全不同的類型。
2. 基類必須是多態(tài)類型(包含虛函數(shù))。
[Github] 代碼
項目實(shí)例均在vs2017上測試,并上傳至GitHub。
[Reference] 參考
Stack Overflow: Set shared_ptr to point existing object?stackoverflow.comC++11 shared_ptr(智能指針)詳解?www.cnblogs.com總結(jié)
以上是生活随笔為你收集整理的get方法报空指针_智能指针shared_ptr踩坑笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dropout层_DNN,CNN和RNN
- 下一篇: 服务器内存一般多大_性能调优第一步,搞定