C++ Primer 学习笔记_57_类与数据抽象 --管理指针成员(一)

2014-11-24 12:26:58 · 作者: · 浏览: 4

复制控制

--管理指针成员



引言:

包含指针的类需要特别注意复制控制,原因是复制指针时只是复制了指针中的地址,而不会复制指针指向的对象!

将一个指针复制到另一个指针时,两个指针指向同一对象。当两个指针指向同一对象时,可能使用任一指针改变基础对象。类似地,很可能一个指针删除了一对象时,另一指针的用户还认为基础对象仍然存在。指针成员默认具有与指针对象同样的行为。

大多数C++类采用以下三种方法之一管理指针成员:

1)指针成员采取常规指针型行为:这样的类具有指针的所有缺陷但无需特殊的复制控制!

2)类可以实现所谓的“智能指针”行为:指针所指向的对象是共享的,但类能够防止悬垂指针。

3)类采取值型行为:指针所指向的对象是唯一的,有每个类对象独立管理。


一、定义常规指针类

1、一个带指针成员的指针类

class HasPtr
{
public:
    HasPtr(int *p,int i):ptr(p),val(i) {}

    int *get_ptr() const
    {
        return ptr;
    }

    int get_val() const
    {
        return val;
    }

    void set_ptr(int *p)
    {
        ptr = p;
    }
    void set_val(int i)
    {
        val = i;
    }

    int get_ptr_val()   const
    {
        return *ptr;
    }
    void set_ptr_val(int i) const
    {
        *ptr = i;
    }

private:
    int *ptr;
    int val;
};


2、默认复制/赋值与指针成员

因为HasPtr类没有定义复制构造函数,所以复制一个HasPtr对象将复制两个成员:

    int obj = 0;
    HasPtr ptr1(&obj,42);
    HasPtr ptr2(ptr1);

复制之后,int值是清楚且独立的,但是指针则纠缠在一起!

【小心地雷】

具有指针成员且使用默认合成复制构造函数的类具有普通指针的所有缺陷。尤其是,类本身无法避免悬垂指针


3、指针共享同一对象

复制一个算术值时,副本独立于原版,可以改变一个副本而不改变另一个:

    ptr1.set_val(0);
    cout << ptr1.get_val() << endl;
    cout << ptr2.get_val() << endl;

复制指针时,地址值是可区分的,但指针指向同一基础对象。因此,如果在任意对象上调用set_ptr_val,则两者的基础对象都会改变:

    ptr1.set_ptr_val(0);
    cout << ptr1.get_ptr_val() << endl;
    cout << ptr2.get_ptr_val() << endl;

两个指针指向同一对象时,其中任意一个都可以改变共享对象的值。


4、可能出现悬垂指针

因为类直接复制指针,会使用户面临潜在的问题:HasPtr保存着给定指针。用户必须保证只要HasPtr对象存在,该指针指向的对象就存在:

    int *ip = new int(42);
    HasPtr ptr(ip,42);
    delete ip;              //会造成悬垂指针
    ptr.set_ptr_val(0);     //Error,但是编译器检测不出来
    cout << ptr.get_ptr_val() << endl;   //Error,但是编译器检测不出来

对该指针指向的对象所做的任意改变都将作用于共享对象。如果用户删除该对象,则类就有一个悬垂指针,指向一个不复存在的对象。

//P421 习题13.20
    int i = 42;
    HasPtr p1(&i,42);
    HasPtr p2 = p1; //调用编译器合成的赋值运算符
                    //复制两个成员
    cout << p2.get_ptr_val() << endl;
    p1.set_ptr_val(1);
    cout << p2.get_ptr_val() << endl;

二、定义智能指针类【可以解决悬垂指针问题】

智能指针除了增加功能外,其行为像普通指针一样。本例中让智能指针负责删除共享对象。用户将动态分配一个对象并将该对象的地址传给新的HasPtr类。用户仍然可以通过普通指针访问对象,但绝不能删除指针。HasPtr类将保证在撤销指向对象的最后一个HasPtr对象时删除对象。

HasPtr在其他方面的行为与普通指针一样。具体而言,复制对象时,副本和原对象将指向同一基础对象,如果通过一个副本改变基础对象,则通过另一对象访问的值也会改变(类似于上例中的普通指针成员)

新的HasPtr类需要一个析构函数来删除指针,但是,析构函数不能无条件地删除指针。如果两个HasPtr对象指向同一基础对象,那么,在两个对象都撤销之前,我们并不希望删除基础对象。为了编写析构函数,需要知道这个HasPtr对象是否为指向给定对象的最后一个。


1、引入使用计数

定义智能指针的通用技术是采用一个使用计数[引用计数]。智能指针类将一个计数器与类指向的对象相关联。使用计数跟踪该类有多少个对象共享同一指针。使用计数为0时,删除对象。

【思想:】

1)每次创建类的新对象,初始化指针并将使用计数置为1

2)当对象作为另一对象的副本而创建时,复制构造函数复制指针并增加与之相应的使用计数的值。

3)对一个对象进行赋值时,赋值操作符减少左操作数所指对象的使用计数的值(如果使用计数减至0,则删除对象),并增加右操作数所指对象的使用计数的值。

4)最后,调用析构函数时,析构函数减少使用计数的值,如果计数减至0,则删除基础对象

唯一的创新在于决定将使用计数放在哪里。计数器不能直接放在HasPtr对象中:

    int obj;
    HasPtr p1(&obj,42);
    HasPtr p2(p1);
    HasPtr p3(p2);

如果使用计数保存在HasPtr对象中,创建p3时怎样更新它 可以在p1中将计数增量并复制到p3,但怎样更新p2中的计数


2、使用计数类

定义一个单独的具体类用以封装使用计数和相关指针:

class U_Ptr
{
    //将HasPtr设置成为友元类,使其成员可以访问U_Ptr的成员
    friend class HasPtr;
    int *ip;
    size_t use;
    U_Ptr(int *p):ip(p),use(1) {}
    ~U_Ptr()
    {
        delete ip;
    }
};

将所有的成员都设置成为private:我们不希望普通用户使用U_Ptr类,所以他没有任何public成员!


U_Ptr 类保存指针和使用计数,每个 HasPtr 对象将指向一个 U_Ptr 对象,使用计数将跟踪指向每个 U_Ptr 对象的 HasPtr 对象的数目。U_Ptr 定义的仅有函数是构造函数和析构函 数,构造函数复制指针,而析构函数删除它。构造函数还将使用计数置为 1,表示一个 HasPtr 对象指向这个 U_Ptr 对象。
假定刚从指向 int 值 42 的指针创建一个 HasPtr 对象,则这些对 象如图所示:

\
如果复制这个对象,则对象如图所示:

\


3、使用计数类的使用

新的HasPtr类保存一个指向U_Ptr对象的指针,U_Ptr对象指向实际的int基础对象:

class HasPtr
{
public:
    HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
    HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val)
    {
        ++ ptr->use;
    }

    HasPtr &operator=(const HasPtr &orig);

    ~HasPtr()
    {
        if ( -- ptr -> use == 0 )
        {
            delete ptr;
        }
    }

private:
    U_Ptr *ptr;
    int val;
};

接受一个指针和一个int值的 HasPtr构造函数使用其指针形参创建一个新的U_Ptr对象。H