C++ Primer 学习笔记_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 对象,则这些对 象如图所示:
如果复制这个对象,则对象如图所示:
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; };