標籤:標準庫 info 更新 變數 實驗 訪問 sig lease 額外
第12章 動態記憶體12.1 智能指標
shared_ptr<T>
make_shared<T>(args)
直接初始化 new int(10);
預設初始化 new int;
值初始化 new int();
由內建指標(而不是智能指標)管理的動態記憶體在被顯示釋放前一直都會存在。
最好堅持只使用智能指標;
delete之後重設指標值為nullptr;
unique_ptr
u = nullptr 釋放u指向的對象,將u置為空白
u.release() u放棄對指標的控制權,返回指標,將u置為空白
u.reset() 釋放u指向的對象
u.reset(q) 釋放u指向的對象,如果q為內建指標,將u指向這個對象
u.reset(nullptr) 釋放u指向的對象,將u置為空白
例子:
unique_ptr<string>p1(new string(“sta”));
unique_ptr<string>p3(new string(“test”));
release返回的指標通常被用來初始化另一個智能指標或給另一個智能指標賦值。
p2.reset(p3.release());
或者unique_ptr<string> p2(p1.release());
12.2 動態數組
int *pia = new int[10](); //10個值初始化為0的int
delete [] pia;
智能指標和動態數組
unique_ptr<int []> up(new int[10]); //up指向一個包含10個未初始化int的數組
up.release(); //自動用delete[]銷毀其指標
allocator 類
allocator<T> a
a.allocate(n)
a.deallocate(p,n)
a.construct(p,args)
a.destroy(p)
第13章 拷貝控制13.1 拷貝、賦值與銷毀
在定義任何C++的類時,拷貝控制操作都是必要部分。
拷貝構造和移動構造定義了當用同類型的另一個對象初始化本對象時做什麼。
拷貝賦值和移動賦值定義了將一個對象賦予另一個對象時做什麼。
string sBook = “C++primer”; // 拷貝初始化 string b; //預設初始化 b = sBook; //拷貝賦值 |
13.1.1 拷貝建構函式
拷貝建構函式:如果一個建構函式的第一個參數是自身類類型的引用,且任何額外的參數都有預設值,則此建構函式是拷貝建構函式。
合成拷貝建構函式:如果沒有定義拷貝建構函式,編譯器會為我們定義一個。
一般情況,合成的拷貝建構函式會將起參數的成員逐個拷貝到正在建立的對象中。
每個成員的類型決定它如何拷貝。數值成員逐個拷貝。
拷貝初始化 VS 直接初始化
參考:https://www.cnblogs.com/cposture/p/4925736.html
直接初始化:要求編譯器使用普通的函數匹配來選擇最匹配的建構函式。
拷貝初始化:要求編譯器將右側運算對象拷貝到正在建立的對象中,如果需要還要經行類型轉換。
拷貝初始化何時發生?
1. 用=定義變數時 *********實驗,臨時變數
2. 將一個對象作為實參傳遞給一個非參考型別的形參
3. 從一個傳回型別為非參考型別的函數返回的對象 ******實驗,編譯器最佳化
4. 用花括弧列表初始化一個數組中的元素或一個彙總類中的成員
5. 標準容器庫調用insert或push成員 *********實驗,emplace比push高效
ClassTest ct2 = "ab"; //複製初始化 實際。。。編譯器的思想是能不用臨時變數就不用臨時變數 |
ClassTest ct6 = getOne();//複製初始化 實際。。。直接將ct6的地址帶入函數經行了初始化 |
#include <map> #include <iostream> #include <unordered_map> #include <vector> #include <cstring> using std::cout; using std::endl; struct T_TEST { unsigned int mme = 13; int test = 5; T_TEST() = default; T_TEST(unsigned int _mme, int t) { mme = _mme; test = t; }; }; typedef struct viterbiNode { int attr = 2; }T_VITERBINODE; unsigned char IP[] = "111.111.111.111"; struct T_CONSTRUCT { int a = 1; int b = 2; T_CONSTRUCT() { std::cout<<"default construct"<<std::endl; }; T_CONSTRUCT(int _a, int _b) { a = _a; b = _b; std::cout<<"two param construct"<<std::endl; }; T_CONSTRUCT(const T_CONSTRUCT& other) { a = other.a; b = other.b; std::cout<<"copy construct"<<std::endl; }; }; class ClassTest { public: ClassTest() { c[0] = ‘\0‘; cout<<"ClassTest()"<<endl; } ClassTest& operator=(const ClassTest &ct) { strcpy(c, ct.c); cout<<"ClassTest& operator=(const ClassTest &ct)"<<endl; return *this; } ClassTest(const char *pc) { strcpy(c, pc); cout<<"ClassTest (const char *pc)"<<endl; } // private: ClassTest(const ClassTest& ct) { strcpy(c, ct.c); cout<<"ClassTest(const ClassTest& ct)"<<endl; } private: char c[256]; }; ClassTest getOne() { cout<<"getOne begine"<<endl; ClassTest a("a"); cout<<"getOne end"<<endl; return a; }; int main() { std::vector<T_CONSTRUCT> v1; std::vector<T_CONSTRUCT> v2; std::cout<<"push:"<<std::endl; v1.push_back(T_CONSTRUCT(3,4)); std::cout<<"emplace:"<<std::endl; v2.emplace_back(5,6); cout<<"ct1: "; ClassTest ct1("ab");//直接初始化 cout<<"ct2: "; ClassTest ct2 = "ab";//複製初始化 cout<<"ct3: "; ClassTest ct3 = ct1;//複製初始化 cout<<"ct4: "; ClassTest ct4(ct1);//直接初始化 cout<<"ct5: "; ClassTest ct5 = ClassTest();//複製初始化 cout<<"ct6: "; ClassTest ct6 = getOne();//複製初始化 } |
延伸:彙總類
滿足條件: 所有成員都是public 沒有定義任何建構函式 沒有類內初始值,如果有初始值,不能用{}進行賦值了,否則編譯不過 沒有基類,也沒有virtual函數 不足: 所有成員public 將正確初始化的工作交給使用者 增加或者刪除一個成員後,所有的初始化語句都需要更新 初始化: 初始化列表,如果個數少於成員數量,靠後的成員被值初始化 預設初始化: 如果內建類型的變數未被顯示初始化,它的值由定義的位置決定。 定義在任何函數體之外的變數被初始化為0; 定義在函數內部的類型變數將不被初始化。 為什麼有這種區別? 1. 變數儲存的位置; 2. C++為了相容C。 |
POD類: 彙總類的一種 參考:https://www.cnblogs.com/DswCnblog/p/6371071.html POD的定義:極簡的、屬於標準布局的 KW檢查:非POD類,不能用memset等記憶體式的拷貝 memset(pthandleInfo, 0, sizeof(T_HANDLEINFO)); 為什麼需要POD類型? 可以使用位元組賦值,比如memset,memcpy操作 對C記憶體布局相容。 保證了靜態初始化的安全有效。 是否是POD類型? c++11 用std::is_pod<T>::value 判斷 |
13.1.2 拷貝賦值運算子
Foo& operator=(const Foo&);
理解:拷貝初始化、賦值運算子的區別
string s = string(“2017”); // s拷貝初始化
string j,k;
j = k; // 使用string的拷貝賦值運算子
實驗:修改拷貝賦值運算子,共幾次拷貝賦值或構造?
cout<<"***********A=getOther(c)"<<endl; d = getOther(c); cout<<"***********getOther(c)"<<endl; getOther(c); cout<<"*********T_CONSTRUCT e = getOther(c)"<<endl; const T_CONSTRUCT e = getOther(c); |
13.1.3 解構函式
智能指標是類類型,具有解構函式。智能指標成員在析構階段會自動銷毀。隱式銷毀一個內建的指標類型成員不會delete它所指向的對象。
什麼時候調用解構函式?
1. 變數在離開範圍是
2. 當一個對象被銷毀時
3. 容器被銷毀時,其元素被銷毀
4. 動態對象應用delete時
5. 臨時對象,當建立它的完整運算式結束時
解構函式體自身並不直接銷毀成員。成員是在解構函式體之後隱含的析構階段中被銷毀的。
實驗:
cout<<"***********getOther(c)"<<endl; getOther(c); copy construct before return copy construct delete delete |
13.1.4 三五法則
1 需要解構函式的類也需要拷貝和賦值操作
2 需要拷貝操作的類也需要賦值操作,反之亦然
13.1.5 使用=default
顯示的要求編譯器產生合成的版本。
13.1.6 阻止拷貝
通過=delete
本質上,當不能拷貝、賦值或銷毀類的成員時,類的合成拷貝控製成員就是被定義為刪除的。
合成的拷貝控製成員可能是刪除的:
如果一個類有資料成員不能預設構造、拷貝、複製或銷毀,則對應的成員函數將被定義為刪除的。
通過private拷貝控制(新標準之前,現在不建議用)
拷貝控製成員聲明為pirvate,但不定義它們。
13.2 拷貝控制和資源管理
兩種策略:
1. 類的行為像值,通過建構函式和賦值函數控制;
類值拷貝賦值運算子:賦值運算子通常組合了解構函式和建構函式的操作。
類似解構函式,賦值操作會銷毀左側對象的資源;
類似拷貝建構函式,賦值操作會從右側運算對象拷貝資料;
2. 類的行為像指標,通過shared_ptr的方案解決或者引用計算的控制;
引用計數的工作方式:
建構函式還需要建立一個引用計數,用來記錄有多少對象與正在建立的對象共用狀態,當我們建立一個對象時,只有一個對象共用狀態,計數器初始化1
拷貝建構函式不分配新的計數器,而是拷貝給定對象的資料成員,包括計數器。拷貝建構函式遞增計數器,指出給定對象的狀態又被一個新使用者。
解構函式遞減計數器,如果計數器變為0,則解構函式釋放狀態。
拷貝賦值運算子先遞增右側運算對象的計數器,然後遞減左側運算對象的計數器。如果左側運算對象的計數器變為0,則銷毀狀態。
賦值運算子:
當將一個對象賦值給它自身時,賦值運算必須正確;
一個好的方法是在銷毀左側運算對象資源之前拷貝右側運算對象。
13.3 交換操作swap
對於分配了資源的類,定義swap可能是一種很重要的最佳化手段。(交換的可能只是某個成員的指標)
class HasPtr{
friend void swap(HasPtr&, HasPtr&);
};
inline
void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
}
13.4 拷貝控制樣本13.5 動態記憶體管理類
很好的程式碼範例
實驗:vecoter容量擴容及拷貝P317vector對象是如何增長的?
std::vector<T_CONSTRUCT> v1; std::vector<T_CONSTRUCT> v2; std::cout<<"push 1:"<<std::endl; v1.push_back(T_CONSTRUCT(3,4)); std::cout<<"push 2:"<<std::endl; v1.push_back(T_CONSTRUCT(2,3)); std::cout<<"emplace:"<<std::endl; v2.emplace_back(5,6); push 1: two param construct copy construct delete push 2: two param construct copy construct copy construct delete delete emplace: two param construct |
如何提高效能?
1、 直接push_back()改成用emplace_back(); 2、 reserve(n) 預定n個空間,當然後續push_back()會增加,其中的值不確定; 3、 resize(n, x) 申請n個空間初始化為x。 reserve只是保持一個最小的空間大小,而resize則是對緩衝區進行重新分配, 裡面涉及到的判斷和記憶體處理比較多所以比reserve慢一些。 對於資料數目可以確定的時候,先預設空間大小是很有必要的。直接push_back資料頻繁移動很是耗時 |
C++11在時空效能方面的改進:https://www.cnblogs.com/me115/p/4788322.html#h26
大小固定容器 array 前向列表 forward_list 雜湊表[無序容器] unordered containers 常量運算式 constexpr 靜態斷言 static_assert move語義和右值引用 原地安置操作 Emplace operations |
13.6 對象移動
移動而非拷貝對象會大幅度提升效能。
標準庫容器、string和shared_ptr類既支援移動也支援拷貝。IO類和unique_ptr類可以移動但不能拷貝。
13.6.1 右值引用
右值引用一個重要的性質:只能綁定到一個將要銷毀的對象。
所以:所引用的對象將要被銷毀;
該對象沒有其他使用者。
有哪些是產生右值?
返回非參考型別的函數
算數、關係、位、後置遞增遞減運算子
std::move 標準庫move函數:顯示地將一個左值轉換為對應的右值參考型別。
注意: 除了對來源物件賦值或銷毀之外,我們將不再使用它。
調用move之後,我們不能對移後來源物件的值做任何假設。
13.6.2 移動建構函式和移動賦值運算子
為了讓我們自己的類型支援移動操作,需要為其定義移動建構函式和移動賦值運算子。
移動建構函式
StrVector::StrVec(StrVec &&s) noexcept // 必須是noexcept
:elements(s.elements)
{
s.elements = nullptr;
}
移動後,來源物件要保證安全。銷毀是無害的。
移動賦值函數
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if (this != &rhs)
{
free(); //釋放已有元素
elements = rhs.elements;
rhs.elements = nullptr; //來源物件可析構
}
return *this;
}
在移動操作之後,移後來源物件必須保持有效、可析構的狀態,但使用者不能對其值進行任何假設。
為什麼是noexcept?
vector異常處理保護。
合成的移動操作:只有當一個類沒有定義任何自己版本的拷貝控製成員,且它的所有資料成員都能夠移動構造或移動賦值時。
移動右值,拷貝左值
StrVec v1, v2;
v1 = v2; // v2是左值,使用拷貝賦值
StrVec getVec(istream &) //getVec 返回一個右值
v2 = getVec(cin); // getVec(cin)是右值,使用移動賦值
左值、右值:
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值(將亡值或純右值)。舉個例子,int a = b+c, a 就是左值,其有變數名為a,通過&a可以擷取該變數的地址;運算式b+c、函數int func()的傳回值是右值,在其被賦值給某一變數前,我們不能通過變數名找到它。
如果沒有移動建構函式,右值也被拷貝。(沒有定義移動建構函式,使用move操作實際是用拷貝建構函式實現)
定義了移動建構函式或移動賦值運算子的類必須也定義自己的拷貝操作,否則這些成員預設地被定義為刪除的。
建議:不要隨意使用移動操作 只有在確信演算法在為一個元素賦值或者將其傳遞給一個使用者定義的函數後不再訪問它。 小心地使用move,可以大幅度提升效能。隨意使用很可能造成錯誤。 |
13.6.3 右值引用和成員函數
區分移動和拷貝的重載函數通常一個版本接受一個const X&,另一個版本接受一個T&&
《C++Primer》12、13章