String類詳解(淺拷貝,深拷貝,引用計數,寫時拷貝)
String類:標準庫型別string類表示可變長的字元序列,定義在std中,專門用來管理字串,下面一起看下它的重要考點。
一,淺拷貝
所謂淺拷貝,是指原物件與拷貝物件公用一份實體,僅僅是物件名字不同而已(類似引用,即對原物件起別名),其中任何一個物件改變都會導致其他的物件也跟著它變。如下面這段程式碼:
//淺拷貝 class String { public: String(const char* pStr = "")//建構函式 :_pStr(new char[strlen(pStr)+1]) { if(0 == *pStr)//字串為空 { *_pStr = '\0'; } else//字串不為空 { strcpy(_pStr,pStr); } } String(const String& s)//拷貝建構函式 { _pStr = s._pStr; } String& operator=(String& s)//賦值運算子過載 { if(_pStr != s._pStr)//判斷是不是自己給自己賦值 { _pStr = s._pStr; } return *this; } ~String()//解構函式 { if(NULL == _pStr) { return; } else { delete []_pStr; _pStr = NULL; } } private: char* _pStr; }; void Funtest() { String s1("abcd"); String s2(s1); String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3) String s4;//s4物件已經存在了 s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4 } int main() { Funtest(); system("pause"); return 0; }
一執行你就會發現,從程式開始執行到s4建立並賦值,程式都沒有問題。
再往下走,進入解構函式
根據棧空間先入後出的原則,應該先析構s4物件,可是有上圖可以看到,當釋放s4後,前面建立的3個物件也都成了隨機值,那麼再往下走會發生什麼呢?
沒錯,程式在執行到370行是崩潰了,這是為什麼呢?請看下圖。
從上我們可以看出淺拷貝存在一定的問題,那麼怎樣對它進行改進防止一個物件被多次釋放呢,你可能會這樣想。
//淺拷貝(引用計數(_count作為普通成員變數)error) class String { public: String(const char* pStr = "")//建構函式 :_pStr(new char[strlen(pStr)+1]) ,_count(0)//初值賦值為0 { if(0 == *pStr) { *_pStr = '\0'; } else { strcpy(_pStr,pStr); } _count++;//每建立一個物件計數器加1 } String(String& s)//拷貝構造 :_count(s._count)//將已存在的物件s的計數器賦給當前物件 { _pStr = s._pStr; s._count++;//將原物件的計數器加1 _count = s._count;//將原物件的計數器加1後賦值給當前物件 } ~String()//解構函式 { if(NULL == _pStr) { return; } else { if(--_count == 0)//如果計數器為0,說明無物件指向該空間,可以釋放 { delete []_pStr; _pStr = NULL; } } } String& operator=(String& s)//賦值運算子過載 { if(_pStr != s._pStr) { _pStr = s._pStr; s._count++;//將原物件的計數器加1 _count = s._count;//將已存在的物件s的計數器賦給當前物件 } return *this; } private: int _count;//給一個計數器控制解構函式 char* _pStr; }; void Funtest() { String s1("abcd"); String s2(s1); String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3) String s4;//s4物件已經存在了 s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4 } int main() { Funtest(); system("pause"); return 0; }
用一個計數器來控制解構函式聽起來好像可以解決上述問題,可實際呢,結果卻是這樣的。
4個物件建立後:
呼叫4次解構函式之後:
之後函式就返回了。
歸結一下上述問題的錯誤:
我們知道,這四個物件本來指向同一塊空間,計數器本來都應為4,可是現在的結果卻是計數器只能控制與它相鄰物件的計數器,物件建立完成後,計數器並不統一。
其次,呼叫4次解構函式之後,本來應該四個物件同時被釋放,可是結果卻是沒有一個物件的計數器為0,也就是這塊空間並沒有被釋放,記憶體又洩露了唄。
為了保持計數器的統一,我們決定把計數器設定為類的靜態成員函式,
//淺拷貝(引用計數(_count作為靜態成員變數)) class String { public: String(const char* pStr = "")//建構函式 :_pStr(new char[strlen(pStr)+1]) { if(0 == *pStr) { *_pStr = '\0'; } else { strcpy(_pStr,pStr); } _count++; } String(const String& s)//拷貝構造 { _pStr = (char*)s._pStr; s._count = _count; _count++; } ~String()//解構函式 { if(NULL == _pStr) { return; } else { if(--_count == 0) { delete []_pStr; _pStr = NULL; } } } String& operator=(String& s)//賦值運算子過載 { if(_pStr != s._pStr) { _pStr = s._pStr; s._count = _count; _count++; } return *this; } private: static int _count; char* _pStr; }; int String::_count = 0; void Funtest() { String s1("abcd"); String s2(s1); String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3) String s4;//s4物件已經存在了 s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4 } int main() { Funtest(); system("pause"); return 0; }
當函式執行到建立好s4還沒有對其賦值時,
再往下執行:
由上圖可知,計數器雖然統一了,然而我們的程式碼依舊存在bug。
錯誤分析:
1.我們一共只建立了4個物件,可是計數器卻為5,那是因為靜態成員變數為所有物件共享,任何物件都可以對它進行修改,每建立一個物件我們都對計數器加1,卻忽略了建立的新物件是否與已存在的物件佔同一塊空間
2.呼叫4次解構函式後,計數器值為1,導致空間又沒有被釋放。
吸取上面的錯誤教訓,我們又想到了指標。
//淺拷貝(引用計數(指標實現計數))
class String
{
public:
String(const char* pStr = "")//建構函式
:count(new int(0))
,_pStr(new char[strlen(pStr)+1])
{
if(NULL == pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
*count = 1;
}
String(const String& s)//拷貝構造
:count(s.count)
{
_pStr = (char*)s._pStr;
count = s.count;
(*count)++;
}
~String()//解構函式
{
if(NULL == _pStr)
{
return;
}
else
{
if(--(*count) == 0)
{
delete[]count;//勿忘了釋放計數器指標
delete[]_pStr;
_pStr = NULL;
count = NULL;
}
}
}
String& operator=(String& s)//賦值運算子過載
{
if(_pStr != s._pStr)
{
_pStr = s._pStr;
count = s.count;
(*count)++;
}
return *this;
}
private:
int* count;
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3)
String s4;//s4物件已經存在了
s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
執行過程:
建立4個物件後:
給s4賦值
後:
呼叫4次解構函式之後:
空間被準確釋放,程式碼寫到這裡,我們會滿意嗎?當然不,永遠記住:只有更好沒有最好。指標計數的缺陷:每個物件都得為它多建立一個指標,浪費空間。還有釋放麻煩,很有可能我們只記得釋放_pStr,卻忘了釋放計數器指標造成記憶體洩漏,得不償失。
接下來,對淺拷貝再次進行優化。
二,寫時拷貝(Copy-On-Write):
即寫時才拷貝,以上諸多問題讓我們得知淺拷貝容易出現指標懸掛的問題,但是我們可以應用引用計數但是方式來解決淺拷貝中多次析構的問題,同時寫時拷貝就應運而生了。
//寫時拷貝(仿照new[]實現)
class String
{
public:
String(const char* pStr = "")//建構函式
:_pStr(new char[strlen(pStr) + 4 + 1])//每次多建立4個空間來存放當前地址有幾個物件
{
if(NULL == pStr)
{
(*(int*)_pStr) = 1;//將前4個位元組用來計數
_pStr += 4;//指標向後偏移4個位元組
*_pStr = '\0';
}
else
{
(*(int*)_pStr) = 1;//將前4個位元組用來計數
_pStr += 4;//指標向後偏移4個位元組
strcpy(_pStr,pStr);//將pStr的內容拷貝到當前物件的_pStr中
}
}
String(const String& s)//拷貝構造
:_pStr(s._pStr)
{
++(*(int*)(_pStr-4));//向前偏移4個位元組將計數加1
}
~String()//解構函式
{
if(NULL == _pStr)
{
return;
}
else
{
if(--(*(int*)(_pStr - 4)) == 0)//向前偏移4個位元組判斷計數是否為0,是0則釋放
{
delete (_pStr-4);
_pStr = NULL;
}
}
}
String& operator=(const String& s)//賦值運算子過載
{
if(_pStr != s._pStr)
{
if(--(*(int*)(_pStr - 4)) == 0)//釋放舊空間
{
delete (_pStr-4);
_pStr = NULL;
}
_pStr = s._pStr;//指向新空間
++(*(int*)(_pStr - 4));//計數加1
}
return *this;
}
char& operator[](size_t index)//下標訪問操作符過載
{
assert(index>=0 && index<strlen(_pStr));
if(*((int*)(_pStr-4)) > 1)//說明有多個物件指向同一塊空間
{
char* temp = new char[strlen(_pStr) + 4 + 1];//新開闢一塊空間
temp += 4;//先將開闢的空間向後偏移4個位元組
strcpy(temp,_pStr);//將_pStr的內容拷貝到temp中
--(*(int*)(_pStr-4));//將原來空間的計數器減1
_pStr = temp;//將當前物件指向臨時空間
*((int*)(_pStr-4)) = 1;//將新空間的計數器變為1
}
return _pStr[index];
}
private:
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3)
s3[2] = 'g';
String s4;//s4物件已經存在了
s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
執行過程:
建立好s1,s2,s3後:
改變s3的內容:
s3的值被改變後:
對s4進行賦值後:
如果還不理解,請看下圖:
三,深拷貝
所謂深拷貝,就是為新物件開闢一塊新的空間,並將原物件的內容拷貝給新開的空間,釋放時就不會牽扯到多次析構的問題。
//深拷貝(普通版)
class String
{
public:
String(const char* pStr = "")//建構函式
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
}
String(const String& s)//拷貝構造
:_pStr(new char[strlen(s._pStr)+1])
{
strcpy(_pStr,s._pStr);
}
String& operator=(const String& s)//賦值運算子過載
{
if(_pStr != s._pStr)//判斷是否自己給自己賦值
{
char* temp = new char[strlen(s._pStr)+1];//先開闢一段新空間
strcpy(temp,s._pStr);//將原物件的值賦給新空間
delete []_pStr;//釋放當前物件
_pStr = temp;//將當前物件指向新開闢的空間
}
return *this;
}
~String()//解構函式
{
if(NULL == _pStr)
{
return;
}
else
{
delete[]_pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3)
String s4;//s4物件已經存在了
s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
執行過程:
仔細觀看上圖,你會發現雖然4個物件指向的內容一樣,不過地址卻均不相同,說明它們各自佔各自的空間,出來值相同外,沒什麼聯絡,析構起來當然也不會有問題了。
下面看下簡潔版的深拷貝:
//深拷貝(簡化版1)
class String
{
public:
String(const char* pStr = "")//建構函式
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
}
String(const String& s)//拷貝構造
:_pStr(new char[strlen(s._pStr)+1])
{
strcpy(_pStr,s._pStr);
}
~String()//解構函式
{
if(NULL == _pStr)
{
return;
}
else
{
delete[]_pStr;
_pStr = NULL;
}
}
String& operator=(const String& s)//賦值運算子過載
{
if(_pStr != s._pStr)
{
delete[]_pStr;
_pStr = new char[strlen(s._pStr)+1];
strcpy(_pStr,s._pStr);
}
return *this;
}
private:
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3)
String s4;//s4物件已經存在了
s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
//深拷貝(簡潔版2)
class String
{
public:
String(const char* pStr = "")//建構函式
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
}
String(String& s)//拷貝構造
:_pStr(NULL)//防止交換後temp指向隨機空間,本函式呼叫結束導致記憶體洩漏以致崩潰
{
String temp(s._pStr);//如果不給出臨時變數,交換後s的值將為NULL
std::swap(_pStr,temp._pStr);
}
//1
String& operator=(const String &s)//賦值運算子過載
{
if(_pStr != s._pStr)
{
String temp(s._pStr);//如果不給出臨時變數,交換後s的值將為NULL
std::swap(_pStr,temp._pStr);
}
return *this;
}
/* 2
String& operator=(const String& s)
{
if (this != &s)
{
String temp(s);
std::swap(_pStr, temp._pStr);
}
return *this;
}*/
/* 3
String& operator=(String temp)
{
std::swap(_pStr, temp._pStr);
return *this;
}*/
~String()//解構函式
{
if(NULL == _pStr)
{
return;
}
else
{
delete[]_pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};
void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//呼叫拷貝建構函式(編譯器會s2直接初始化s3)
String s4;//s4物件已經存在了
s4 = s3;//編譯器會呼叫賦值運算子過載將s3的值賦給s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
Fighting!!!
相關推薦
String類詳解(淺拷貝,深拷貝,引用計數,寫時拷貝)
String類:標準庫型別string類表示可變長的字元序列,定義在std中,專門用來管理字串,下面一起看下它的重要考點。 一,淺拷貝 所謂淺拷貝,是指原物件與拷貝物件公用一份實體,僅
C++中string類詳解(轉載)(最下面有程式碼實現)
作者:yzl_rex 來源:CSDN 原文:https://blog.csdn.net/yzl_rex/article/details/7839379 要想使用標準C++中string類,必須要包含 #include < string>// 注意是< string>
Java String類詳解(一)
String類是一個字串型別的類,使用“XXXX”定義的內容都是字串,雖然這個類在使用上有一些特殊,但是String本身是一個類。 一、String的例項化兩種方式 1、直接賦值例項化: String StringName= "xxx"; 以上是Stri
String類引用計數的寫時拷貝
寫時拷貝: 當一個物件被拷貝構造多次,在不改變內容的情況下,多個物件共用同一個空間。 如果某個物件要改變內容,就要另外開闢一塊相同的空間,然後改變這個物件的_str指標,再進行寫操作 #include<iostream> #include<assert.
java常用集合類詳解(有例子,集合類糊塗的來看!)
TreeSet:TreeSet是依靠TreeMap來實現的.TreeSet是一個有序集合,TreeSet中元素將按照升序排列,預設是按照自然排序進行排列,意味著TreeSet中元素要實現Comparable介面.我們可以在構造TreeSet物件時,傳遞實現了Comparator介面的比較器物件.java.ut
常用類Object,String類詳解
ash trace native object類 obj 關鍵字 圖片 sys img 深拷貝 淺拷貝 垃圾回收 package com.demo.obj; /** * 學習Object類 * native 本地棧方法, 方
String 類詳解
方法 類型 nal 提高 () masm static 基本類型 代碼 StringBuilder與StringBuffer的功能基本相同,不同之處在於StringBuilder是非線程安全的,而StringBuffer是線程安全的,因此效率上StringBuilder類更
Java Scanner 類詳解(附例子)學習
在筆試程式設計過程中,關於資料的讀取如果迷迷糊糊,那後來的程式設計即使想法很對,實現很好,也是徒勞,於是在這裡認真總結了Java Scanner 類的使用 通過 Scanner 類來獲取使用者的輸入,下面是建立 Scanner 物件的基本語法: Scanner s =
System.AppDomain類詳解(一)
AppDomain是CLR(Common Language Runtime:公共語言執行庫),它可以載入Assembly、建立物件以及執行程式。 AppDomain是CLR實現程式碼隔離的基本機制。 每一個AppDomain可以單獨執行、停止;每個AppDomain都有自己預設的異常
OpenCV參考手冊之Mat類詳解(二)
譯文參考The OpenCV Reference Manual (Release 2.3)August 17 2011 Mat::~Mat Mat的解構函式。 C++: Mat::~Mat() 解構函式呼叫Mat::release()。 Mat::operato
媽媽再也不用擔心我的面試之String類詳解
其實在java中String並不能算是一個基本型別,迴歸到String的本質其實在jdk1.8以前他是一個final修飾的char陣列,1.9以後他是一個final修飾的byte陣列;由開發者將其封裝成String類;其實我們也可以從程式碼中來證明String是一個類的事實:
【Java入門提高篇】Day34 Java容器類詳解(十五)WeakHashMap詳解
public class WeakHashMapTest { public static void main(String[] args){ testWeakHashMap(); } private static void testWeakHashMap
java中String類詳解
String類 String類存在java.lang包中,專門儲存字串。是引用資料型別。 String類的兩種例項化方法 1.直接賦值 String str1= "hello"; 2.傳統賦值 Str
Java 原子操作類詳解(AtomicInteger、AtomicIntegerArray等)
當程式更新一個變數時,如果多執行緒同時更新這個變數,可能得到期望之外的值,比如變數i=1,A執行緒更新i+1,B執行緒也更新i+1,經過兩個執行緒操作之後可能i不等於3,而是等於2。因為A和B執行緒在更新變數i的時候拿到的i都是1,這就是執行緒不安全的更新操作,通常我們會使
Java 的 IO 四大基類詳解(上)
1、概述 Java 的IO通過java.io 包下的類和介面來支援,java.io包下主要包括輸入、輸出兩種流。每種輸入輸出流又可分為位元組流和字元流兩大類。 位元組流以位元組為單位處理輸入、輸出操作; 字元流以字元來處理輸入
javascript中this詳解(史上最簡單易理解的講解,包你不再找錯this指向)
判斷方法 this永遠指向一個物件,但普通函式與箭頭函式this指向不同。 普通函式: 普通函式的this是動態的,由函式是如何被呼叫的來決定。 ①是否使用了new以建構函式方式來呼叫函式,如果是則指向新建立的物件 ②是否使用(物件.屬性)的方式呼叫函式(如Obj.
JDK 原子操作類詳解(AtomicInteger、AtomicIntegerArray等)
當程式更新一個變數時,如果多執行緒同時更新這個變數,可能得到期望之外的值,比如變數i=1,A執行緒更新i+1,B執行緒也更新i+1,經過兩個執行緒操作之後可能i不等於3,而是等於2。因為A和B執行緒在更新變數i的時候拿到的i都是1,這就是執行緒不安全的更新操作,通常我們會使用
String類詳解
1.String class初步認知:不可變性 JDK中 String類中定義的方法,不會去改變字串引用中的資料值,最終會建立、返回新的字串物件引用。 2.編譯器如何實現兩個字串的連線操作 不同於想象,compiler會利用可變字串類StringBuilder的append方法,實現字串的拼
OpenCV學習筆記(04):Mat類詳解(一)
1. 前言:Mat類簡介 OpenCV 作為強大的計算機視覺開源庫,很大程度上參考了MatLab的實現細節和語法風格,比如說,在OpenCV2.x版本以後,越來越多的函式實現了MatLab所具有的功能,甚至乾脆連函式名都一模一樣(如 imre
java.util.Arrays類詳解(原始碼總結)
概述 Arrays類位於java.util包下,是一個對陣列操作的工具類。今天詳細的看了看Arrays類的4千多行原始碼,現將Arrays類中的方法做一個總結(JDK版本:1.6.0_34)。Arrays類中的方法可以分為八類: sort(對陣列排序) b