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

时间:2022-01-02 21:39:57

复印控制

--管理指针成员

引言:

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

将一个指针拷贝到还有一个指针时,两个指针指向同一对象。当两个指针指向同一对象时,可能使用任一指针改变基础对象。相似地,非常可能一个指针删除了一对象时,还有一指针的用户还觉得基础对象仍然存在。指针成员默认具有与指针对象同样的行为。

大多数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 对象,则这些对 象如图所看到的:

C++ Primer 学习笔记_57_类和数据抽象 --管理指针成员
   假设复制这个对象。则对象如图所看到的:

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

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对象。HasPtr构造函数运行完成后,HasPtr对象指向一个新分配的U_Ptr对象,该U_Ptr对象存储给定指针。新U_Ptr中的使用计数为1,表示仅仅有一个HasPtr对象指向它。

复制构造函数从形參复制成员并添加使用计数的值。复制构造函数运行完成后,新创建对象与原有对象指向同一U_Ptr对象,该U_Ptr对象的使用计数加1。

析构函数将检查U_Ptr基础对象的使用计数。

假设使用计数为0,则这是最后一个指向该U_Ptr对象的HasPtr对象,在这种情况下,HasPtr析构函数删除其U_Ptr指针。删除该指针将引起对U_Ptr析构函数的调用,U_Ptr析构函数删除int基础对象。

4、赋值与使用计数

赋值操作符比复制构造函数要复杂一点:

HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
++ rhs.ptr -> use;
if ( -- ptr -> use == 0)
delete ptr;
ptr = rhs.ptr;
val = rhs.val; return *this;
}

在这里,首先将右操作数中的使用计数加1,然后将左操作数对象的使用计数减1并检查这个使用计数。像析构函数中那样,假设这是指向U_Ptr对象的最后一个对象,就删除该对象,这会依次撤销int基础对象

将左操作数中的当前值减1(可能撤销该对象)之后,再将指针从rhs拷贝到这个对象。

这个赋值操作符在降低左操作数的使用计数之前使rhs的使 用计数加1,从而防止自身赋值。假设左右操作数同样,赋值操作符的效果将是U_Ptr基础对象的使用计数加1之后马上减 1。

5、改变其它成员

class HasPtr
{
public:
int *get_ptr() const
{
return ptr -> ip;
}
int get_val() const
{
return val;
} void set_ptr(int *p)
{
ptr -> ip = p;
}
void set_val(int i)
{
val = i;
} int get_ptr_val() const
{
return *(ptr -> ip);
// or return * ptr->ip;
}
void set_ptr_val(int i)
{
* ptr-> ip = i;
} private:
U_Ptr *ptr;
int val;
};

复制HasPtr对象时,副本和原对象中的指针仍指向同一基础对象,对基础对象的改变将影响通过任一 HasPtr对象所看到的值。

然而,HasPtr的用户无须操心悬垂指针。仅仅要他们让HasPtr类负责释放对象,HasPtr类将保证仅仅要有指向基础对象的HasPtr对象存在,基础对象就存在。

【建议:管理指针成员 P425值得细致品读】

具有指针成员的对象一般须要定义复制控制成员。假设依赖合成版本号,会给类的用户添加负担。用户必须保证成员所指向的对象存在,仅仅要还有对象指向该对象

为了管理具有指针成员的类,必须定义三个复制控制成员:复制构造函数、赋值操作符和析构函数。这些成员能够定义指针成员的指针型行为或值型行为。

值型类将指针成员所指基础值的副本给每一个对象。

复制构造函数分配新元素并从被复制对象处复制值,赋值操作符撤销所保存的原对象并从右操作数向左操作数复制值,析构函数撤销对象。

作为定义值型行为或指针型行为的还有一选择,是使用称为“智能指针”的一些类。

这些类在对象间共享同一基础值,从而提供了指针型行为。

但它们使用复制控制技术以避免常规指针的一些缺陷。为了实现智能指针行为,类须要保证基础对象一直存在,直到最后一个副本消失。使用计数是管理智能指针类的通用技术。

管理指针的这些方法用得非常频繁,因此使用带指针成员类的程序猿必须充分熟悉这些编程技术

//P425 习题13.24
class U_Ptr
{
friend class HasPtr;
int *ip;
size_t use;
U_Ptr(int *p): ip(p), use(1) { }
~U_Ptr()
{
delete ip;
}
}; 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&); ~HasPtr()
{
if (--ptr->use == 0)
delete ptr;
} int *get_ptr() const
{
return ptr->ip;
}
int get_int() const
{
return val;
} void set_ptr(int *p)
{
ptr->ip = p;
}
void set_int(int i)
{
val = i;
} int get_ptr_val() const
{
return *ptr->ip;
}
void set_ptr_val(int i)
{
*ptr->ip = i;
} private:
U_Ptr *ptr;
int val;
}; HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++rhs.ptr->use;
if (--ptr->use == 0)
delete ptr;
ptr = rhs.ptr;
val = rhs.val;
return *this;
}

三、定义值型类

复制值型对象时,会得到一个不同的新副本。

对副本所作的改变不会反映在原有对象上,反之亦然。(相似于string)

class HasPtr
{
private:
HasPtr(const int &p,int i):ptr(new int(p)),val(i) {} //复制控制
HasPtr(const HasPtr &rhs):ptr(new int(*rhs.ptr)),val(rhs.val) {}
HasPtr &operator=(const HasPtr &rhs);
~HasPtr()
{
delete ptr;
} 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;
} public:
int *ptr;
int val;
};

复制构造函数不再复制指针。它将分配一个新的int对象,并初始化该对象以保存与被复制对象同样的值。

每一个对象都保存属于自己的int值的不同副本。

由于每一个对象保存自己的副本,所以析构函数将无条件删除指针

赋值操作符也因而不用分配新对象,它仅仅是必须记得给其指针所指向的对象赋新值,而不是给指针本身赋值:

HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
*ptr = *rhs.ptr;
val = rhs.val; return *this;
}

即使要将一个对象赋值给它本身,赋值操作符也必须总是保证正确。本例中,即使左右操作数同样,操作本质上也是安全的,因此,不须要显式检查自身赋值。

//P427 习题13.26、27
//请參照前面的代码与解析,在此就不再赘述了。O(∩_∩)O谢谢
//习题13.28
//(1)
class TreeNode
{
public:
TreeNode():count(0),left(0),right(0){}
TreeNode(const TreeNode &node):value(node.value),count(node.count)
{
if (node.left)
{
left = new TreeNode(*node.left);
}
else
{
left = 0;
} if (node.right)
{
right = new TreeNode(*node.right);
}
else
{
right = 0;
} }
~TreeNode()
{
if (left)
delete left;
if (right)
delete right;
} private:
std::string value;
int count;
TreeNode *left;
TreeNode *right;
};

//(2)
class BinStrTree
{
public:
BinStrTree():root(0) {}
BinStrTree(const BinStrTree &node)
{
if (node.root)
root = new TreeNode(*node.root);
else
root = 0;
}
~BinStrTree()
{
if (root)
delete root;
} private:
TreeNode *root;
};

版权声明:本文博主原创文章,博客,未经同意不得转载。