文档章节

C++(25)管理指针成员

Simon丶Ma
 Simon丶Ma
发布于 2016/04/14 14:28
字数 3487
阅读 3
收藏 0

复制控制

--管理指针成员



引言:

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

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

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

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

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

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


一、定义常规指针类

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class HasPtr  
  2. {  
  3. public:  
  4.     HasPtr(int *p,int i):ptr(p),val(i) {}  
  5.   
  6.     int *get_ptr() const  
  7.     {  
  8.         return ptr;  
  9.     }  
  10.   
  11.     int get_val() const  
  12.     {  
  13.         return val;  
  14.     }  
  15.   
  16.     void set_ptr(int *p)  
  17.     {  
  18.         ptr = p;  
  19.     }  
  20.     void set_val(int i)  
  21.     {  
  22.         val = i;  
  23.     }  
  24.   
  25.     int get_ptr_val()   const  
  26.     {  
  27.         return *ptr;  
  28.     }  
  29.     void set_ptr_val(int i) const  
  30.     {  
  31.         *ptr = i;  
  32.     }  
  33.   
  34. private:  
  35.     int *ptr;  
  36.     int val;  
  37. };  


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

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int obj = 0;  
  2. HasPtr ptr1(&obj,42);  
  3. HasPtr ptr2(ptr1);  

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

【小心地雷】

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


3、指针共享同一对象

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ptr1.set_val(0);  
  2. cout << ptr1.get_val() << endl;  
  3. cout << ptr2.get_val() << endl;  

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ptr1.set_ptr_val(0);  
  2. cout << ptr1.get_ptr_val() << endl;  
  3. cout << ptr2.get_ptr_val() << endl;  

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


4、可能出现悬垂指针

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int *ip = new int(42);  
  2. HasPtr ptr(ip,42);  
  3. delete ip;              //会造成悬垂指针  
  4. ptr.set_ptr_val(0);     //Error,但是编译器检测不出来  
  5. cout << ptr.get_ptr_val() << endl;   //Error,但是编译器检测不出来  

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //P421 习题13.20  
  2.     int i = 42;  
  3.     HasPtr p1(&i,42);  
  4.     HasPtr p2 = p1; //调用编译器合成的赋值运算符  
  5.                     //复制两个成员  
  6.     cout << p2.get_ptr_val() << endl;  
  7.     p1.set_ptr_val(1);  
  8.     cout << p2.get_ptr_val() << endl;  

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

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

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

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


1、引入使用计数

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

【思想:】

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

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

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

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

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. int obj;  
  2. HasPtr p1(&obj,42);  
  3. HasPtr p2(p1);  
  4. HasPtr p3(p2);  

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


2、使用计数类

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class U_Ptr  
  2. {  
  3.     //将HasPtr设置成为友元类,使其成员可以访问U_Ptr的成员  
  4.     friend class HasPtr;  
  5.     int *ip;  
  6.     size_t use;  
  7.     U_Ptr(int *p):ip(p),use(1) {}  
  8.     ~U_Ptr()  
  9.     {  
  10.         delete ip;  
  11.     }  
  12. };  

将所有的成员都设置成为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基础对象:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class HasPtr  
  2. {  
  3. public:  
  4.     HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}  
  5.     HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val)  
  6.     {  
  7.         ++ ptr->use;  
  8.     }  
  9.   
  10.     HasPtr &operator=(const HasPtr &orig);  
  11.   
  12.     ~HasPtr()  
  13.     {  
  14.         if ( -- ptr -> use == 0 )  
  15.         {  
  16.             delete ptr;  
  17.         }  
  18.     }  
  19.   
  20. private:  
  21.     U_Ptr *ptr;  
  22.     int val;  
  23. };  

    接受一个指针和一个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、赋值与使用计数

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. HasPtr &HasPtr::operator=(const HasPtr &rhs)  
  2. {  
  3.     ++ rhs.ptr -> use;  
  4.     if ( -- ptr -> use == 0)  
  5.         delete ptr;  
  6.     ptr = rhs.ptr;  
  7.     val = rhs.val;  
  8.   
  9.     return *this;  
  10. }  

    在这里,首先将右操作数中的使用计数加1,然后将左操作数对象的使用计数减1并检查这个使用计数。像析构函数中那样,如果这是指向U_Ptr对象的最后一个对象,就删除该对象,这会依次撤销int基础对象。将左操作数中的当前值减1(可能撤销该对象)之后,再将指针从rhs复制到这个对象。

    这个赋值操作符在减少左操作数的使用计数之前使rhs的使 用计数加1,从而防止自身赋值。如果左右操作数相同,赋值操作符的效果将是U_Ptr基础对象的使用计数加1之后立即减 1


