《C++Primer》12、13章
第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章