初始化列表
首先,初始化列表是我們的祖師爺本賈尼博士為了解決在某些成員變量在定義時必須初始化的情況。這個初始化列表其實發生在構造函數之前,也就是實例化整個對象時先對所有的成員都進行了初始化
初始化的概念區分?
在之前的博客學習中,我們已經學習了【C++】的六大默認成員函數?,想必大家已經對構造函數已經比較熟悉了,可是大家是否遇到過,在構造函數后面跟了一個冒號,這個問題讓我很是困惑
在了解?初始化列表之前,我們首先回顧兩個重要的知識:
1. 構造函數是干嘛的?
答:?用于初始化類中的成員變量
2. 什么是初始化?
答:?在創建對象時,編譯器通過調用構造函數,給對象中各個成員變量一個合適的初始值
接下來再來看一段代碼:
class Date
{
public://構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
? ? ? ? 上面這個Date類是我們之前寫過的,這里有一個它的有參構造函數,雖然在這個構造函數調用之后,對象中已經有了一個初始值,但是不能將其稱為對對象中成員變量的初始化。構造函數體中的語句只能將其稱為【賦初值】,而不能稱作初始化。因為初始化只能初始化一次,而構造函數體內可以多次賦值。
class Date
{
public:Date(int year = 2022, int month = 5, int day = 24){_year = year;_year = 2023; //第二次賦值_year = 2024; //第三次賦值_month = month;_day = day;}
private:int _year;int _month;int _day;
};
既然構造函數體的語句只能稱作為賦初值,現在,可否有一種方式進行初始化呢?即初始化列表初始化。
總結????
- 我們之前寫的構造函數其實并不是對成員變量進行初始化而是進行賦初值。
- 如果想要對成員變量進行初始化,需要用到初始化列表
初始化列表的概念理解
?以一個冒號?“ :”?開始,接著是一個以 , 分隔的數據成員列表,每個"成員變量"后面跟一個放在?()的初始值或表達式
例如如下代碼:
class Date
{
public://構造函數: -->初始化列表初始化Date(int year = 2024, int month = 8, int day = 2):_year(year), _month(month), _day(day){}
private:int _year;int _month;int _day;
};
當然,我可以在初始化列表初始化,也可以在大括號內進行賦值:
Date(int year = 2024, int month = 8, int day = 2):_year(year), _month(month)
{_day = day;
}
初始化列表的注意事項
初始化列表可以認為就是對象成員變量定義的地方
每個成員變量在初始化列表中只能出現一次(初始化只能初始化一次)
類中包含以下成員,必須放在初始化列表位置進行初始化
- 引用成員變量
- const成員變量
- 自定義類型成員(該類沒有默認構造函數)
先前我們都知道引用的變量和const變量只能在定義時初始化,而普通的變量在定義時不強求初始化,所以我們就不能按照如下的方式操作:
?成員變量為const和引用的時候-----正確的代碼為:
class Date
{
public://析構函數Date(int year = 12, int month = 10, int day = 1):_year(year), _month(month), _day(day){}void Printf(){cout << "year為:" << _year << endl;cout << "month為:" << _year << endl;cout << "day為:" << _year << endl;}
private://定義時不強求初始化,后面可以再賦值修改int _year; //聲明//const修飾的變量 和 引用的變量 需要在定義的時候就進行初始化const int _month;int& _day;
};int main()
{Date d1;d1.Printf();return 0;
}
自定義類型成員(該類沒有默認構造函數)同樣也得在初始化列表進行初始化:?
class A
{
public:A(int x) //不是默認構造函數,因為接受一個參數:_x(x){}private:int _x;
};class Date
{
public:Date(int a) //在初始化列表對自定義類型 _aa 進行初始化:_aa(a){}
private:A _aa;
};
注意這里的條件,一定要是沒有默認構造函數的自定義類型成員才得在初始化列表進行初始化,而默認構造函數簡單來說就是不需要傳參的函數
成員變量在類中聲明次序就是其在初始化列表中的初始化順序,與其在初始化列表中的先后次序無關
class A
{
public:A(int a):_a1(a), _a2(_a1){}void Print(){cout << _a1 << " " << _a2 << endl;}
private:int _a2;int _a1;
};
int main()
{A aa(1);aa.Print();
}
A、輸出1 1? ? ? ? B、程序崩潰? ? ? ? C、編譯不通過? ? ? ? D、1? ?隨機值
答案:D
?解析:注意成員變量在類中聲明次序就是其在初始化列表中的初始化順序,既然_a2先聲明,則必然進入初始化列表要先執行, _a2(_a1) 。意思是說拿_a1去初始化_a2,不過此時的_a1還是隨機值,自然_a2即為隨機值,隨后執行:_a1(a)。拿a初始化_a1,所以輸出的值為1和隨機值。
explicit關鍵字
在我們自己平時寫?C++ 代碼的時候,較少會用到?explicit關鍵字?。但是在C++相關的標準類庫中,看到explicit關鍵字的頻率還是很高的。既然出現的頻率這么高,那么我們就來看看explicit關鍵字的作用到底是干什么的。
什么是explicit關鍵字
? explicit是C++中的一個關鍵字,它用來修飾只有一個參數的類構造函數,以表明該構造函數是顯式的,而非隱式的。當使用explicit修飾構造函數時,它將禁止類對象之間的隱式轉換,以及禁止隱式調用拷貝構造函數。?
既然解釋中提到了?類的構造函數? 那么下面我將從構造函數中詳細的給大家,講解explicit其中的含義。
構造函數還具有類型轉換的作用
? 在理解?explicit 關鍵字?之前,我們必須要了解構造函數的類型轉換作用,以便于我們更好的理解?explicit 關鍵字
單參構造函數與explicit關鍵字
還是來說說老朋友日期類,我們通過下面這個日期類進行講解
class Date
{
public:
// 構造函數Date(int year):_year(year) // 初始化列表{}private:int _year;int _month = 3;int _day = 31;
};
?對于下面的?d1?很清楚一定是調用了有參構造進行初始化,不過對于?d2?來說,也是一種構造方式
int main()
{// d1 和 d2 都會調用構造函數Date d1(2022); Date d2 = 2023;return 0;
}
我們依舊通過調試來看就會非常清晰,這種?【Date d2 = 2023】?寫法也會去調用構造函數
此時,大家可能會產生疑問,這種構造方式從來沒有見過,為什么?Date d2 = 2023?會調用?構造函數呢?? 其實這都是因為有【隱式類型轉換】的存在,下面我將從一個簡單的例子來為大家講解。
像下面將一個int類型的數值賦值給到一個double類型的數據,此時就會產生一個隱式類型轉換
int i = 1;
double d = i;
?對于類型轉換而言,這里并不是將值直接賦值給到左邊的對象,而是在中間呢會產生一個臨時變量,例如右邊的這個?i?會先去構造一個臨時變量,這個臨時變量的類型是?[double]?。把它里面的值初始化為 1,然后再通過這個臨時對象進行拷貝構造給d,這就是編譯器會做的一件事
那對于這個?d2?其實也是一樣,2023會先去構造一個臨時對象,這個臨時對象的類型是[Date]
把它里面的year初始化為2023,然后再通過這個臨時對象進行拷貝構造給到d2
不是說構造函數有初始化列表嗎?拷貝構造怎么去初始化呢?
?別忘了【拷貝構造】也是屬于構造函數的一種哦,也是會有初始化列表的
//拷貝構造
Date(const Date& d):_year(d._year),_month(d._month),_day(d._day)
{}
剛才說到了中間會產生一個臨時對象,而且會調用構造 + 拷貝構造,那此時我們在Date類中寫一個拷貝構造函數,調試再去看看會不會去進行調用
- 很明顯沒有,我在進入Date類后一直在按F11,但是卻進不到拷貝構造中,這是為什么呢?
原因其實在于編譯器在這里地方做了一個優化,將【構造 + 拷貝構造】優化成了【一個構造】,因為編譯器在這里覺得構造再加拷貝構造太費事了,干脆就合二為一了。其實對于這里的優化不同編譯器是有區別的,像一下VC++、DevC++可能就不會去優化,越是新的編譯器越可能去進行這種優化。
但是怎么知道中間賦值這一塊產生了臨時對象呢?如果不清楚編譯器的優化機制這一塊肯定就會認為這里只有一個構造?
這點確實是,若是我現在不是直接賦值了,而是去做一個引用,此時會發生什么呢?
Date& d3 = 2024;
?可以看到,報出了一個錯誤,原因就在于d3是一個Date類型,2024則是一個內置類型的數據
一個常量讓d3共用會造成權限放大!!
- 但若是我在前面加一個
const
做修飾后,就不會出現問題了,這是為什么呢? - 其實這里的真正原因就在于產生的這個【臨時變量】(臨時變量具有常性),它就是通過Date類的構造函數構造出來的,同類型之間可以做引用。還有一點就是臨時變量具有常性,所以給到一個
const
類型修飾對象不會有問題?
從這里我們就可以看到在中間賦值的時候是產生了臨時變量。
但若是你不想讓這種隱式類型轉換發生怎么辦呢?此時就可以使用到C++中的一個關鍵字叫做explicit
?
- 它加在構造函數的前面進行修飾,有了它就不會發生上面的這一系列事兒了,它會【禁止類型轉換】
explicit Date(int year):_year(year)
{}
多參構造函數與explicit關鍵字
//多參構造函數
Date(int year, int month ,int day = 31):_year(year),_month(month),_day(day)
{}
根據從右往左缺省的規則,我們在初始化構造的時候要給到2個參數,d1
沒有問題傳入了兩個參數,但是若是像上面那樣沿襲單參構造函數這么去初始化還行得通嗎?很明顯不行,編譯器報出了錯誤
這個時候就要使用到我們C++11中的新特性了,在對多參構造進行初始化的時候在外面加上一個{}
就可以了,可能你覺得這種寫法像是C語言里面結構體的初始化,但實際不是,而是在調用多參構造函數
Date d2 = { 2023, 3 };
- 不僅如此,對于下面這種也同樣適用,調用構造去產生一個臨時對象
const Date& d3 = { 2024, 4 };
那要如何去防止這樣的隱式類型轉換的發生呢,還是可以使用到explicit
關鍵字嗎?
//多參構造函數
explicit Date(int year, int month ,int day = 31):_year(year),_month(month),_day(day)
{}
還有一種例外,當缺省參數從右往左給到兩個的時候,此時只需要傳入一個實參即可,那也就相當于是單參構造explicit
關鍵字依舊可以起到作用
explicit Date(int year, int month = 3,int day = 31):_year(year),_month(month),_day(day)
{}
友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以
友元不宜多用。
友元分為:友元函數和友元類
友元函數
去重載operator<<,然后發現沒辦法將operator<<重載成成員函數。因為cout的輸出流對象和隱含的this指針在搶占第一個參數的位置。this指針默認是第一個參數也就是左操作數了。但是實際使用中cout需要是第一個形參對象,才能正常使用。所以要將operator<<重載成全局函數。但又會導致類外沒辦法訪問成員,此時就需要友元來解決。operator>>同理。
? ? ?但是在類外定義的話沒辦法直接用類里面的私有成員,如果強行變成公有就破壞了封裝性,所以這里會用到友元的知識,友元函數可以直接訪問類的私有成員,它是定義在類外部的普通函數,不屬于任何類,但需要在類的內部聲明,聲明時需要加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;
};
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、友元函數不能用const修飾(沒有this指針)
3、友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制
4、一個函數可以是多個類的友元函數
5、友元函數的調用與普通函數的調用原理相同
友元類?
友元類的所有成員函數都可以是另一個類的友元函數,都可以訪問另一個類中的非公有成員。
class Time
{
friend class Date; // 聲明日期類為時間類的友元類,則在日期類中就直接訪問Time類
中的私有成員變量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接訪問時間類私有的成員變量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
注意:
1、友元關系是單向的,不具有交換性。
? ? ? ? 比如上述Time類和Date類,在Time類中聲明Date類為其友元類,那么可以在Date類中直接
訪問Time類的私有成員變量,但想在Time類中訪問Date類中私有的成員變量則不行。
2、友元關系不能傳遞
? ? ? ?如果C是B的友元, B是A的友元,則不能說明C時A的友元。?
3、友元關系不能繼承?
內部類
內部類的原理:B的房子的圖紙在A的圖紙里面。比如說我是地產大亨,你之前幫助過我。最近你跟我說你要在你工作的地方建個別墅。我說好,你建吧,并且我還附贈你一個別墅,把你老家也建造起來。此時 你工作的別墅就不包含你老家的別墅。然后你說,工作的別墅我不要了,能不能單獨給我老家建起來,大亨說:你還想空手套白狼是吧 想得美!
內部類的定義
內部類是在一個類的成員部分定義的另一個類。
內部類是?個獨立的類。跟定義在 全局相比,他只是受外部類類域限制和訪問限定符限制,所以外部類定義的對象中不包含內部類。
計算外部類對象的大小就不會將內部類的成員包括在內
內部類與外部類?
內部類訪問外部類的成員
簡單來說:內部類默認是外部類的友元類
- 內部類可以定義在外部類的public、protected、private中都是可以的。
- 內部類可以直接訪問外部類中的static、枚舉成員、不需要外部類的對象名。
- 內部類訪問外部類的普通成員,需要借助外部類對象(否則無法得知訪問的是哪一個對象的)
這里cout<<h<<endl;是一個非常常見的錯誤。因為內部類是一個獨立的類,不屬于外部類,所以此時還沒有外部類的對象,顯然也不存在h。
而k就不同了,靜態成員不需要外部類的對象就已存在,所以這里k是OK的。
想要在內部類訪問外部類的普通成員,就需要通過外部類對象的方式,比如下方代碼,傳遞一個外部類對象作為參數就可以訪問外部類成員
內部類的經典實例
使用的特點就是內部類天生是外部類的友元
class Solution {
public:int Sum_Solution(int n) {Sum a[n];//根據創建數組多少次就調用多少次構造函數return _sum;}
private:class Sum{public:Sum(){_sum+=_i;++_i;}};//內部類可以訪問外部類的靜態成員static int _i;static int _sum;
};
int Solution::_i=1;
int Solution::_sum=0;
外部類訪問內部類的成員?
內部類不屬于外部類,更不能通過外部類的對象去調用內部類。外部類對內部類沒有任何優越的訪問權限。
內部類在定義它的外圍類的作用域內是可見的,但在外圍類外部是不可見的。?
- 假如內部類受外部類公有限定符限制,需要通過外部類限定符限定的方式創建內部類對象
- 假如內部類是受外部私有或保護限定符限制,那么在類外無法創建內部類的對象(如果你不想外部可以創建內部類的對象,就可以這么做)
- 外部類無法訪問內部類的私有成員
class A
{
private:static int _k;int _h = 1;
public:class B // B默認就是A的友元{public:void foo(const A& a){cout << _k << endl; //OKcout << a._h << endl; //OK}private:static int a;};void fun(){//cout << B::a << endl;//外部類無法訪問內部類的私有成員}};
int A::_k = 1;
int main()
{A::B b;//假如內部類受外部類公有限定符限制,可以這樣創建內部類對象A aa;return 0;
}
?內部類的使用場景
- 封裝和隱藏實現細節:內部類可以隱藏實現細節,使得外圍類的接口更加簡潔。—內部類本質也是?種封裝,當A類跟B類緊密關聯,A類實現出來主要就是給B類使用,那么可以考 慮把A類設計為B的內部類,如果放到private/protected位置,那么A類就是B類的專屬內部類,其他地方都用不了
- 實現輔助類:內部類可以作為外圍類的輔助類,用于實現一些具體的功能,而不影響外圍類的整體結構。
- 避免命名沖突:通過內部類,可以避免不同命名空間或類中的命名沖突。
- 訪問權限控制:內部類可以更好地控制對特定成員或方法的訪問權限。
?匿名對象
匿名對象的生命周期在當前行
匿名對象具有常性
const引用會延長匿名對象的的生命周期,聲明周期跟當前函數的作用域
即用即銷毀