5、改变其他成员

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class HasPtr  
  2. {  
  3. public:  
  4.     int *get_ptr() const  
  5.     {  
  6.         return ptr -> ip;  
  7.     }  
  8.     int get_val() const  
  9.     {  
  10.         return val;  
  11.     }  
  12.   
  13.     void set_ptr(int *p)  
  14.     {  
  15.         ptr -> ip = p;  
  16.     }  
  17.     void set_val(int i)  
  18.     {  
  19.         val = i;  
  20.     }  
  21.   
  22.     int get_ptr_val() const  
  23.     {  
  24.         return *(ptr -> ip);  
  25.         // or return * ptr->ip;  
  26.     }  
  27.     void set_ptr_val(int i)  
  28.     {  
  29.         * ptr-> ip = i;  
  30.     }  
  31.   
  32. private:  
  33.     U_Ptr *ptr;  
  34.     int val;  
  35. };  

    复制HasPtr对象时,副本和原对象中的指针仍指向同一基础对象,对基础对象的改变将影响通过任一 HasPtr对象所看到的值。然而,HasPtr的用户无须担心悬垂指针。只要他们让HasPtr类负责释放对象,HasPtr类将保证只要有指向基础对象的HasPtr对象存在,基础对象就存在


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

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

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

    值型类将指针成员所指基础值的副本给每个对象。复制构造函数分配新元素并从被复制对象处复制值,赋值操作符撤销所保存的原对象并从右操作数向左操作数复制值,析构函数撤销对象。

    作为定义值型行为或指针型行为的另一选择,是使用称为“智能指针”的一些类。这些类在对象间共享同一基础值,从而提供了指针型行为。但它们使用复制控制技术以避免常规指针的一些缺陷。为了实现智能指针行为,类需要保证基础对象一直存在,直到最后一个副本消失。使用计数是管理智能指针类的通用技术

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


