这两天复习C++ Primer时,在“复制控制”这一章看到管理指针成员,在没使用标准库的情况下自己创建一个智能指针。不过书中还是建议使用标准库unique_ptr、shared_ptr、weak_ptr实现,减少程序员内存管理问题的工作。
指针成员默认具有与指针对象同样的行为,但通过不同的复制控制策略,可以为指针成员实现不同的行为。多数C++类采用以下三种方法管理指针成员:
(1)指针成员采用常规指针行为。这样的类具有指针的所有缺陷但不需要自定义特殊的复制控制函数;
(2)类实现自定义的“智能指针”行为。指针所指向的对象是共享的,但能够防止悬垂指针问题;
(3)类采取值型行为。类的指针所指向的对象是唯一,复制控制采取深拷贝策略;
为了阐明方法(2),接下来设计一个简单的类Simple.
class Simple{
public:
Simple(int *p, int i):ptr(p), val(i) {};
int *get_ptr() const { return ptr;};
int get_int() const { return val;};
void set_ptr(int *p) { ptr = p;};
void set_int(int i) { val = i;};
int get_ptr_val() const { return *ptr;};
void set_ptr_val(int val) const { *ptr = val};
private:
int *ptr;
int val;
};
因为Simple类没有定义复制构造函数,所以复制一个Simple对象将简单的复制两个数据成员:
int obj = 0;
Simple s1(&obj, 42);
Simple s2(s1);
s1.set_ptr_val(42);
s2.get_ptr_val(); // return 42
s1与s2的指针指向同一个对象,在任意一个对象上调用set_ptr_val()都会改变所指向的基础对象的值。
这种简单的指针型行为会使用户面临潜在的问题:悬垂指针。
int *ip = new int(42);
Simple s1(ip, 10);
Simple s2(s1);
delete s1;
s2.set_ptr_val(0); // disater: the object to which s2 points was freed!
解决指针悬垂问题的一个通用方法是采用引用计数。我们将智能指针单独设计成一个智能指针类,它使用一个计数器与类指向的元数据对象相关联,计数器跟踪该类有多少个对象共享同一个指向元数据的指针。当引用计数为0时,删除元数据对象。
定义一个单独的具体类来封装引用计数和相关指针:
class U_ptr{
friend class Simple;
int *ip;
size_t use_count;
U_ptr(int *p): ip(p), use_count(1) {}
~U_ptr() {delete ip;}
};
U_ptr类仅仅保存指向元数据的指针和引用计数。使用U_ptr改写Simple类:
class Simple{
public:
Simple(int *p, int i): ptr(new U_ptr(p)), val(i) {}
Simple(const Simple &orig): ptr(orig.ptr), val(orig.val) {}
Simple& operator=(const Simple&);
~Simple() {
if(--ptr->use_count == 0)
delete ptr;
}
private:
U_ptr *ptr;
int val;
};
Simple& Simple::operator= (const Simple& rhs)
{
++rhs.ptr->use_count;
if( --ptr->use_count == 0)
delete ptr;
ptr = rhs.ptr;
val = rhs.val;
return *this;
}
Simple的构造函数执行后,Simple对象含有一个指向新U_ptr对象的指针,新的U_ptr对象中引用计数为1,表示只有一个Simple对象指向它。
复制构造函数从形参复制成员并增加引用计数值。复制构造函数执行完毕后,新创建的Simple对象与原对象指向同一个U_ptr对象,该U_ptr对象的引用计数加1。
析构函数将检查U_ptr对象的引用计数,如果为0则删除U_ptr指针,删除该指针将引起U_ptr析构函数的调用,最终删除元数据对象。
赋值操作符稍微复杂点,首先将右操作数的引用计数加1,然后将左操作数(this)的引用计数减1并检查该计数是否为0,是则直接删除之前指向的U_ptr对象。最后将右操作数rhs的数据成员依次复制过来。
但是上述基于引用计数的指针管理方法还是有弊端的,比如循环引用问题,在树、图等数据结构中是常见的。比如在树结构中,父节点和子节点相互引用,导致每个对象的引用计数都不为0. 此时采用标准库中的std::weak_ptr可以解决问题。同理在python中可以参考弱引用的方法python中的弱引用weakref。