复印控制

--管理指针成员

引言:

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

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

大多数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对象。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;
};

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

C++ Primer 学习笔记_57_类和数据抽象 --管理指针成员的更多相关文章

  1. C++ Primer 学习笔记_53_类和数据抽象 --友元、static员

    分类 --友元.static成员 一.友元 友元机制同意一个类将对其.友元关系:一个样例 如果一个窗体管理类Window_Mgr可能须要訪问由其管理的Screen对象的内部数据.Screen应该同意其 ...

  2. C++ Primer 学习笔记_56_ 类和数据抽象 --消息处理演示示例

    拷贝控制 --消息处理演示样例 说明: 有些类为了做一些工作须要对复制进行控制. 为了给出这种样例,我们将概略定义两个类,这两个类可用于邮件处理应用程序.Message类和 Folder类分别表示电子 ...

  3. C++ Primer 学习笔记_54_类和数据抽象 --拷贝构造函数、赋值运算符

    拷贝控制 --复制构造函数.赋值操作符 引言: 当定义一个新类型时,须要显式或隐式地指定复制.赋值和撤销该类型的对象时会发生什么– 复制构造函数.赋值操作符和析构函数的作用!      复制构造函数: ...

  4. C++ Primer学习笔记(三) C++中函数是一种类型!!!

    C++中函数是一种类型!C++中函数是一种类型!C++中函数是一种类型! 函数名就是变量!函数名就是变量!函数名就是变量! (---20160618最新消息,函数名不是变量名...囧) (---201 ...

  5. C++ Primer学习笔记(二)

    题外话:一工作起来就没有大段的时间学习了,如何充分利用碎片时间是个好问题. 接  C++ Primer学习笔记(一)   27.与 vector 类型相比,数组的显著缺陷在于:数组的长度是固定的,无法 ...

  6. Java学习笔记——File类之文件管理和读写操作、下载图片

    Java学习笔记——File类之文件管理和读写操作.下载图片 File类的总结: 1.文件和文件夹的创建 2.文件的读取 3.文件的写入 4.文件的复制(字符流.字节流.处理流) 5.以图片地址下载图 ...

  7. python学习笔记4_类和更抽象

    python学习笔记4_类和更抽象 一.对象 class 对象主要有三个特性,继承.封装.多态.python的核心. 1.多态.封装.继承 多态,就算不知道变量所引用的类型,还是可以操作对象,根据类型 ...

  8. Java学习笔记之---类和对象

    Java学习笔记之---类和对象 (一)类 类是一个模板,它描述一类对象的行为和状态  例如:动物类是一个类,动物们都有属性:颜色,动物们都有行为:吃饭 public class Dog { Stri ...

  9. UML学习笔记:类图

    UML学习笔记:类图 有些问题,不去解决,就永远都是问题! 类图 类图(Class Diagrame)是描述类.接口以及它们之间关系的图,用来显示系统中各个类的静态结构. 类图包含2种元素:类.接口, ...

随机推荐

  1. Linux系统管理员必备的监控工具(88款)

    随着互联网行业的不断发展,各种监控工具多得不可胜数.这里列出网上最全的监控工具.让你可以拥有超过80种方式来管理你的机器.在本文中,我们主要包括以下方面: 命令行工具 网络相关内容 系统相关的监控工具 ...

  2. 暂停和屏蔽右键网页中的Flash

    如何暂停网页中的Flash?原理很简单,就是屏蔽Flash的消息即可.屏蔽右键也可以通过此方法 直接贴代码吧,加了注释,很容易就能懂了 新建工程,加一个WebBrowser,再加两个按钮.Flash ...

  3. php网站共享session方法(相同一级域名)

    这段时间做web开发使用的是php语言 要实现从主站进入子站时无需再登录(如已登录) 使用memcache实现 方法如下 修改php.ini如下 添加 extension=php_memcache.d ...

  4. Struts2 Action接收表单参数

    struts2 Action获取表单传值    1.通过属性驱动式    JSP:        <form action="sys/login.action" method ...

  5. 初步掌握Yarn的架构及原理(转)

    1.YARN 是什么? 从业界使用分布式系统的变化趋势和 hadoop 框架的长远发展来看,MapReduce的 JobTracker/TaskTracker 机制需要大规模的调整来修复它在可扩展性, ...

  6. poj 1872 A Dicey Problem WA的代码,望各位指教!!!

    A Dicey Problem Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 832   Accepted: 278 Des ...

  7. XML实例文档

    from: http://www.w3school.com.cn/xpath/xpath_examples.asp XML实例文档 我们将在下面的例子中使用这个 XML 文档: "books ...

  8. POJ1789 Truck History 【最小生成树Prim】

    Truck History Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 18981   Accepted: 7321 De ...

  9. 实现ListView A~Z快速索引

    ListView A~Z快速索引这种效果在通信录和城市列表中经常看到,方便用户查找,是一种增加用户体验的好方法. 实现步骤: 1.自定义一个名叫SlideBar 的View. 2.在布局文件中加入这个 ...

  10. sqlit使用要点之引入libsqlite3.dylib

    怎样引入libsqlite3.dylib? 在.h 或 .cpp文件里包括相应的头文件