[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //P425 习题13.24  
  2. class U_Ptr  
  3. {  
  4.     friend class HasPtr;  
  5.     int *ip;  
  6.     size_t use;  
  7.     U_Ptr(int *p): ip(p), use(1) { }  
  8.     ~U_Ptr()  
  9.     {  
  10.         delete ip;  
  11.     }  
  12. };  
  13.   
  14. class HasPtr  
  15. {  
  16. public:  
  17.     HasPtr(int *p, int i): ptr(new U_Ptr(p)), val(i) { }  
  18.   
  19.     HasPtr(const HasPtr &orig):  
  20.         ptr(orig.ptr), val(orig.val)  
  21.     {  
  22.         ++ptr->use;  
  23.     }  
  24.     HasPtr& operator=(const HasPtr&);  
  25.   
  26.     ~HasPtr()  
  27.     {  
  28.         if (--ptr->use == 0)  
  29.             delete ptr;  
  30.     }  
  31.   
  32.     int *get_ptr() const  
  33.     {  
  34.         return ptr->ip;  
  35.     }  
  36.     int get_int() const  
  37.     {  
  38.         return val;  
  39.     }  
  40.   
  41.     void set_ptr(int *p)  
  42.     {  
  43.         ptr->ip = p;  
  44.     }  
  45.     void set_int(int i)  
  46.     {  
  47.         val = i;  
  48.     }  
  49.   
  50.     int get_ptr_val() const  
  51.     {  
  52.         return *ptr->ip;  
  53.     }  
  54.     void set_ptr_val(int i)  
  55.     {  
  56.         *ptr->ip = i;  
  57.     }  
  58.   
  59. private:  
  60.     U_Ptr *ptr;  
  61.     int val;  
  62. };  
  63.   
  64. HasPtr& HasPtr::operator=(const HasPtr &rhs)  
  65. {  
  66.     ++rhs.ptr->use;  
  67.     if (--ptr->use == 0)  
  68.         delete ptr;  
  69.     ptr = rhs.ptr;  
  70.     val = rhs.val;  
  71.     return *this;  
  72. }  

三、定义值型类

    复制值型对象时,会得到一个不同的新副本。对副本所作的改变不会反映在原有对象上,反之亦然。(类似于string

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. class HasPtr  
  2. {  
  3. private:  
  4.     HasPtr(const int &p,int i):ptr(new int(p)),val(i) {}  
  5.   
  6.     //复制控制  
  7.     HasPtr(const HasPtr &rhs):ptr(new int(*rhs.ptr)),val(rhs.val) {}  
  8.     HasPtr &operator=(const HasPtr &rhs);  
  9.     ~HasPtr()  
  10.     {  
  11.         delete ptr;  
  12.     }  
  13.   
  14.     int *get_ptr() const  
  15.     {  
  16.         return ptr;  
  17.     }  
  18.   
  19.     int get_val() const  
  20.     {  
  21.         return val;  
  22.     }  
  23.   
  24.     void set_ptr(int *p)  
  25.     {  
  26.         ptr = p;  
  27.     }  
  28.     void set_val(int i)  
  29.     {  
  30.         val = i;  
  31.     }  
  32.   
  33.     int get_ptr_val()   const  
  34.     {  
  35.         return *ptr;  
  36.     }  
  37.     void set_ptr_val(int i) const  
  38.     {  
  39.         *ptr = i;  
  40.     }  
  41.   
  42. public:  
  43.     int *ptr;  
  44.     int val;  
  45. };  

    复制构造函数不再复制指针,它将分配一个新的int对象,并初始化该对象以保存与被复制对象相同的值。每个对象都保存属于自己的int值的不同副本。因为每个对象保存自己的副本,所以析构函数将无条件删除指针

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

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. HasPtr &HasPtr::operator=(const HasPtr &rhs)  
  2. {  
  3.     *ptr = *rhs.ptr;  
  4.     val = rhs.val;  
  5.   
  6.     return *this;  
  7. }  

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


[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //P427 习题13.26、27  
  2. //请参照前面的代码与解析,在此就不再赘述了,O(∩_∩)O谢谢  


[csharp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //习题13.28  
  2. //(1)  
  3. class TreeNode  
  4. {  
  5. public:  
  6.     TreeNode():count(0),left(0),right(0){}  
  7.     TreeNode(const TreeNode &node):value(node.value),count(node.count)  
  8.     {  
  9.         if (node.left)  
  10.         {  
  11.             left = new TreeNode(*node.left);  
  12.         }  
  13.         else  
  14.         {  
  15.             left = 0;  
  16.         }  
  17.   
  18.         if (node.right)  
  19.         {  
  20.             right = new TreeNode(*node.right);  
  21.         }  
  22.         else  
  23.         {  
  24.             right = 0;  
  25.         }  
  26.   
  27.     }  
  28.     ~TreeNode()  
  29.     {  
  30.         if (left)  
  31.             delete left;  
  32.         if (right)  
  33.             delete right;  
  34.     }  
  35.   
  36. private:  
  37.     std::string value;  
  38.     int count;  
  39.     TreeNode *left;  
  40.     TreeNode *right;  
  41. };  

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //(2)  
  2. class BinStrTree  
  3. {  
  4. public:  
  5.     BinStrTree():root(0) {}  
  6.     BinStrTree(const BinStrTree &node)  
  7.     {  
  8.         if (node.root)  
  9.             root = new TreeNode(*node.root);  
  10.         else  
  11.             root = 0;  
  12.     }  
  13.     ~BinStrTree()  
  14.     {  
  15.         if (root)  
  16.             delete root;  
  17.     }  
  18.   
  19. private:  
  20.     TreeNode *root;  
  21. };  

© 著作权归作者所有

Simon丶Ma
粉丝 4
博文 134
码字总数 299850
作品 0
深圳
程序员
私信 提问
C++ 对象资源管理惯用法

原文:C++ 对象资源管理惯用法 作者:Breaker 关于 C++ 对象资源管理的惯用法,note-to-self + keynote + idiom case + cross-reference 式笔记 keyword: RAII, deleter, Two-stage Initiali...

晨曦之光
2012/05/23
171
0
《鸡啄米C++编程入门系列》系列技术文章整理收藏

《鸡啄米C++编程入门系列》已整理成PDF文档,点击可直接下载至本地查阅 https://www.webfalse.com/read/201812.html 文章 鸡啄米:C++编程入门系列之前言 鸡啄米:C++编程入门系列之一(进制...

开元中国2015
2015/06/27
83
0
技术原理:C语言中函数指针数组浅析

发现问题 今天,在阅读Linux内核中关于socket的源代码时,遇到了下面一段代码: 在这段代码中,我们注意到proto_ops结构体的成员包括下面这样的成员变量: 这边是函数指针作为结构体成员变量...

adoryn
2015/04/01
0
0
Effective C++ Notes(读书笔记)

1,视C++为一种语言联邦,大致分为4个部分: A)C。说到底C++仍是以C为基础。区块、语句、预处理器、内置数据类型、数组、指针等等统统来自C。 B)Object-Oriented C++。这部分也就是C with ...

borey
2014/10/12
312
0
C++ 智能指针

智能指针(Smart Pointer),是一个来用存储指向动态分配(堆)对象指针的类。简单的说,它本身是一个类,这个类是用来存储对象指针。 一、智能指针的介绍 智能指针,就是具备指针功能同时提...

长平狐
2013/01/06
814
0

没有更多内容

加载失败,请刷新页面

加载更多

OpenStack 简介和几种安装方式总结

OpenStack :是一个由NASA和Rackspace合作研发并发起的,以Apache许可证授权的自由软件和开放源代码项目。项目目标是提供实施简单、可大规模扩展、丰富、标准统一的云计算管理平台。OpenSta...

小海bug
39分钟前
4
0
DDD(五)

1、引言 之前学习了解了DDD中实体这一概念,那么接下来需要了解的就是值对象、唯一标识。值对象,值就是数字1、2、3,字符串“1”,“2”,“3”,值时对象的特征,对象是一个事物的具体描述...

MrYuZixian
今天
6
0
数据库中间件MyCat

什么是MyCat? 查看官网的介绍是这样说的 一个彻底开源的,面向企业应用开发的大数据库集群 支持事务、ACID、可以替代MySQL的加强版数据库 一个可以视为MySQL集群的企业级数据库,用来替代昂贵...

沉浮_
今天
4
0
解决Mac下VSCode打开zsh乱码

1.乱码问题 iTerm2终端使用Zsh,并且配置Zsh主题,该主题主题需要安装字体来支持箭头效果,在iTerm2中设置这个字体,但是VSCode里这个箭头还是显示乱码。 iTerm2展示如下: VSCode展示如下: 2...

HelloDeveloper
今天
6
0
常用物流快递单号查询接口种类及对接方法

目前快递查询接口有两种方式可以对接,一是和顺丰、圆通、中通、天天、韵达、德邦这些快递公司一一对接接口,二是和快递鸟这样第三方集成接口一次性对接多家常用快递。第一种耗费时间长,但是...

程序的小猿
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部