写时拷贝(Copy On Write)方案详解
本文旨在通过对 写时拷贝 的四个方案(Copy On Write)分析,让大家明白写时拷贝的实现及原理。
关于浅拷贝与深拷贝,我在之前的博客中已经阐述过了
浅拷贝容易出现指针悬挂的问题,深拷贝效率低,但是我们可以应用引用计数来解决浅拷贝中多次析构的问题,写时拷贝也就应运而生了。
首先要清楚写时拷贝是利用浅拷贝来解决问题!!
方案一
1
2
3
4
5
6
|
class
String
{
private
:
char
* _str;
int
_refCount;
};
|
方案一最不靠谱,它将用作计数的整形变量_refCount定义为类的私有成员变量,任何一个对象都有它自己的成员变量_refCount,它们互不影响,难以维护。只要拷贝出了对象,_refCount大于了0,每个对象在调用自己的析构函数时–_refCount不等于0,那么它们指向的那块内存都将得不到释放,无法达到我们要的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
//以下是对方案一的简单实现,大家可以结合上图感受到方案一的缺陷
class
String
{
public
:
String(
char
* str =
""
)
//不能strlen(NULL)
:_refCount(0)
{
_str =
new
char
[
strlen
( str) + 1];
strcpy
(_str, str);
_refCount++;
}
String(String &s)
:_refCount( s._refCount)
{
_str = s._str;
_refCount++;
s._refCount = _refCount;
//这里虽然可以让两个对象的_refCount相等,
//但如果超过两个对象的_str指针都指向同一块内存时,
//就无法让所有对象的_refCount都保持一致
//这是方案一的缺陷之一
}
~String()
{
if
(--_refCount == 0)
{
delete
[] _str;
_str = NULL;
cout <<
"~String "
<< endl;
}
}
friend
ostream& operator<<( ostream& output,
const
String &s);
private
:
char
* _str;
int
_refCount;
};
ostream& operator<<( ostream& output,
const
String & s)
{
output << s._str;
return
output;
}
void
Test()
{
String s1(
"aaa"
);
String s2(s1);
String s3(s2);
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
|
方案二
1
2
3
4
5
6
|
class
String
{
private
:
char
* _str;
static
int
count;
};
|
设置了一个静态整形变量来计算指向一块内存的指针的数量,每析构一次减1,直到它等于0(也就是没有指针在指向它的时候)再去释放那块内存,看似可行,其实不然!
这个方案只适用于只调用一次构造函数、只有一块内存的情形,如果多次调用构造函数构造对象,新构造的对象照样会改变count的值,那么以前的内存无法释放会造成内存泄漏。
结合上图和下面的代码,我们可以清楚地看到该方案相比方案一的改善,以及缺陷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
class
String
{
public
:
String(
char
* str =
""
)
//不能strlen(NULL)
{
_str =
new
char
[
strlen
( str) + 1];
strcpy
(_str, str);
count++;
}
String(
const
String &s)
{
_str = s._str;
count++;
}
String& operator=( String& s)
{
_str = s._str;
count++;
return
*
this
;
}
~String()
{
if
(--count == 0)
{
delete
[] _str;
_str = NULL;
cout <<
"~String "
<< endl;
}
}
friend
ostream& operator<<( ostream& output,
const
String &s);
friend
istream& operator>>( istream& input,
const
String &s);
private
:
char
* _str;
static
int
count;
};
int
String::count = 0;
//初始化count
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void
Test()
//用例测试
{
String s1(
"abcdefg"
);
String s2(s1);
String s3;
s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
String s4(
"opqrst"
);
String s5(s4);
String s6 (s5);
s6 = s4;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
}
|
方案三
问题的关键是,我们不是要为每一个对象建立一个引用计数,而是要为每一块内存设置一个引用计数,只有这样才方便我们去维护。当指向这块内存的指针数为0时,再去释放它!
1
2
3
4
5
6
|
class
String
{
private
:
char
* _str;
int
* _refCount;
};
|
方案三设置了一个int型的指针变量用来引用计数,每份内存空间对应一个引用计数,而不是每个对象对应一个引用计数,而且内存之间的引用计数互不影响,不会出现方案一和方案二出现的问题。
1.在实现赋值运算符重载时要谨慎,不要遇到下图的情形
s1指向内存1,s2指向内存2,利用s2拷贝出的对象s3也指向内存块2,这时候内存块1的引用计数等于1 ,内存块2的引用计数等于2。一切似乎都很正常,但是调用赋值运算符重载执行语句:s2=s1后,错误慢慢显现出来了。将s2指向内存1 并把内存1 的引用计数加1,这理所当然,但是不能把s2原本指向的空间直接delete,s3还指向内存2着呢!这里千万在释放一块空间前,对指向这块内存的引用计数进行检查,当引用计数为0的时候再去释放,否则只做减引用计数就行。
1
2
3
4
5
6
7
8
9
10
11
12
|
//错误代码<br>String& operator=(String& s)
{
if
(_str!= s._str)
{
delete
[] _str;
delete
_refCount;
_str = s._str;
_refCount = s._refCount;
(*_refCount)++;
}
return
*
this
;
}
|
2.改变字符串的某个字符时要谨慎,不要遇到类似下图所遇到的问题。
如果多个对象都指向同一块内存,那么只要一个对象改变了这块内存的内容,那所有的对象都被改变了!!
如下图:当s1和s2都指向内存块1,s3经过赋值运算符重载后也指向内存块1,现在s2如果对字符串进行修改后,所有指向内存块1 的指针指向的内容都会被改变!
可以用下图的形式改善这种问题:新设置一块内存来存要改变的对象,这样就不会影响其他的对象了
案例3我画的图较多,方便大家结合代码去理解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
//案例三<br>class String
{
public
:
String(
char
* str =
""
)
//不能strlen(NULL)
{
_refCount =
new
int
(1);
//给_refCount开辟空间,并赋初值1
_size =
strlen
(str);
_capacity = _size + 1;
_str =
new
char
[
strlen
(str) + 1];
strcpy
(_str, str);
}
String(
const
String &s)
{
_refCount = s._refCount;
_str = s._str;
_size =
strlen
(s._str);
_capacity = _size + 1;
(*_refCount)++;
//拷贝一次_refCount都要加1
}
//要考虑是s1=s2时,s1原先不为空的情况,要先释放原内存
//如果要释放原内存时,要考虑它的_refCount减1后是否为0,为零再释放,否则其它对象指针无法再访问这片空间
String& operator=(String& s)
{
if
(_str!= s._str)
{
_size =
strlen
(s._str);
_capacity = _size + 1;
if
(--(*_refCount) == 0)
{
delete
[] _str;
delete
_refCount;
}
_str = s._str;
_refCount = s._refCount;
(*_refCount)++;
}
return
*
this
;
}<br>
//如果修改了字符串的内容,那所有指向这块内存的对象指针的内容间接被改变
//如果还有其它指针指向这块内存,我们可以从堆上重新开辟一块内存空间,
//把原字符串拷贝过来
//再去改变它的内容,就不会产生链式反应
// 1.减引用计数 2.拷贝 3.创建新的引用计数
char
& String::operator[](
const
size_t
index)
//参考深拷贝
{
if
(*_refCount==1)
{
return
*(_str + index);
}
else
{
--(*_refCount);
char
* tmp =
new
char
[
strlen
(_str)+1];
strcpy
(tmp, _str);
_str = tmp;
_refCount =
new
int
(1);
return
*(_str+index);
}
}
~String()
{
if
(--(*_refCount)== 0)
//当_refCount=0的时候就释放内存
{
delete
[] _str;
delete
_refCount;
_str = NULL;
cout <<
"~String "
<< endl;
}
_size = 0;
_capacity = 0;
}
friend
ostream& operator<<(ostream& output,
const
String &s);
friend
istream& operator>>(istream& input,
const
String &s);
private
:
char
* _str;
//指向字符串的指针
size_t
_size;
//字符串大小
size_t
_capacity;
//容量
int
* _refCount;
//计数指针
};
ostream& operator<<(ostream& output,
const
String &s)
{
output << s._str;
return
output;
}
istream& operator>>(istream& input,
const
String &s)
{
input >> s._str;
return
input;
}
void
Test()
//用例测试
{
String s1(
"abcdefg"
);
String s2(s1);
String s3;
s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
s2[3] =
'0'
;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
|
方案四
1
2
3
4
5
|
class
String
{
private
:
char
* _str;
};
|
方案四与方案三类似。方案四把用来计数的整型指针变量放在所开辟的内存空间的首部。
用*((int*)_str)就能取得计数值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
class
String
{
public
:
String(
char
* str =
""
)
//不能strlen(NULL)
{
_str =
new
char
[
strlen
( str) + 5];
_str += 4;
strcpy
(_str, str);
GetRefCount(_str) = 1;
}
String(
const
String &s)
{
_str = s._str;
++GetRefCount(_str);
}
//要考虑是s1=s2时,s1原先不为空的情况,要先释放原内存
//如果要释放原内存时,要考虑它的_refCount减1后是否为0,
//为零再释放,否则其它对象指针无法再访问这片空间
String& operator=(String& s)
{
if
(
this
!= &s )
{
if
(GetRefCount(_str ) == 1)
{
delete
(_str-4);
_str = s._str;
++GetRefCount(_str );
}
else
{
--GetRefCount(_str );
_str = s._str;
++GetRefCount(_str );
}
}
return
*
this
;
}
//如果修改了字符串的内容,那所有指向这块内存的对象指针的内容间接被改变
//如果还有其它指针指向这块内存,我们可以从堆上重新开辟一块内存空间,
//把原字符串拷贝过来.
//再去改变它的内容,就不会产生链式反应
char
& String ::operator[](
const
size_t
index )
//深拷贝
{
if
(GetRefCount(_str) == 1)
{
return
_str[index ];
}
else
{
// 1.减引用计数
--GetRefCount(_str );
// 2.拷贝 3.创建新的引用计数
char
* tmp =
new
char
[
strlen
(_str) + 5];
*((
int
*)tmp) = 1;
tmp += 4;
strcpy
(tmp, _str);
_str = tmp;
return
_str[index ];
}
}
int
& GetRefCount(
char
* ptr)
//获取引用计数(隐式内联函数)
{
return
*((
int
*)(ptr -4));
}
~String()
{
if
(--GetRefCount(_str) == 0)
{
cout <<
"~String"
<< endl;
delete
[] (_str-4);
}
}
friend
ostream& operator<<( ostream& output,
const
String &s);
friend
istream& operator>>( istream& input,
const
String &s);
private
:
char
* _str;
};
ostream& operator<<(ostream& output,
const
String &s)
{
output << s._str;
return
output;
}
istream& operator>>(istream& input,
const
String &s)
{
input >> s._str;
return
input;
}
void
Test()
//用例测试
{
String s1(
"abcdefg"
);
String s2(s1);
String s3;
s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
s2[3] =
'0'
;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
|