定義一個類我們需要顯式或者隱式的指定這個類在拷貝,賦值,移動,銷燬時需要做什麼,
我們需要定義拷貝構造函數,移動構造函數,拷貝賦值運算符,移動賦值運算符和析構函數來控制這些操作。
拷貝和移動構造函數定義了一個對象初始化本對象時,需要做什麼。
拷貝和移動賦值運算符則決定了將 一個對象賦值給同類型的另一個對象時需要做什麼。
析構函數定義了銷燬對象時需要做什麼。
這些5個成員函數被稱爲拷貝控制操作。
13.1 拷貝、賦值和銷燬
13.1.1 拷貝構造函數
什麼是拷貝構造函數?
如果一個構造函數的第一個參數是該類型的引用類型,且額外參數都有默認值,那麼這個函數是拷貝構造函數。
默認的拷貝構造函數在執行時,將一個對象的所有非static數據成員拷貝給本對象。如果數據成員爲類類型,則執行該類型的拷貝構造函數,如果數據成員爲內置類型,則直接拷貝。
如果數據成員是數組類型,則將數組中的成員逐一拷貝到本對象中。在其他時候,數組是無法直接拷貝的。
什麼時候執行拷貝初始化
1.使用=定義變量時
2.使用一個對象初始化另一個對象時
3.一個函數的形參類型爲非引用類型,而傳入的實參是對象時
4.返回值類型是非引用類型,返回的是一個對象時
5.使用初始化列表,初始化數組或者聚合類的成員時。
注意拷貝初始化有時調用的是拷貝構造函數,但是有時調用的是移動構造函數
更重點的是,有時編譯器會跳過拷貝構造函數,直接執行普通的構造函數。
class A {
public:
A(int i=1) :a(i) {
cout<<"構造函數"<<endl;
};
A(const A& _a):a(_a.a) {
cout << "拷貝構造函數" << endl;
}
private:
int a;
};
A a1 = 1;
在書上A a1 = 1;這種定義變量的方式是拷貝初始化,但是在VS2017下,執行的是構造函數,且拷貝構造被設置爲private同樣可以執行。
但是下面這種情況執行的是構造函數
A a(1);
A a2 = a;
因此我們可以假定,默寫編譯器中使用=定義變量時,如果包含了隱式轉換,則直接執行構造函數,而不是拷貝構造。
因此我們也可以更好的理解,爲什麼當一個構造函數時explicit的時候。
A a1 = 1;
是錯誤的,因爲執行的是構造函數,而此時構造函數不允許隱式轉換。
這是我在VS2017上得到的結論,具體還是以書上爲主
即上面5種情況執行的都是拷貝初始化,當構造函數爲explicit時,使用=進行隱式轉換時無法調用拷貝初始化。
另外一點,拷貝構造函數的形參,一般爲const類型,因爲我們不會在拷貝一個對象時修改它。且最好不要將拷貝構造函數聲明爲explict,不然使用
A a(1);
A a2 = a;//報錯
這種形式定義變量會報錯。
對於標準庫的容器,insert和push執行的是拷貝初始化,而emplace()是直接初始化。
練習
13.1
當一個類的構造函數的第一個參數是該類型的引用,且其他的參數都有默認實參時,這個構造函數就是拷貝構造函數。
class A {
public:
A(int i) :a(i) {
cout<<"構造函數"<<endl;
};
//private:
A(const A& _a):a(_a.a) {
cout << "拷貝構造函數" << endl;
}
private:
int a;
};
0.使用=定義變量時,我在VS2017的情況下,使用=定義變量調用的是構造函數而不是拷貝構造,即使把拷貝構造變爲private,代碼同樣可以正常的運行
A a(1);
A a1 = 1;//VS2017下,顯示的執行的是構造函數
A a2 = a1;//拷貝構造函數
1.使用另一個對象初始化本對象時
A a3(a2);
2.使用一個對象實參傳遞給一個非引用類型的形參
func(a2);
3.返回值爲非引用類型的函數返回一個對象
A fun_1() {
A a(1);
return a;
}
fun_1();
4.用花括號列表來初始化數組中的元素或者聚合類中的成員。
A a(1);
A a2[] = {a,a,a,a,a};
13.2
根據上面的定義我們知道,我們傳入的實參是一個對象,而一個函數的形參類型是非引用類型時,這個形參將執行拷貝構造函數。
如果拷貝構造函數的形參又是非引用類型的,那麼這意味着我們要調用拷貝構造函數來初始化這個對象,這樣一來就變成了死循環。
13.3
StrBlob只有一個數據成員
std::shared_ptr<vector<string>> data;
所以拷貝一個StrBlob時,將一個對象的data初始化本對象的data,因爲shared_ptr<vector<string>>是類類型,所以執行的是它的拷貝構造函數。此時他們共同維護的對象引用計數+1
StrBlobPtr由三個數據成員
std::weak_ptr<vector<string>> wptr;
size_t curr;
拷貝StrBlobPtr對象時,將一個對象的wptr和cur的值用於初始化本對象的wptr和curr。size_t是內置類型,所以直接初始化,而wptr的類型是類類型,所以執行該類類型的拷貝構造函數。
13.4
1.形參arg的初始化將執行拷貝構造
2.local的初始化
3.head的初始化
4.pa數組的初始化
5.返回*heap類型
class A {
public:
A(int i=1) :a(i) {
cout<<"構造函數"<<endl;
};
A(const A& _a):a(_a.a) {
cout << "拷貝構造函數" << endl;
}
private:
int a;
};
A global(1);
A foo_bar(A arg) {
A local = arg, *heap = new A(global);
*heap = local;
A pa[4] = {local,*heap};
return *heap;
}
13.5
class HasPtr {
public:
HasPtr(const std::string& s = std::string()) :ps(new std::string(s)),i(0){};
HasPtr(const HasPtr& hasptr):ps(new string(*hasptr.ps)),i(hasptr.i) {
};
private:
std::string *ps;
int i;
};
13.1.2 拷貝賦值運算符
我們可以通過定義拷貝賦值運算符,自己定義一個類類型如何賦值。
拷貝賦值運算符實際是重載=運算符。
使用operator+運算符來重載運算符,operator+運算符構成函數名同定義函數一樣,我們需要定義返回值和形參列表,有些運算符必須在類中重載,比如賦值運算符。
如果一個運算符是成員函數,那麼在調用時左側運算對象將綁定到隱式的this上。
對於賦值運算符,右側運算對象將作爲參數顯式的傳入,而函數返回的是左側對象的引用。
對於拷貝賦值運算符,在使用一個對象爲同類型的對象賦值時,如果數據成員是類類型,則執行該數據成員的拷貝賦值運算符,如果是內置類型,也是執行內置類型的拷貝賦值運算符。對於數組則和在拷貝構造函數中一樣,逐個元素賦值。
練習
13.6
拷貝賦值運算符,實際就是在類中重載賦值運算符。
在爲對象賦值時將調用拷貝賦值運算符。
當使用一個對象爲另一個同類型對象賦值時調用。
如果我們沒有定義賦值運算符,那麼會生成一個合成的拷貝賦值運算符
13.7
對於StrBlob,在賦值時,將調用data的拷貝賦值運算符將一個對象的值賦值給另外一個,此時兩個對象所指向的對象引用計數+1。
對於StrBlobPtr,在賦值時,將分別調用
std::weak_ptr<vector<string>> wptr;
size_t curr;
ptr和curr對應類型中定義的拷貝賦值運算符進行賦值。對於wptr,其所指向的對象引用計數不變。
13.8
class HasPtr {
public:
HasPtr(const std::string& s = std::string()) :ps(new std::string(s)),i(0){};
HasPtr(const HasPtr& hasptr):ps(new string(*hasptr.ps)),i(hasptr.i) {
};
//列表初始化只能在構造函數中使用
HasPtr& operator=(const HasPtr& p) {
ps = new string(*p.ps);
i = p.i;
};
private:
std::string *ps;
int i;
};
13.1.3 析構函數
析構函數的作用和是回收對象的資源,銷燬對象。
使用波浪號~+類名定義析構函數,在析構函數的函數中執行我們想要在對象被釋放和銷燬之前需要做的操作。
一般來說這裏會將動態分配的內置指針delete掉。
如果我們只定義析構函數,但是函數體內什麼都不做,在析構函數的函數體執行完畢之後,會按照構造函數初始化數據成員的逆序回收對象非static數據成員的資源並銷燬對象。
這個過程是隱式的,在析構函數函數體執行之後執行。如果數據成員是類類型,銷燬該數據成員需要執行它的析構函數,如果是內置類型,則不需要執行析構,直接銷燬。
注意指針也是內置類型,所以如果數據成員中由指針數據成員,而我們在析構函數中不對這個指針做delete操作,則這個指針只會銷燬自身而不會銷燬所指向的對象。
什麼時候執行析構函數
一句話來說,由對象被銷燬(生命週期結束),則需要執行該對象的析構函數。
具體來說
1.非靜態局部變量離開作用域
2.變量被銷燬時,其成員也會被銷燬
3.容器被銷燬時
4.動態分配的內存,使用內置指針,delete該指針時
4.臨時變量表達式結束時
一定要記住回收對象的資源和銷燬對象不是在析構函數的函數體內完成的,而是在執行完析構函數之後,隱式執行的。
現在感覺拷貝構造函數,賦值拷貝函數和析構函數很像是C++提供的接口,我們可以不自己定義,他會生成默認的版本,但是我們也可以自定義他們的操作。
練習
13.9
析構函數是類中用於回收爲對象分配的資源,銷燬對象。它的工作和構構造函數相反。
合成的析構函數用於回收對象的資源並銷燬類的非static數據成員
沒有顯式定義析構函數時,編譯器生成合成析構函數/
13.10
StrBlob對象銷燬時會調用類類型數據成員的析構函數,其中數據成員data是類類型,所以調用shared_ptr<vector<string>>的析構函數。如果該對象data的指向的對象引用計數爲0。則銷燬data指向的對象
size_t類型是內置類型不用析構,而wptr是類類型,所以調用weak_ptr<vector<string>>的析構函數,回收資源銷燬對象。
std::weak_ptr<vector<string>> wptr;
size_t curr;
13.11
~HasPtr() {
delete ps;
}
13.12
因爲指針是內置類型不需要調用析構函數,所以調用哦個該函數,accum,item1,item2都會執行析構,一共三次。
13.13
需要注意的是,對於vec,當我們push_back參數時,可能因爲當前分配的空間不夠,所以需要重新分配空間,再將原來的元素拷貝進去,所以這裏會執行拷貝構造函數和析構函數,但是這並不是每次push_back都會發生這樣的情況。
struct X {
X() { cout << "X()" << endl; }
X(const X&) {
cout << "X(const X&)" << endl;
};
X& operator=(const X&) {
cout<<"operator="<<endl;
}
~X() {
cout << "~X()" << endl;
}
};
void f(X x) {
}
void f1(X& x) {
}
測試代碼
X x;//構造
f(x);//拷貝構造
f1(x);//沒有
auto p = new X();//構造
vector<X> vec(20);//構造
cout << vec.capacity() << endl;
vec.push_back(x);//拷貝構造
cout << vec.capacity() << endl;
13.1.4 三/五法則
有了拷貝構造函數,拷貝賦值函數和析構函數,我們就可以得到拷貝控制的目的。
移動構造函數和移動賦值函數是新標準加的。
通常我們定義了析構函數就一定需要定義拷貝構造和拷貝賦值函數。
因爲我們定義了析構,意味着由動態分配的內存。所以在拷貝構造和拷貝引用時也需要動態分配。
定義了拷貝構造通常需要定義拷貝賦值,但是不一定需要析構,同樣,定義了拷貝賦值往往需要定義拷貝構造,但是不一定需要析構
例子:一個類中的每一個對象都需要一個唯一id標註該對象。這個時候我們不需要析構函數
練習
13.14
因爲使用的是合成的拷貝構造,所以該函數只會對對象的數據成員直接拷貝而沒有任何特殊的操作,所以得到的唯一需要都是一樣的
13.15
會改變,因爲在自定義的拷貝構造函數中執行了自定義的操作,使得每個對象由唯一的序號,而形參是非引用類型,傳入時會執行拷貝構造函數所以f(a),f(b),f( c)三者得到的輸出都不是三個對象的需要。
13.16
輸出的序號就是a,b,c三個的序號。因爲f的形參類型時引用類型,調用函數時不會創建對象。
13.17
struct numbered {
numbered() {
num = cur_num;
++cur_num;
};
numbered(const numbered&) {
num = cur_num;
++cur_num;
};
~numbered() {
--cur_num;
}
int num;
static int cur_num;
};
int numbered::cur_num = 0;
void f(const numbered& s) {
cout << s.num << endl;
}
測試用例:
numbered a, b = a, c = b;
f(a);
f(b);
f(c);
13.1.5 使用=default
如果我們沒有定義拷貝構造,拷貝賦值運算符和析構函數,C++編譯器會爲我們合成一個。
我們也可以使用=default,顯式的告訴機器,使用合成的版本。
13.1.阻止拷貝
有時我們不希望對象可以執行拷貝或者賦值操作,比如流對象。
過去可以通過將拷貝構造函數和拷貝賦值運算符修飾爲private,使用這種方法在類外無法進行拷貝和賦值,但是在成員函數和類的友元函數中可以。
因此C++新標準推出了=delete,將函數定義爲函數的函數。
struct A{
A(const A&)=delete;
}
除了對拷貝構造函數,拷貝賦值函數,析構函數添加=delete,我們可以對任何的函數使用=delete,不過那樣做的意義不太大。
我們一般不將析構函數設置爲刪除的函數,因爲這樣做之後,對象就無法釋放對象的資源和銷燬對象了。
由編譯器合成的拷貝控制成員有可能是刪除的,這體現在,如果某個對象的數據成員不能默認構造、拷貝、複製、或者銷燬則默認的拷貝控制函數將會是刪除的。
比如,類中包含了const或者引用類型的但是沒有類內初始化的數據成員。
struct A {
const int a;
};
測試代碼
A a;
編譯器報錯
練習
13.18
struct Employee{
Employee():_id(id) {
++id;
};
Employee(const string& str) : _id(id), _name(str) { ++id; };
Employee(const Employee& ee) = delete;
Employee& operator=(const Employee& ee) = delete;
int _id;
string _name;
static int id;
};
int Employee::id = 0;
13.19
需要,但是在這裏我將拷貝構造函數和拷貝賦值函數都定義爲delete,因爲我覺得一個僱員不可能擁有兩個id。
13.20
拷貝,賦值和銷燬
TextQuery的數據成員爲
shared_ptr<std::vector<std::string>> file;
map<string,shared_ptr<set<line_no>>> wm.
QueryResult的數據成員爲
可以看到都是類類型,所以在拷貝時,執行各自類型的拷貝函數。在賦值時執行各自類別的賦值函數,在銷燬時,執行各自的析構。對於指針類型,在執行完析構之後,會查看所指向的對象引用計數是否爲0,如果爲0則釋放並刪除指針所指向的對象。
13.21
不需要,因爲所有的數據成員都是標準庫的類類型,由各自的拷貝控制函數,而且兩個類都沒有內置指針,智能指針不需要手動的維護指向對象的釋放和銷燬。