目錄
1. 淺拷貝
2. 深拷貝?
3. string類傳統寫法
4. string類現代版寫法?
5. 自定義類實現swap成員函數
6. 標準庫swap函數的調用?
7. 引用計數和寫時拷貝
1. 淺拷貝
若string類沒有顯示定義拷貝構造函數與賦值運算符重載,編譯器會自動生成默認的,編譯器生成的默認版本只會簡單的復制指針地址,當用s1構造s2時,s1的_str指針存了"hello"的地址,拷貝給s2后,兩者都指向同一塊內存,這種拷貝方式叫做淺拷貝。當s1和s2先后析構時,這塊內存會被delete兩次,一旦其中一個對象釋放了這塊內存,_str所指的空間被釋放掉,另一個對象的_str指針就會變成野指針,再次釋放就會導致程序崩潰。
2. 深拷貝?
要避免淺拷貝問題,就需要自己實現深拷貝,讓s2有獨立的內存復制s1的內容,每個對象都有自己獨立的內存空間,這樣析構時各刪各的就不會出問題。
//拷貝構造函數(實現深拷貝)
string(const string& s)
{_str = new char[s._capacity + 1]; //新開辟內存strcpy(_str, s._str); //復制內容_size = s._size;_capacity = s._capacity;
}
如果一個類中涉及到資源的管理,其拷貝構造函數、賦值運算符重載以及析構函數必須要顯示給出來。
3. string類傳統寫法
class string
{
public://默認構造函數string(const char* str=""){if (str == nullptr) //strlen(nullptr)會觸發未定義行為可能導致程序崩潰{str = ""; //將nullptr轉為空字符串}_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);}//拷貝構造函數(深拷貝)string(const string& s):_str(new char[s._capacity + 1]), _size(s._size), _capacity(s._capacity){ strcpy(_str, s._str); //拷貝字符串內容 }//賦值運算符重載(深拷貝)string& operator=(const string& s){if (this != &s)//防止自己給自己賦值{delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;return *this;}}//析構函數~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;
};
4. string類現代版寫法?
class string
{
public://默認構造函數string(const char* str=""){if (str == nullptr){str = ""; //將nullptr轉為空字符串}_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);}//swap成員函數void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷貝構造函數優化 s2 = s1string(const string& s) //沒有在成員初始化列表進行顯式初始化時,使用了默認成員初始化器,防止默認初始化成隨機值。{string tmp(s._str); //用s的字符串數據創建局部對象tmp 這里也可以調用拷貝構造string tmp(s);swap(tmp); //交換當前對象s2和tmp tmp出了作用域調用析構函數銷毀}賦值運算符重載優化 s2 = s1//string& operator=(const string& s)//{ // string tmp(s._str);// swap(tmp); // return *this;//}//賦值運算符重載再優化 s2 = s1string& operator=(string tmp) //傳值傳參觸發拷貝構造!使用s1構造局部對象tmp{swap(tmp); //交換當前對象s2和tmp return *this;}//析構函數~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;
};
5. 自定義類實現swap成員函數
當自定義string類實現了swap成員函數,執行?std::swap(s1,s2)?時會通過模版機制自動轉發到swap成員函數即s1.swap(s2),完成高效交換(交換內部指針,長度等,避免深拷貝),實現高效交換。如果自定義類中不實現成員swap,std::swap會走模版邏輯:
- C++98:T c(a); a=b; b=c; 走“1次拷貝構造+2次拷貝賦值”的邏輯,3次深拷貝開銷。
- C++11:T c(std::move(a)); a=std::move(b); b=std::move(c);移動構造+兩次移動賦值,依賴類的移動語義。
std::swap是一個模版函數,它的標準實現大致如下:
namespace std {// 默認模板:通過三次賦值實現交換(對于無swap成員函數的類類型交換:走深拷貝 對于內置類型的交換:無性能損耗)template <class T>void swap(T& a, T& b) {T temp = std::move(a);a = std::move(b);b = std::move(temp);}// 特化模板:若類型T存在swap成員函數,則調用該成員函數template <class T>void swap(T& a, T& b, std::enable_if_t<std::is_class_v<T> && {a.swap(b); //……檢測T是否有swap成員函數,如果有就調用它。} }
這個模版的特別之處在于:它會自動檢測類型T是否存在swap成員函數,若類型T未定義swap成員函數,std::swap會使用默認模版邏輯。
注意:std::swap模版的核心機制是嚴格匹配成員函數名swap,若成員函數名叫其它名字(如my_swap)無法轉發調用成員函數,只能走模版邏輯。
總結:所以,只要類有swap成員函數,調用std::swap(s1,s2)最終會轉調s1.swap(s2)。
//swap成員函數void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}
示例:
int main() {string s1("hello world");string s2("xxxxxxxxxxxxxxxxxxx");std::swap(s1, s2);//通過模版機制自動轉發調用成員函數s1.swap(s2)cout << s1 << endl;cout << s2 << endl;s1.swap(s2); //調用成員函數cout << s1 << endl;cout << s2 << endl;return 0; }
運行結果:
std::string類自身實現了swap成員函數,用于高效交換兩個字符串的內部數據,直接交換內部的指針、大小、容量,相當于只交換“容器的殼”,數據本體不動,效率接近O(1),幾乎是“零拷貝”操作,比默認的拷貝邏輯快得多。
6. 標準庫swap函數的調用?
當調用 std::swap(basic_string) ?這個全局函數時,它內部實際上會轉發調用 basic_string 類的成員函數swap,也就是 std::basic_string::swap ?。 所以,當swap(s1,s2)時,會匹配到特化版本,實際執行的是string對象成員的swap邏輯,等價于s1.swap(s2),例:
#include <string> #include <iostream> using namespace std;int main() {string s1 = "Hello";string s2 = "World";//調用全局 swap(匹配特化版本)swap(s1, s2); //內部轉發調用s1.swap(s2);cout << "s1: " << s1 << ", s2: " << s2 << endl; // 輸出 s1: World, s2: Helloreturn 0; }
簡單來說,怎么方便怎么寫,想寫什么寫什么,它們最終執行的是同一個邏輯。
成員函數風格:s1.swap(s2)? ?
全局函數風格:swap(s1,s2) 或 std::(s1,s2)
7. 引用計數和寫時拷貝
引用計數
用來記錄有多少個對象正在引用該資源。在構造時,將資源的計數給成1,每增加一個對象使用該資源,就給計數增加1,當某個對象被銷毀時,先給該計數減1,然后再檢查是否需要釋放資源, 如果計數為1,說明該對象時資源的最后一個使用者,將該資源釋放;否則就不能釋放,因為還有其他對象在使用該資源。
寫時拷貝?
當對象被復制時,采用淺拷貝的方式,不立即拷貝實際數據(“寫時”才拷貝),此時多個對象共享同一資源。普通淺拷貝若多個對象共享數據,修改時會影響所有對象,所以當某個對象需要修改數據時,會觸發數據的深拷貝,確保修改不會影響其他共享該數據的對象。
比如:先構造s1,再調用拷貝構造構造s2,我們走淺拷貝,將引用計數+1變成2,銷毀s2的時候,不需要釋放資源,只需要將引用計數-1變成1,對象的資源留給最后一個使用者釋放。但是如果要修改s2,就需要引用計數-1,再執行深拷貝,修改s2的數據,對s2的修改不影響s1。
引用計數和寫時拷貝通過“延遲拷貝”和“共享資源”減少開銷,適用于讀多寫少的場景。