涉及的關于類中的默認成員函數的知識點可以看我的這篇博客哦~
C++入門必須知道的知識?類的默認成員函數,一文講透+運用
目錄
初始化列表
類型轉換
static成員?
友元
內部類
匿名對象
?對象拷貝時的一些編譯器的優化
初始化列表
我們知道類中的構造函數的任務是完成對象的初始化,使用構造函數完成初始化的方式除了在函數體內對成員變量進行賦值的方式,如下:
class Date
{
public:// 構造函數Date(int year, int month, int day){// 函數體內賦值初始化_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
還有一種方式可以進行初始化——初始化列表
初始化列表的格式:
以一個冒號開始,接著是一個以逗號分隔的數據成員列表,每個“成員變量”后面跟一個放在括號中的初始值或者表達式?
比如上面的構造函數就可以寫成下面的樣子:?
class Date
{
public:// 初始化列表的形式Date(int year,int month,int day):_year(year),_month(month),_day(day){}
private:int _yeaer;int _month;int _day;
};
兩種初始化的區別:
使用函數體內賦值的方式時,成員變量會先進行默認初始化,然后再在構造函數體內被賦值。也就是在函數體內被賦值時不能稱為"初始化",只能稱為"賦值",初始化只能進行一次,而構造函數體內可以多次賦值;
如果用初始化列表,則直接跳過默認初始化步驟,一步到位完成初始化
默認初始化:指當對象被創建時,如果沒有顯式指定初始值,編譯器會自動進行的初始化行為。具體行為取決于成員變量的類型。具體行為如下:
-
內置類型(如?
int
,?double
, 指針等):-
如果變量在全局作用域(或靜態存儲區),默認初始化為?
0
/nullptr
。 -
如果變量在局部作用域(如函數內、類構造函數體內),不初始化,值是未定義的(垃圾值)。
-
-
類類型(如?
std::string
, 自定義類):-
調用該類的默認構造函數(如果沒有默認構造函數,會編譯報錯)。
-
兩種方式在面對自定義類型的初始化時,還會有效率的差異,初始化列表的效率更高:
使用函數體內賦值的方式,會先去調用自定義類型的構造函數,然后再調用賦值重載將構造的值賦值給成員變量
使用初始化列表的方式,對于自定義類型只會調用一次拷貝構造函數一次性完成初始化
初始化列表的注意事項:
1、每個成員變量在初始化列表中只能出現一次(即初始化只能初始化一次)?
2、類中包含以下成員時,它們的初始化必須放在初始化列表位置進行(否則編譯報錯):
- 引用成員變量(因為引用在定義初始化時必須賦值,且不能更改引用的對象)
- const修飾的成員變量(const的原因與引用類似)
- 自定義類型的成員變量,且該成員變量的類沒有默認構造函數時(因為使用函數體內的方式時,對于自定義類型,會先去調用其構造函數)
3、成員變量在類中的聲明順序就是在初始化列表的初始化順序,與其在初始化列表中的位置順序無關(★),所以建議初始化列表的順序和聲明的順序一致
關于第三條:如下述代碼,會得到錯誤的結果,因為雖然初始化列表中的順序是先初始化_a,再用_a的值初始化_b,看著是沒錯,但是因為成員變量聲明時,是先聲明的_b,再是_a,所以初始化時會先初始化_b,而不是_a,也就是初始化順序只和聲明的順序相關,和初始化列表中的順序無關,但是因為_a還未初始化,所以_b的值就會是隨機數
class A
{
public:// 初始化列表A(int a):_a(a),_b(_a+1){}
private:int _b;int _a;
};
另外,C++11還支持在成員變量聲明的位置給缺省值,這個缺省值主要是給沒有顯示在初始化列表初始化的成員使用的,如:
class A
{
public:A(int a):_a1(a){}
private:int _a2 = 2;int _a1;
};
此時,_a2初始化后的值就是2,相當于_a2也走了初始化列表,只不過用的聲明時給的缺省值進行初始化,_a1初始化后的值就是傳給參數a的值?
總結:
盡量使用初始化列表初始化,因為無論是否顯示寫初始化列表,每個構造函數都有初始化列表;?論是否在初始化列表顯示初始化,每個成員變量都要走初始化列表初始化,如果這個成員在聲明位置給了缺省值,初始化列表就會用這個值進行初始化。如果你沒有給缺省值,那么編譯器對其的初始化是不確定的,且對于自定義類型,使用初始化列表的方式會更加高效一點
?成員變量初始化思維導圖:
類型轉換
C++支持內置類型隱式類型轉換為類類型的對象,需要有相關內置類型為參數的構造函數
如下:使用一個int類型的值構造了一個A類型的對象,其中發生了隱式類型轉換,將int類型轉為自定義類型A
class A
{
public:// 以int類型為參數的相關構造函數A(int a):_a(a){}
private:int _a;
};
int main()
{// 使用int類型的1構造了一個A類型的對象A b(1);return 0;
}
explicit關鍵字?
但是上述的隱式類型轉換,如果你不想讓它發生可以使用explicit關鍵字對構造函數進行修飾,那么這種隱式類型就會被禁止
// 此時再想用一個數字去構造A類型的對象就會編譯報錯
class A
{
public:// 以int類型為參數的相關構造函數explicit A(int a):_a(a){}
private:int _a;
};
對于內置類型隱式類型轉換構造自定義類型時,相關內置類型的構造函數必須要有,且需要保證傳入該內置類型參數時,可以構造出一個對象
除了內置類型和類類型的隱式類型轉換,類類型的對象之間也可以隱式轉換,需要相應的構造函數的支持
如下方的將A類型的對象隱式類型轉換為B類型的對象進行構造,構造出一個臨時對象用來拷貝構造B類型的對象b,但是編譯器會優化,所以就變成了直接用A類型的對象構造出了一個B類型的對象,但這個過程是因為有相關類型參數的構造函數支持才完成的
class A
{
public:A(int a1, int a2):_a1(a1), _a2(a2){}int Get() const{return _a1 + _a2;}
private:int _a1 = 1;int _a2 = 2;
};
class B
{
public:// 臨時對象具有常性,所以需要用const接收B(const A& a):_b(a.Get()){}
private:int _b = 0;
};
int main()
{// { 2,2 }構造?個A的臨時對象,再?這個臨時對象拷?構造a// 編譯器遇到連續構造+拷?構造->優化為直接構造A a = { 2,2 }; // C++11之后才?持的多參數轉化// a 隱式類型轉換為b對象// 原理跟上?類似B b = a;return 0;
}
static成員
?定義:
被static修飾的成員稱為static成員,也叫類的靜態成員
- 靜態成員也是類的成員,受public、protected、private訪問限定符的限制
- 突破類域可以訪問靜態成員,可以通過類名::靜態成員 或者 對象.靜態成員?來訪問靜態成員變量和靜態成員函數
由static修飾的成員變量稱為靜態成員變量,由static修飾的成員函數稱為靜態成員函數。
靜態成員變量:?
靜態成員變量一定要在類外進行初始化,即不在構造函數初始化
靜態成員變量為類的所有對象共享,不屬于某個具體的對象,不存在對象中,存放在靜態區
靜態成員變量不能在聲明位置給缺省值初始化,因為缺省值是給構造函數初始化列表初始化的,靜態成員變量不屬于某個對象,不走構造函數的初始化列表
靜態成員函數:?
靜態成員函數沒有this指針
靜態成員函數中可以訪問其他的靜態成員,但是不能訪問非靜態的,因為沒有this指針
非靜態成員函數可以訪問任意的靜態成員變量和靜態成員函數
場景:統計一個類實例化了多少個對象——使用靜態成員變量
#include<iostream>
using namespace std;
class A
{
public:A(){count++;}static int getCount(){return count;}
private:static int count;
};
int A::count = 0;
int main()
{A a1;A a2;A a3;cout << "創建了"<< A::getCount() << "個A類型對象" << endl;return 0;
}
在A類型的構造函數中使用靜態成員變量count++進行計數,則每初始化一個對象就會對其進行一次++,最終就會得到實例化出的對象個數,同時注意count變量是在類外進行初始化的,只是在類內聲明,且在訪問靜態成員時都使用了類域::靜態成員變量的方式(A::count、A::getCount)
上方代碼,如果是非靜態的成員函數getCount()的話,就通過對象.靜態成員函數的方式獲得靜態成員變量count的值
// 非靜態成員函數
int getCount()
{return count;// 返回靜態成員變量
}
// 其余代碼一致....
int main()
{A a1;A a2;A a3;cout << "創建了"<< a3.getCount() << "個A類型對象" << endl;return 0;
}
友元
定義:
友元分為友元函數和友元類,在一個類里,給函數聲明或者類的聲明前面加上friend的修飾,則稱其為類的友元函數或者友元類,成為類的友元后可以訪問類中的私有成員和保護成員
友元類:
class A
{// 友元聲明,B這個類變成A的友元類friend class B;
private:int _a1 = 1;
};class B
{
public:void func(const A& aa){// 友元類B就可以訪問A類的私有成員cout << aa._a1 << endl;cout << _b1 << endl;}
private:int _b1 = 3;
};
int main()
{A aa;B bb;bb.func(aa);return 0;
}
成為一個類的友元類后,友元類的所有成員函數都可以是另一個類的友元函數,都可以訪問另一個類中的非公有成員?
- 友元關系是單向的,不具有交換性。(比如上面的B類可以訪問A類的私有成員,但是A類就不可以反過來訪問B類的非公有成員,因為A類不是B類的友元類)
- 友元關系不能傳遞(如果C是B的友元, B是A的友元,則不能說明C時A的友元)
- 友元關系不能繼承(涉及繼承知識)
友元函數:
友元函數可以直接訪問類的私有成員,它是定義在類外部的普通函數,不屬于任何類,即不屬于類的成員函數,但需要在類的內部聲明,聲明時需要加friend關鍵字,如下
class Date
{// 友元函數聲明friend ostream& operator<<(ostream& _cout, const Date& d);friend istream& operator>>(istream& _cin, Date& d);
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
// 自定義Date類的輸入輸出
ostream& operator<<(ostream& _cout, const Date& d)
{_cout << d._year << "-" << d._month << "-" << d._day;return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{_cin >> d._year;_cin >> d._month;_cin >> d._day;return _cin;
}
int main()
{
Date d;
cin >> d;
cout << d << endl;
return 0;
}
1、友元函數可以在類定義的任意地方聲明(注意:是在類定義的地方聲明,類的定義有聲明和定義都在一個文件中實現的方式,也有聲明定義分離的方式,如果是分離的方式,要在定義的地方聲明友元函數),且它不受任何訪問限定符的限制
2、一個函數可以是多個類的友元函數
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多用
內部類
定義:
如果一個類定義在另一個類的內部,這個內部的類就叫做內部類。內部類是一個獨立的類,跟定義在全局相比,他只是受外部類類域限制和訪問限定符限制,所以它不屬于外部類,更不能通過外部類的對象去訪問內部類的成員。外部類對內部類沒有任何優越的訪問權限。
注意:
- 內部類默認就是外部類的友元類,內部類可以通過外部類的對象參數來訪問外部類中的所有成員。但是外部類不是內部類的友元。
- 內部類可以訪問的外部的所有成員中包括外部類的靜態成員,不需要外部類的對象/類名來突破類域訪問
- sizeof(外部類)=外部類,和內部類沒有任何關系。
內部類本質也是一種封裝,當A類跟B類緊密關聯,A類實現出來主要是給B類使用時,可以考慮將其設為B類的內部類,?如果放到private/protected位置,那么A類就是B類的專屬內部類,其他地方都用不了。
匿名對象
定義:
用類型(實參) 定義出來的對象叫做匿名對象,前面我們使用?類型 對象名(實參) 格式定義出來的叫有名對象
// 假設有一個名為A的類,且可以用int類型隱式轉換初始化
int main()
{A aa1(1);// 有名對象A(1);// 匿名對象
}
如果A類的構造是可以無參的,那么A類的匿名對象的創建也不可以省略掉括號,否則語法出錯
A();// 無參的A類型匿名對象
注意:匿名對象的生命周期只在當前一行,一般臨時定義一個對象當前用一下即可,就可以使用匿名對象
?匿名對象調用場景:
?①成員函數調用:
class Solution {
public:int Sum_Solution(int n) {//...return n;}
};
int main()
{// 沒有匿名對象時候,需要寫兩行調用Solution s1;cout << s1.Sum_Solution(10) << endl;// 有了匿名對象直接寫成一行調用cout << Solution().Sum_Solution(10) << endl;
}
?②函數參數自定義類型,需要給參數缺省值:給匿名對象
void func(A aa = A(1))
{}
?延長匿名對象生命周期:
?引用匿名對象,延長到和引用的生命周期一致,
?但是匿名對象和臨時對象一樣具有常性,要加const
const A& r = A();
匿名對象就像一個一次性的事物,用完就可丟~
?對象拷貝時的一些編譯器的優化
現代編譯器會為了盡可能提?程序的效率,在不影響正確性的情況下會盡可能減少?些傳參和傳返回值的過程中可以省略的拷貝
假設一個類的定義如下:
class A
{
public:// 構造函數A(int a = 0):_a1(a){cout << "A(int a)" << endl;}// 拷貝構造A(const A& aa):_a1(aa._a1){cout << "A(const A& aa)" << endl;}// 賦值重載A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a1 = aa._a1;}return *this;}// 析構函數~A(){cout << "~A()" << endl;}private:int _a1 = 1;
};
傳參時的編譯器優化:
連續構造+拷貝構造優化——>直接構造,省略了一次拷貝
void f1(A aa)
{}
int main()
{// 優化// 按理這里會先構造臨時對象,再把臨時對象拷貝構造給aa0// 但是這里優化成直接用1進行構造了A aa0 = 1;// 隱式類型轉換cout << endl;// 優化// 按理是用1構造一個匿名對象,再把匿名對象拷貝構造給aa// 隱式類型轉換,連續構造+拷貝構造->優化為直接構造f1(1);// 優化// 一個表達式中,連續構造+拷貝構造->優化為一個構造f1(A(2));cout << endl;return 0;
}
傳值返回時的編譯器優化:
兩次連續的拷貝構造優化成一次拷貝構造
A f2()
{A aa;return aa;// 不會返回aa,會用aa創建一個臨時對象,返回臨時對象
}
// 傳值返回的優化
int main()
{// 按理需要先用返回值aa拷貝構造一個臨時對象,再用臨時對象拷貝構造aa2// 優化:兩個拷貝構造合二為一為一個拷貝構造// 更新的編譯器優化:直接優化成一次構造,直接用aa的值構造aa2A aa2 = f2();cout << endl;// 按理是返回值aa拷貝構造一個臨時對象,再用臨時對象賦值拷貝給aa1// 優化:直接用aa的值賦值拷貝給aa1A aa1 = 1;aa1 = f2();cout << endl;return 0;
}
完