管理C++類中的指針成員


圖論看的頭大…於是翻了翻抱佛腳必備書:《程序員面試寶典》,這書編的確實不怎么樣,邊邊角角的題目有點多,有些題目的解答思路很不清晰,當做題庫看看也就罷了。今天翻到一道標准容器復制含有指針成員的類導致重復解析的問題,專門回憶了下這方面的知識,在這里做個總結。

C++最諷刺的地方就是“用指針實現了面向對象”這點,所以C++壓根不是什么面向對象,說是面向指針更恰當一點。內存管理這塊一直是C++最復雜的地方之一,也是很多人討厭C++的最大原因之一(我猜C風格字符串是另外一個原因之一)。復制(或使用隱含復制的操作)含有指針成員的類,必須對指針成員做一些特殊的處理,主要方法包括以下幾種:

①值型類,在復制指針成員時,重新分配內存,復制對象。使得不同的對象之間完全無耦合,配合C++11引入的右值引用,高效又省事。

②使用C++11中引入的新技術,包括shared_ptr,unique_ptr和weak_ptr(#include <memory>)。

③自己定義智能指針類,實現②中的功能。

我個人最喜歡還是用①,這么干最簡單,如果加上垃圾回收器,基本就成了java。但是對於大規模數據運算的類,還是不要這么干為妙。方法②是比較方便的方法,前提是對這三個smart pointer有比較好的理解,而且你個人喜歡這個風格的書寫。(其實我看着感覺不是很爽…)

其中unique_ptr被設計出來是用來取代auto_ptr的,shared_ptr就是一個使用計數類,配合weak_ptr來使用。詳細的介紹可以直接看微軟的文檔,或者這個翻譯的版本:http://kheresy.wordpress.com/2012/03/05/c11_smartpointer_p2/

如果討厭stl的智能指針類,最后的方法就是自己實現這個玩意。C++prime 4th在13.5.1和15.8.1兩節介紹了兩個方法來實現使用計數,總結一下:

 1 class U_Ptr;
 2 class HasPtr
 3 {
 4 public:
 5     HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
 6     HasPtr(const HasPtr &orig):
 7         ptr(orig.ptr),val(orig.val)
 8     {
 9         ++ptr->use;
10     }
11     HasPtr& operator=(const HasPtr&);
12     ~HasPtr(){if (--ptr->use==0)delete ptr;}
13     int *get_ptr()const{return ptr->ip;}
14     int get_int()const{return val;}
15     void set_ptr(int *p){ptr->ip=p;}
16     void set_int(int i){val=i;}
17     int get_ptr_val()const{return *ptr->ip;}
18     int set_ptr_val(int i)const{*ptr->ip=i;}
19 private:
20     U_Ptr *ptr;
21     int val;
22 };
23 class U_Ptr
24 {
25     friend class HasPtr;
26     int *ip;
27     size_t use;
28     U_Ptr(int *p):ip(p),use(1){}
29     ~U_Ptr(){delete ip;}
30 };
1 HasPtr& HasPtr::operator=(const HasPtr& other)
2 {
3     ++other.ptr->use;
4     if(--ptr->use==0)delete ptr;
5     ptr=other.ptr;
6     val=other.val;
7     return *this;
8 }

這里使用了一個計數類,用來實際存放本來應該在HasPtr中保存的指針,然后又使用了友元。這種設計風格會破壞掉類的封裝,也就是所謂侵入式智能指針,故一般不推薦。

 1 #include <exception>
 2 #include <iostream>
 3 class BasePtr;
 4 class HandlePtr
 5 {
 6 public:
 7     HandlePtr():ptr(nullptr),use(new size_t(1)){}
 8     HandlePtr(const HandlePtr& i)
 9         :ptr(i.ptr),use(i.use)
10     {
11         ++*use;
12     }
13     HandlePtr(HandlePtr&& i)
14         :ptr(i.ptr),use(i.use)
15     {
16     }
17     HandlePtr(const BasePtr&);
18     ~HandlePtr(){decr_use();}
19     HandlePtr& operator=(const HandlePtr&);
20     const BasePtr* operator->()const
21     {
22         if(ptr)return ptr;
23         else throw std::logic_error("unbound HandlePtr");
24     }
25     const BasePtr& operator*()const
26     {
27         if(ptr)return *ptr;
28         else throw std::logic_error("unbound HandlePtr");
29     }
30 private:
31     BasePtr* ptr;
32     size_t *use;
33     void decr_use()
34     {
35         if(--*use==0){delete ptr;delete use;}
36     }
37 };
38 class BasePtr 
39 {
40 public:
41     virtual BasePtr* clone()const
42     {
43         return new BasePtr(*this);
44     } 
45 };
1 HandlePtr::HandlePtr(const BasePtr& other)
2     :ptr(other.clone()),use(new size_t(1)){}

上面則是更好的,也是比較常見的指針管理方式:使用句柄類。句柄類中包含指向管理的類和其子類的指針,句柄類重載了箭頭和解引用操作符,使其指向實際管理的指針,完成動態綁定。句柄類的可以直接由基類引用初始化, 但是由於基類引用可能指向子類,所以必須定義虛克隆來返回實際的類型。

以上兩個方法的核心思想都是“引用計數”,而shared_ptr也是這個原理。令人蛋疼的是,boost引入了大量智能指針,僅僅是掌握這些智能指針的用法就夠頭疼的,好在shared_ptr幾乎可以解決所有的問題,所以它得到了最廣泛的應用,掌握shared_ptr基本上可以解決大部分memory leak的問題(如果不需要考慮引用計數,可以使用unique_ptr)。

我試着用shared_ptr完成了書中sales_item的例子,如下:

 1 #include <string>
 2 #include <ostream>
 3 class Basket;
 4 class Item_base{
 5 public:
 6     Item_base(const std::string& book="",
 7                 double sales_price=0.0):
 8     isbn(book),price(sales_price){}
 9     std::string book()const
10     {
11         return isbn;
12     }
13     virtual double net_price(std::size_t n)const
14     {
15         return n*price;
16     }
17     virtual ~Item_base(){}
18 private:
19     std::string isbn;
20 protected:
21     double price;
22 };
23 
24 class Bulk_item:public Item_base{
25 public:
26     double net_price(std::size_t n)const;
27 private:
28     std::size_t min_qty;
29     double discount;
30 };
31 void print_total(std::ostream &os,const Item_base &item,std::size_t n);
#include "common.h"
#include <memory>
#include <set>
using namespace std;
double Bulk_item::net_price(std::size_t cnt)const
{
    if(cnt>=min_qty)
        return cnt*(1-discount)*price;
    else
        return cnt*price;
}
void print_total(ostream &os,const Item_base &item,size_t n)
{
    os<<"ISBN:"<<item.book()
        <<"\tnumber sold:"<<n<<"\ttotal price:"
        <<item.net_price(n)<<endl;
}
inline bool
    compare(const shared_ptr<Item_base>& lb1,const shared_ptr<Item_base>& lb2)
{
    return lb1->book() < lb2->book();
}
class Basket{
    typedef bool (*Comp)(const shared_ptr<Item_base>&,const shared_ptr<Item_base>&);
    multiset<shared_ptr<Item_base>,Comp> items;
public:
    typedef multiset<shared_ptr<Item_base>,Comp> set_type;
    typedef set_type::size_type size_type;
    typedef set_type::const_iterator const_iter;
    Basket():items(compare){}
    void add_item(const shared_ptr<Item_base> &pItem)
    {
        items.insert(pItem); 
    }
    size_type size(const shared_ptr<Item_base> &i)
    {
        return items.count(i);
    }
    double total()const
    {
        double sum=0.0;
        for(auto iter=items.begin();iter!=items.end()
            ;iter=items.upper_bound(*iter))
        {sum+=(*iter)->net_price(items.count(*iter));}
        return sum;
    }
};

大部分情況下,shared_ptr的使用比較簡單,當做一個自己書寫的句柄類使用即可。但是shared_ptr有一些陷阱,比如不能傳遞數組(但是可以用vector代替,或者指定刪除器);不要在函數實參中初始化;最好不要把this指針傳給shared_ptr,如果需要返回this,可以考慮使用繼承std::enable_shared_from_this,然后返回shared_from_this()(但是這么做之前必須已經有一個正常產生的shared_ptr來存放這個返回的指針);在可能出現循環引用時,使用weak_ptr打斷這種循環;如果是命名對象,最好不要使用new而使用make_shared;另外大量使用shared_ptr會產生碎片,需要自定義分配器進行內存管理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM