本文旨在通過對 寫時拷貝 的四個方案(Copy On Write)分析,讓大家明白寫時拷貝的實現及原理。
深拷貝效率低,我們可以應引用計數的方式去解決淺拷貝中析構多次的問題。
首先要清楚寫時拷貝是利用淺拷貝來解決問題!!
方案一
class String
{
private:
char* _str;
int _refCount;
};
方案一最不靠譜,它將用作計數的整形變量_refCount定義為類的私有成員變量,任何一個對象都有它自己的成員變量_refCount,它們互不影響,只要拷貝出了對象,_refCount大於了1,那麼每個對象調用自己的析構函數時--_refCount不等於0,那麼它們指向的那塊內存都將得不到釋放,無法達到我們要的效果。
//以下是對方案一的簡單實現,大家可以結合上圖感受到方案一的缺陷
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<assert.h>
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;
}
int main()
{
Test();
system("pause");
return 0;
}
方案二
class String
{
private:
char* _str;
static int count;
};
設置一個靜態整形變量來計算指向一塊內存的指針的數量,每析構一次減1,直到它等於0(也就是沒有指針在指向它的時候)再去釋放那塊內存,看似可行,其實不然!
這個方案只適用於只調用一次構造函數、只有一塊內存的情形,如果多次調用構造函數構造對象,新構造的對象照樣會改變count的值,那麼以前的內存無法釋放會造成內存洩漏。
結合上圖和下面的代碼,我們可以清楚地看到該方案相比方案一的改善,以及缺陷
#define_CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<assert.h>
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;
};
ostream& operator<<( ostream& output, const String & s)
{
output << s._str;
return output;
}
istream& operator>>( istream& input, const String & s)
{
input >> s._str;
return input;
}
int String::count = 0; //初始化count
void Test()
{
String s1("aaa");
String s2(s1);
String s3 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
方案三
class String
{
private:
char* _str;
int* _refCount;
};
方案三設置了一個int型的指針變量用來引用計數,每份內存空間對應一個引用計數,而不是每個對象對應一個引用計數,而且每塊內存的引用計數互不影響,不會出現方案一和方案二出現的問題。
1.在實現賦值運算符重載要謹慎,不要遇到下圖的情形
2.改變字符串的某個字符時要謹慎,不要遇到類似下圖所遇到的問題。
如果多個對象都指向同一塊內存,那麼只要一個對象改變了這塊內存的內容,那所有的對象都被改變了!!
可以用下圖的形式改善這種問題:新設置一塊內存來存要改變的對象
案例3我畫的圖較多,方便大家結合代碼去理解
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<assert.h>
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;
}
//如果修改了字符串的內容,那所有指向這塊內存的對象指針的內容間接被改變
//如果還有其它指針指向這塊內存,我們可以從堆上重新開辟一塊內存空間,
//把原字符串拷貝過來
//再去改變它的內容,就不會產生鏈式反應
// 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;
//String s4("opqrst");
//String s5(s4);
//String s6 (s5);
//s6 = s4;
//cout << s4 << endl;
//cout << s5 << endl;
//cout << s6 << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
方案四
class String
{
private:
char* _str;
};
方案四與方案三類似。方案四把用來計數的整形變量放在所開辟的內存空間的首部,
用*((int*)_str)就能取得計數值
#define_CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<assert.h>
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;
//String s4("opqrst");
//String s5(s4);
//String s6 (s5);
//s6 = s4;
//cout << s4 << endl;
//cout << s5 << endl;
//cout << s6 << endl;
}
int main()
{
Test();
system("pause" );
return 0;
}