前排提醒:本文不適合初學者觀看
目錄
更加詳細地了解型別別的默認函式
默認解構式
默認建構式
默認拷貝構造
復制賦值運算子
移動構造和移動賦值函式(C++11)
總結
不要繼承任何STL容器
不要使用例外規范
不要在構造和析構中呼叫虛函式
更加詳細地了解型別別的默認函式
熟悉c艸的人知道, 如果我們宣告一個型別別(class, struct,Union),即使什么都不寫,編譯器也會為你宣告很多東西,比如我們下面的代碼并不會報錯:
using namespace std;
class nums
{
};
int main()
{
nums n1;
nums n2(n1);
nums n3;
n3 = n1;
n3.~nums();
nums n4=move(n2);
system("pause");
}
也就是說,析構,拷貝構造,移動構造,賦值操作(包括移動賦值)和建構式都已經寫好了,而且毫無以為肯定是public的,而且默認inline,如果我們不進行對應的操作比如創建復制,編譯器不會自動構造這些函式,只有在后面被使用到了才會被構建,
我們一個一個講:
默認解構式
值得注意的是有幾點:
- 若這滿足 constexpr 解構式的要求,則生成的解構式為 constexpr , (C++20 起)
- 默認的析構分為棄置析構(=delete)和平凡析構和nontrivial,需要滿足一些條件才會針對不同型別的類物件產生對應的解構式:
- 棄置析構:非靜態型別別資料成員或基類棄置解構式,或解構式不能訪問(例如將解構式設定為私有)
如果delete掉析構無法初始化任何物件,就算自定義也沒用,但是類定義是沒任何問題的 - 平凡析構:當且僅當基類析構非虛,基類為平凡析構,所有成員物件都有平凡析構
平凡解構式是不進行任何動作的解構式,有平凡解構式的物件不要求 delete 運算式,并可以通過簡單地解分配其存盤進行釋放, - nontrivial,除了棄置和平凡析構
- 棄置析構:非靜態型別別資料成員或基類棄置解構式,或解構式不能訪問(例如將解構式設定為私有)
- 如果基類或者成員物件有虛析構,那么默認析構也是虛析構
例子1 無法訪問析構:
using namespace std;
class nums {
private:
~nums()=default;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
n3.~nums1();//error
system("pause");
}
例子2 析構棄置
using namespace std;
class nums{
public:
~nums() = delete;
};
class nums1:public nums {
};
int main(){
nums1 n3; //error
n3.~nums1();//error
system("pause");
}
默認建構式
值得注意的有幾點:
- 若它滿足對于 constexpr 建構式的要求,則生成的建構式為 constexpr, (C++11 起)
- 默認構造也分為棄置和平凡構造(trivial)和nontrivial:
- 棄置構造:非靜態型別別資料成員或基類棄置或者無法訪問無參建構式,非靜態型別別資料成員或基類棄置或者無法訪問解構式,含有右值參考成員
這種情況下沒辦法使用這個類中的非靜態物件和函式,就算自定義無參的也沒用,但是類定義是沒任何問題的,
注意棄置和沒有默認構造是兩回事,如果人為加上還是沒問題的,但是棄置的意思是無論如何都沒有, - 平凡構造:當且僅當沒有虛類成員,沒有虛基類,沒有默認初始化器的非靜態成員,每個基類和成員都有平凡建構式,
平凡默認建構式是不進行任何動作的建構式, - nontrivial:除了棄置和平凡
- 棄置構造:非靜態型別別資料成員或基類棄置或者無法訪問無參建構式,非靜態型別別資料成員或基類棄置或者無法訪問解構式,含有右值參考成員
例子1 構造無法訪問
using namespace std;
class nums {
private:
nums()=default;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子2 構造棄置
using namespace std;
class nums {
public:
nums() = delete;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子3 析構棄置
using namespace std;
class nums {
public:
~nums()=delete;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子4 析構無法訪問
using namespace std;
class nums {
private:
~nums()=default;
};
class nums1 :public nums {
};
int main() {
nums1 n3; //error
system("pause");
}
例子5 右值參考
class upper
{
int&& num;
};
int main()
{
upper b1;//error
system("pause");
}
例子6 只定義拷貝構造,這個時候只是沒有默認構造,定義后可以正常使用
#include<iostream>
using namespace std;
class nums{
public:
int z = 0;
nums(const nums& n)
{
z = n.z;
}
};
int main() {
nums n; //error
system("pause");
}
例7 默認被洗掉使用有參構造
#include<iostream>
using namespace std;
class nums {
public:
nums(const int n) { cout << n << endl; };
nums() = delete;
};
class nums1 :public nums {
public:
nums1(const int z):nums(z) {};
};
int main() {
nums1 n3(1);
n3.~nums1();
system("pause");
}
默認拷貝構造
值得注意的幾點:
- 若它滿足對于 constexpr 建構式的要求,則生成的復制建構式為 constexpr, (C++11 起)
- 類可以擁有多個復制建構式,當存在用戶定義的復制建構式時,用戶仍可用關鍵詞 default 強迫編譯器生成隱式宣告的復制建構式(C++11),
- 省略復制及移動 (C++11 起)建構式,導致零復制的按值傳遞語意,
當運算元和回傳型別是純右值的時候,(C++17)
當初始化器運算式與變數型別為同一型別的純右值時(C++17)T f() { return T(); } f(); // 僅呼叫一次 T 的默認建構式T x = T(T(f())); // 僅呼叫一次 T 的默認建構式以初始化 x - 棄置復制構造:類內有無法復制非靜態資料(棄置或者無法訪問),或有無法復制直接或者虛基類(棄置或者無法訪問),或有被棄置或者無法訪問的析構, 或有右值參考類成員(c++11起),因為可以同時有多個復制構造,因此就算是棄置也可以用戶自己定義
- 平凡復制構造:沒有虛基類,沒有虛成員函式,每個基類和每個型別的非靜態成員的復制構造都是平凡的,非聯合類的平凡復制建構式,效果為復制實參的每個標量子物件(遞回地包含子物件的子物件,以此類推),且不進行其他動作,不過不需要復制填充位元組,甚至只要其值相同,每個復制的子物件的物件表示也不必相同,
- nontrivial復制構造:除了上面兩個
例1 基類拷貝構造被棄置
using namespace std;
class base
{
public:
base() {};
base(const base&) = delete;
};
class upper:public base
{
public:
};
int main()
{
upper b1;
upper b2(b1);//error
system("pause");
}
例2 右值參考的成員
class upper
{
public:
upper(int&& temp):num(forward<int>(temp)) {};
int&& num;
};
int main()
{
upper b1(1);
upper b2(b1);//error
system("pause");
}
例子3 有無法復制的成員
using namespace std;
class base
{
public:
base() {};
base(const base&) = delete;
};
class upper
{
base b1;
public:
};
int main()
{
upper b1;
upper b2(b1);//error
system("pause");
}
例4 默認被洗掉呼叫其他拷貝構造
using namespace std;
class nums {
public:
nums() {};
nums(const nums& n,const int z) {
cout << z << endl;
};
nums(const nums& n) = delete;
};
int main() {
nums n1;
nums n2(n1,1);
system("pause");
}
復制賦值運算子
值得注意的幾點:
- 類可以擁有多個復制賦值運算子,如 T& T::operator=(const T&) 和 T& T::operator=(T),當存在用戶定義的復制賦值運算子時,用戶仍可用關鍵詞 default 強迫編譯器生成隱式宣告的復制賦值運算子, (C++11 )
- 若用戶自定義移動賦值,則復制復制被棄置
- 有const限定的或參考型別的非靜態資料成員,被棄置
- 有無法復制的非靜態成員,直接或者(虛)基類,被棄置
- 用戶自定義析構,被棄置
- 因為可以同時有多個復制賦值,因此就算是棄置也可以用戶自己定義
- 如果沒有虛成員或者虛基類,每個成員和基類的賦值運算子都是平凡的,沒有volitile限定的非靜態資料成員(C++14),那么它就是平凡的
- 否則為nontrivial
例1 移動賦值被定義
using namespace std;
class nums1{
public:
nums1 operator =(const nums1&& n) {
cout << 1 << endl;
return {};
};
};
int main() {
nums1 n1;
nums1 n2;
n2 = n1;//error
system("pause");
}
例2 有const成員變數
using namespace std;
class nums1{
public:
const int num = 1;
};
int main() {
nums1 n1;
nums1 n2;
n2 = n1;//error
system("pause");
}
例3 無法復制賦值的基類
using namespace std;
class nums {
private:
nums operator =(const nums& n) { return n};
};
class nums1 :public nums {
public:
};
int main() {
nums1 n1;
nums1 n2;
n2 = n1; //error
system("pause");
}
例4 因為析構無法賦值
using namespace std;
class nums{
const int n = 0;
public:
~nums() {};
};
int main() {
nums n;
nums n2;
n2 = n;//error
system("pause");
}
移動構造和移動賦值函式(C++11)
編譯器將默認宣告一個移動建構式,作為其類的非 explicit 的 inline public 成員,簽名為 T::T(T&&), 或者一個移動賦值運算子:作為其類的 inline public 成員,并擁有簽名 T& T::operator=(T&&),
棄置和平凡的定義與對應的復制版本一致,不再贅述,加上一點:
- 有用戶宣告的移動賦值(構造)運算子,則移動構造(賦值)被棄置,
移動賦值函式,如果等號右邊是右值,會優先使用移動復制,否則也會用拷貝賦值,因此就算我們沒有移動賦值,編譯器同樣不會報錯,
例子1 宣告移動構造后呼叫移動賦值
using namespace std;
class nums{
public:
nums(const nums&& n) {}
nums() {};
};
int main() {
nums n;
nums n2;
n2= move(n);//error
system("pause");
}
總結
委員會真的有大病,難怪沒人跟他們玩
不要繼承任何STL容器
相信各位對虛析構都很熟悉,它的作用也都知道,比如我們在使用工廠模式進行軟體開發時候對記憶體泄漏有很好的防范作用,與純虛析構的區別在與能不能實體化(instance)
但是遺憾的是c++中STL容器幾乎都不是用虛析構的容器,比如我們常用的string,我們看一下原始碼:

不要使用例外規范
例外規范是在程式后寫一個throw表明希望拋出什么型別的錯誤,比如這樣:
void func()throw(char*, exception) {
throw 100;
cout << "[1]This statement will not be executed." << endl;
}
這表明只能拋出string型別的exception,但是實際上如果我們運行下面代碼:
using namespace std;
void f()throw(string, exception) {
throw 100;
}
int main() {
try {
f();
}
catch (int) {
cout << "22222222222" << endl;
}
return 0;
}
結果表明我們拋出了int型別的錯誤

這是在98版本中新增的功能,在C++11后被拋棄,但是并不會有語法錯誤,
此外我們注意到string的析構帶有關鍵字noexcept,我們在定義析構的時候也建議使用,其實主要目的在于確保析構不會拋出例外,因為比如我們定義了一連串的存放某種類的陣列,在delete的時候,如果每個析構都出問題,那么一連串的例外會導致不明確行為,
因此我們最好在析構中捕捉例外,不讓其傳播,
不要在構造和析構中呼叫虛函式
這一點很好理解,比如子類呼叫建構式,父類建構式呼叫一個虛函式,那么我們生成子類的時候就呼叫了父類定義的虛函式而不是子類的,解決方法就是用引數傳遞的方式講子類需要自定義的東西傳入給父類的非虛函式中,或者用placeholder,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/352293.html
標籤:其他
上一篇:C | 檔案的操作
