在包含指针的类中需要注意复制控制,复制指针时只复制指针中的地址,不会复制指针指向的对象。
大多数c++类采用三种方法管理指针成员:
1)指针成员采用常规指针型行为。
2)采用智能指针
3)采取值型行为
常规指针缺陷:可能会出现悬垂指针。当一个指针复制到另一个指针,两个指针指向同一个对象,当一个指针删除对象时,另一个指针不知道,所以出现悬垂指针。即使使用默认合成复制构造函数也会出现,类本身无法避免。
智能指针:加入了引用计数。引用计数跟踪该类有多少对象共享同一指针。当引用计数为0 时,删除对象。创建新类时,初始化指针并将引用计数置为1.进行复制时,增加相应引用计数值。赋值时,减少左操作数所指对象的引用计数的值(减至0,就删除对象),增加右操作数所指对象的引用计数的值。最后,调用析构函数时,减少引用计数的值。如果减至0,就删除对象。
值型类:复制时会new一个新的副本,指针所指向的对象是唯一的,每个类对象独立管理。
为了管理具有指针成员的类,必须定义三个复制控制成员:复制构造函数,赋值操作符和析构函数。这些成员可以定义指针成员的指针型行为或者值型行为。
c++出现内存问题的地方一般:
1)缓冲区溢出
2)悬垂指针/野指针
3)重复释放
4)内存泄漏
5)不配对的 new[]/delete
都可以通过智能指针很好的解决这些问题,比如:
1)->用vector/string或者自己写的buffer类管理,自动增加缓冲区大小,用成员函数操作,不直接通过野指针操作
2),3),4)->可以通过智能指针解决,只在对象析构的时候释放一次内存,引用计数为0的时候才删除指针,自动释放
5)->使用vector,自己释放内存
智能指针的陷阱:
这样的一个引用计数型智能指针目的是为了防止资源泄漏,但是只需要一个很小巧的代码就可以让这样的初衷化为乌有……
class A { public: A() {cout<<"A CON"<<endl;} ~A() {cout<<"A DES"<<endl;} void hold(CountedPtr<A> ptr) { m_ptr = ptr; } private: CountedPtr<A> m_ptr; }; void self_cir_area() { CountedPtr<A> pA(new A()); pA->hold(pA); }
可以看见,一个对象A中有一个引用计数型智能指针,这样的设计可能会很常见(指向自身类型的结构体——链表)
但是,当自身循环引用发生的时候会怎么样呢? 下面就来看看这么两句代码
CountedPtr<A> pA(new A());
这里我们新建一个资源,并且把这个资源的管理权移交给pA这个引用计数型智能指针对象管理。如此,pA中的引用计数被初始化为1。
pA->hold(pA);
这里,我们把pA对象传入给实例化的A对象中的引用计数型智能指针m_ptr,m_ptr执行这样的一个成员函数:
//assignment (unshare old and share new value) CountedPtr<T>& operator= (const CountedPtr<T>& p) throw() { if (this != &p) { dispose(); ptr = p.ptr; count = p.count; ++*count; } return *this; }
因为这里很明显不是自身赋值,A中的m_ptr和pA不是同一个对象,所以进入if结构中调用下面的内容。dispose是用作清理,因为m_ptr并没有指向任何东西,所以第一个函数并没有真正的意义。
然后
m_ptr.ptr = pA.ptr; m_ptr.count = pA.count; ++(*m_ptr.count); //(*pA.count)也被++
到此,pA的引用计数为2
嗯,下面就pA这个对象理所当然的离开了作用域,调用其析构函数:
~CountedPtr () throw() { dispose(); }
噢,是一个转向,调用其private成员函数dispose():
很简单,将引用计数-1,由2变成1,不为0,所以if结构内的语句不被执行。
由此,我们又制造了一个完美的太空垃圾……
void dispose() { if (--*count == 0) { delete count; delete ptr; } }
这样的循环引用问题应该是在设计的过程中就应该避免的,如果用UML语言描述
A中持有一个 引用计数型智能指针 的语义就是 这个 持有关系 是需要在 A消失的时候所持有的对象也随之消失(这正是智能指针的作用,在脱离作用域自动清除其持有的资源)。如此就构成了 组合 关系。如果要表示 聚合 关系,即有 部分-整体 关系但是部分不随整体的消失而消失,这就不是 智能指针 所表达的语义。
还有可能遇见的循环引用就是 A1 持有 A2, A2 持有 A1 的情况……
这样A1,A2中对双方的引用计数都是2,当一方“销毁”的时候,双方的应用计数都变为1,实际上并没有销毁任何东西,制造了两个完美无暇的太空垃圾~
这里又引发出一个问题,这样的资源泄漏问题实际上还是由程序员自身引起的。
C++之所以是一个很容易出错的语言,很大一部分在于其资源的管理权力全权交给了程序员。这样的权力到底是造福了程序员还是迷惑了程序员呢?