[1] More Effective C++ (智能指针)
【1】认识智能指针
什么是智能指针?根据C++面向对象编程的思想,智能指针是一种对象。
常规指针与智能指针的区别如下:
对比发现常规指针pd的问题在于,不是有析构函数的类对象。
如果它是个对象,则可以在对象过期时,它的析构函数删除掉指向的内存。而这正是智能指针背后的思想。
好吧!那么,既然是对象,肯定就有对应的类。所构建智能指针对象的类是智能指针的核心内容。
简而言之,所构建智能指针对象的类是有且仅有一个指针成员变量的类。
智能指针的潜规则是所谓的这个指针成员变量肯定指向一个在堆上创建的对象。
所以,智能指针本质是由存储动态分配(堆上创建)对象指针的类所构建的对象(下面称堆上所创建的那个对象为基础对象)。
主要用于基础对象的生存期控制,能够确保自动正确的销毁基础对象(动态分配的对象),防止内存泄露。
它的一种通用实现技术是使用引用计数(reference count)。
即是智能指针类将一个计数器与类指向的基础对象彼此关联,引用计数跟踪该类有多少个对象共享同一指针(即就是指向同一块内存)。
通过构造函数每次创建类的新对象时,初始化指针值并将引用计数默认先置为1。
通过拷贝构造函数每次创建类的新对象时,拷贝构造函数将拷贝指针并增加与之相应的引用计数(每次加1)。
通过赋值构造函数每次创建类新对象时:
赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数。
当调用析构函数时,析构函数判断引用计数(如果引用计数减至0,则删除基础对象),执行对应的操作。
【2】为什么我们需要智能指针?
当生成一个对象的时候,如果这个类很大,这个对象会占用很多空间。
那么每生成一个对象就需要占用一片空间,这样会占用很多系统资源。同时降低效率。
一个解决方法就是当用拷贝构造函数新创建一个对象时:
让它不存储数据,而只存储一个指向原来对象数据的指针(其实也就是我们所说的浅拷贝)。 这样空间就节省了很多。
但问题在于,这样的话两个object完全联结在了一起。如果修改了其中一个,另一个也跟着变了。所以这种方法不可取。
那么,下面讲的技术就是解决这类问题的方法。
当通过一个已有对象通过拷贝构造创建新对象时,新object只有一个指向已有object的指针。这两个object共享数据。
直到其中一个需要修改数据的时候,再把这两块数据分离。
这里举一个最简化的例子:假设一个class叫 CLargeObject,里面存有很多数据。
我们用一个inner class来把所有数据放在一起,叫CData。CData里面存有大量数据,例如一个数据库。
这里用最简单的模型来表示,假设只有一个整数int m_nVal。
CData里面需要包含另一个变量,叫作索引数目(reference count)。
它记录了总共有多少CLargeObject的object正在引用着当前的CData object。
示例代码如下:
1 #include<iostream>
2 using namespace std;
3 class CLargeObject
4 {
5
6 public:
7 CLargeObject(int nVal);
8 CLargeObject(const CLargeObject & ob);
9 ~CLargeObject();
10 CLargeObject & operator = (const CLargeObject &ob);
11 void SetVal(int nNewVal);
12 int GetVal()const;
13
14 private:
15 struct Data
16 {
17 public:
18 Data(int nVal):m_nVal(nVal),m_nReferenceCount(1)
19 {}
20 private:
21 friend class CLargeObject;
22 Data *get_own_copy()
23 {
24 if(m_nReferenceCount == 1)
25 return this;
26 m_nReferenceCount--;
27 return new Data(m_nVal);
28 }
29 int m_nReferenceCount;
30 int m_nVal;
31 };
32 Data *m_pData;
33
34 };
35
36 CLargeObject::CLargeObject(int nVal)
37 {
38 m_pData = new Data(nVal);
39 }
40 CLargeObject::CLargeObject(const CLargeObject &ob)
41 {
42 ob.m_pData->m_nReferenceCount++;
43 m_pData = ob.m_pData;
44 }
45 CLargeObject::~CLargeObject()
46 {
47 if(--m_pData->m_nReferenceCount == 0)
48 delete m_pData;
49 }
50 CLargeObject & CLargeObject::operator =(const CLargeObject &ob)
51 {
52 ob.m_pData->m_nReferenceCount++;
53 if(--m_pData->m_nReferenceCount == 0)
54 delete m_pData;
55 m_pData = ob.m_pData;
56 return *this;
57 }
58 void CLargeObject::SetVal(int nNewVal)
59 {
60 m_pData = m_pData->get_own_copy();
61 m_pData->m_nVal = nNewVal;
62 }
63 int CLargeObject::GetVal() const
64 {
65 return m_pData->m_nVal;
66 }
67 void main()
68 {
69 CLargeObject obj1(10);
70 CLargeObject obj2(obj1);
71 obj2.SetVal(22);
72 cout<<obj1.GetVal()<<endl; //10
73 cout<<obj2.GetVal()<<endl; //22
74 }
36~39行:构造函数,建立堆上的对象
40~44行:拷贝构造函数,仅仅引用计数加1,通过赋值语句完成数据共享
50~57行:赋值函数
1 CLargeObject & CLargeObject::operator =(const CLargeObject &ob)
2 {
3 ob.m_pData->m_nReferenceCount++;
4 if(--m_pData->m_nReferenceCount == 0)
5 delete m_pData;
6 m_pData = ob.m_pData;
7 return *this;
8 }
深度分析:
<1> 为了防止自赋值(即就是如果两个对象相同),
在对左对象(或左操作数)的使用计数操作之前使实参对象的使用计数先加1
若恰相同,if语句必再进行减1操作,意味着计数前后没有改变,达到目的
若不相同,意味着实参对象计数加1,被赋值对象(左操作数)计数减1,也成立
然后相应成员进行赋值
<2> 如果左对象的使用计数减至0,那么赋值操作符必须删除它之前所指向的对象,
再进行相应成员赋值
45~49行:析构函数,通过计数判断是否为最后一次删除。若是,摧毁共享数据;若非,仅仅计数减1,达到目的
58~62行:设置值函数,可以看出其中调用了函数get_own_copy()
1 Data *get_own_copy()
2 {
3 if(m_nReferenceCount == 1)
4 return this;
5 m_nReferenceCount--;
6 return new Data(m_nVal);
7 }
60行:m_pData = m_pData->get_own_copy();
左操作数的指针成员(m_pData)很关键。
若引用计数为1,说明该共享数据仅仅被引用了一次,那么,修改值就仅仅需要在原内存中进行更改,所以,返回this指针,即本身指针
若引用计数大于1,说明该共享数据不仅被一个对象引用。那么,修改该值的就要统筹兼顾,顾全大局,步骤如下:
首先:保证原数据块依然存在
另外:使引用计数减1
最后:建立一个新的数据块
函数结束,返回新数据块的指针赋值给m_pData
即就是实现了所谓的分离
61行:m_pData->m_nVal = nNewVal; 通过指针实现新数据的输入。
63~66行:取值函数。之所以定义为常函数是因为防止在函数内部m_pData被修改。
【3】坊间流传部分代码(整理篇)
1 #include<iostream>
2 using namespace std;
3
4 class U_Ptr
5 {
6 friend class HasPtr; //友元类
7
8 int *ip;
9 size_t use; //计数器
10
11 U_Ptr(int *p=NULL):ip(p),use(1)
12 {
13 }
14 ~U_Ptr()
15 {
16 delete ip;
17 cout<<"U_Ptr"<<this<<endl;
18 ip = NULL;
19 }
20 };
21
22 class HasPtr
23 {
24 private:
25 U_Ptr *ptr;
26 int val;
27 public:
28 ////////////////////构造函数
29 HasPtr(int *ip = NULL, int i = 0):ptr(new U_Ptr(ip)),val(i)
30 {
31 cout<<"construct"<<this<<endl;
32 }
33 ///////////////////拷贝构造函数
34 HasPtr(const HasPtr & orig):ptr(orig.ptr),val(orig.val)
35 {
36 cout<<"copy construct"<<this<<endl;
37 ++ptr->use;
38 }
39 ////////////////赋值函数
40 //赋值意味着左操作对象已经存在
41 //赋值分为自身赋值和非自身赋值
42 //自身赋值:尽量避免。若此例中自身赋值,分析如下:
43 //首先,各自计数均加1;其次,左操作对象计数减1,若为0,则delete;否则对应成员赋值。
44 //非自身赋值:一般必做判断if(this != &rhs)此例中分析如下:
45 //首先,有操作对象计数加1;其次,左操作对象计数减1,若为0,则delete;否则对应成员赋值。
46 HasPtr & operator =(const HasPtr &rhs)
47 {
48 cout<<"operator = "<<this<<endl;
49 ++rhs.ptr->use; //右操作数的计数加1
50 if(--ptr->use == 0) //如果左操作数的引用计数减为0,则执行delete
51 {
52 delete ptr;
53 cout<<"operator = ptr->use == 0"<<" "<<this<<endl;
54 }
55 ptr = rhs.ptr;
56 val = rhs.val;
57 return *this;
58 }
59 //防止自身赋值
60 //如果是自身赋值,那么意味着引用计数应该不会改变,仅仅只是赋值
61 //如果非自身赋值,那么右操作数的计数加1。左操作数的计数减1,若减后为0,则执行delete.
62 ///////////////析构函数
63 ~HasPtr()
64 {
65 if(--ptr->use == 0)
66 {
67 delete ptr;
68 cout<<"delete ptr"<<" "<<this<<endl;
69 }
70 cout<<"~ HasPtr()"<<this<<" "<<this->ptr->use<<endl;
71 ptr = NULL;
72 }
73
74 int *get_ptr() const
75 {
76 return ptr->ip;
77 }
78 int get_int() const
79 {
80 return val;
81 }
82 void set_ptr(int *p)
83 {
84 ptr->ip = p;
85 }
86 void set_int(int i)
87 {
88 val = i;
89 }
90 int get_ptr_val() const
91 {
92 return *(ptr->ip);
93 }
94 void set_ptr_val(int i)
95 {
96 *ptr->ip = i;
97 }
98
99 };
100
101 void main()
102 {
103
104 int *p = new int(1);
105 HasPtr obj1(p,100);
106 cout<<obj1.get_int()<<endl;
107 cout<<obj1.get_ptr()<<endl;
108 cout<<obj1.get_ptr_val()<<endl;
109
110 HasPtr obj2(obj1); //拷贝构造函数
111 cout<<obj2.get_int()<<endl;
112 cout<<obj2.get_ptr()<<endl;
113 cout<<obj2.get_ptr_val()<<endl;
114
115 HasPtr obj3 = obj1; //拷贝构造函数
116 cout<<obj3.get_int()<<endl;
117 cout<<obj3.get_ptr()<<endl;
118 cout<<obj3.get_ptr_val()<<endl;
119
120 HasPtr obj4;
121 obj4 = obj1; //赋值函数
122 cout<<obj4.get_int()<<endl;
123 cout<<obj4.get_ptr()<<endl;
124 cout<<obj4.get_ptr_val()<<endl;
125
126 obj4 = obj4; //自身赋值语句 检测赋值函数作用
127
128
129 int *s = new int(20);
130 cout<<s<<endl;
131 obj1.set_ptr(s);
132 cout<<obj1.get_ptr()<<endl;
133 cout<<obj2.get_ptr()<<endl;
134 cout<<obj3.get_ptr()<<endl;
135
136 obj1.set_int(99);
137 obj2.set_int(88);
138 obj3.set_int(77);
139 cout<<obj1.get_int()<<endl;
140 cout<<obj2.get_int()<<endl;
141 cout<<obj3.get_int()<<endl;
142
143 obj1.set_ptr_val(66);
144 obj1.set_ptr_val(55);
145 obj1.set_ptr_val(44);
146 cout<<obj1.get_ptr_val()<<endl;
147 cout<<obj2.get_ptr_val()<<endl;
148 cout<<obj3.get_ptr_val()<<endl;
149 }
150 /*
151 *运行结果:
152 construct003EFEF0
153 100
154 00724B48
155 1
156 copy construct003EFEE0
157 100
158 00724B48
159 1
160 copy construct003EFED0
161 100
162 00724B48
163 1
164 construct003EFEC0
165 operator = 003EFEC0
166 U_Ptr00724C20
167 operator = ptr->use == 0 003EFEC0
168 100
169 00724B48
170 1
171 operator = 003EFEC0
172 00724C20
173 00724C20
174 00724C20
175 00724C20
176 99
177 88
178 77
179 44
180 44
181 44
182 ~ HasPtr()003EFEC0 3
183 ~ HasPtr()003EFED0 2
184 ~ HasPtr()003EFEE0 1
185 U_Ptr00724B88
186 delete ptr 003EFEF0
187 ~ HasPtr()003EFEF0 4277075694
188 */
分析过程:
第一:需要定义一个单独的具体类用以封装使用计数和相关指针,
把U_ptr类的所有成员定义为private,因为我们不希望普通用户使用U_ptr类
第二:将HasPtr类设置为其友元
第三:每个HasPtr对象将指向一个U_Ptr对象,使用计数机制跟踪指向每个U_Ptr对象的HasPtr对象的数目。
第四:U_Ptr类定义的仅有函数为构造函数和析构函数,构造函数复制指针,并且将使用计数设置为1,表示一个HasPtr对象指向这个U_Ptr对象,析构函数释放指针
第五:HasPtr类的赋值函数很经典:
1> 为了防止自身赋值,在减少左操作数的使用计数之前使rhs的使用计数加1
即就是:如果左右操作数相同,赋值操作符的效果将是U_Ptr基础对象的使用计数加一之后立即减一,保持不变
2> 如果左操作数的使用计数减至0,那么赋值操作符必须删除它所指向的对象
Good Good Study, Day Day Up.
顺序 选择 循环 坚持