一、再談構造函數
1.1 構造函數體內賦值
我們知道,在創建對象時,編譯器會自動調用構造函數給對象中的各個成員變量一個合適的初始值
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};
雖然調用構造函數后,各個成員變量已經有了一個初始值,但是對于這種在函數體內賦值的語句不能稱為對成員變量的初始化,只能將其稱為賦初值。
因為初始化只能有一次,而構造函數體內可以寫多個賦值的語句,例如:
1.2 初始化列表
像這樣,被const修飾的成員變量,必須在聲明時就進行初始化
雖然C++11允許我們給成員變量一個缺省值,但是C++11以前是怎么做的呢?
這里引入一個新的概念:初始化列表
初始化列表定義在構造函數的函數名和函數體之間,以一個冒號開始,接著是一個用逗號分隔的數據成員列表,每個成員變量后面跟著一個放在括號中的初始值或表達式,例如:
Date(int year, int month, int day): _year(year), _month(month), _day(day)
{}
像這樣,才能稱之為真正的初始化
但是,如果一個成員變量既給了缺省值,又在初始化列表中顯式定義了,那它最后的值如何呢?
通過監視窗口我們可以觀察到:
如果一個成員變量既有缺省值又在初始化列表中定義了,那么就按照初始化列表中的值進行初始化
如果一個成員變量有缺省值,但是沒在初始化列表中定義,那么就用它的缺省值初始化
如果一個成員變量既沒有缺省值又沒在初始化列表中定義,那么就給一個隨機值
初始化列表有以下幾個特點:
(1)每個成員變量在初始化列表中只能出現一次,因為只能初始化一次
(2)類中的以下幾個成員變量,必須放在初始化列表中進行初始化
- 引用類型的成員變量
- const成員變量
- 沒有默認構造函數的自定義類型成員變量
原因如下:?
對于引用類型的成員變量,我們必須在聲明變量時就給出它的引用對象
對于const成員變量前面已經提到過了,也是必須在聲明時就初始化
對于沒有默認構造函數的自定義類型成員變量,我們在初始化時需要傳參
(3)盡量多使用初始化列表去初始化,因為不管你是否使用,對于自定義類型成員變量都一定會先使用初始化列表去初始化
例如:
可以看到,Date類里有一個Time類的成員函數,雖然我們沒有在初始化列表中初始化它,它也會使用初始化列表去調用自己的默認構造函數。
如果該自定義類型成員變量的構造函數不是無參的或者全缺省的,我們就需要手動將該變量添加至初始化列表中并給出參數。
總之,我們平時寫構造函數的時候盡量用初始化列表來初始化成員變量即可
(4)成員變量在類中的聲明順序就是它在初始化列表中的初始化順序,與其在初始化列表中的先后順序無關
例如:
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();
}
最后打印的結果是什么呢?
可能很多人認為是 1 1,實際上是:
因為_a2比_a1先聲明,所以在初始化列表中也是_a2先初始化
最后,拷貝構造函數因為也是構造函數,所以它也有初始化列表?
1.3 explicit關鍵字
我們知道,內置類型變量在發生類型轉換的時候會生成一個臨時的常性變量,例如:
int a = 1;
double b = a;
但是,類型轉換不止能發生在內置類型中,內置類型也可以轉換成自定義類型,這里就和構造函數扯上關系了。
一個類的構造函數,不僅起到初始化成員變量的作用,對于單個參數或第一個參數無缺省值的半缺省構造函數來說,它還具有類型轉換的作用。
或許沒看懂,那就舉一個例子
像這樣,對象aa1在創建時正常調用構造函數,aa2又是怎么回事?為什么一個自定義類型能被內置類型初始化?
前面說到,內置類型在發生類型轉換的時候會生成一個臨時的常性變量,這里也是一樣
首先編譯器使用1作為參數調用構造函數,創建一個臨時變量,再用這個臨時變量調用拷貝構造函數對aa2賦值
所以aa1只調用了一次構造函數,而aa2這一行代碼調用了一次構造函數和一次拷貝構造函數
這種方式既影響代碼可讀性,又增加了消耗,有什么辦法可以禁止構造函數類型轉換呢?
這里引入explicit關鍵字,在構造函數的前面加上它,即可禁止類型轉換了
這是單參數的構造函數,對于第一個參數無缺省值的半缺省構造函數也是同理
只要是只傳遞一個參數的構造函數,用這種方式都會發生類型轉換
另外,對于需要傳遞多個參數的構造函數,在C++11后也開始支持類型轉換了,例如:
如果不想類型轉換,用explicit修飾構造函數即可
二、static成員
2.1 概念
static修飾的類成員稱為類的靜態成員,static修飾的成員變量稱為靜態成員變量,static修飾的成員函數稱為靜態成員函數。
有一道面試題:實現一個類,計算程序中創建過多少個類對象
可以看到,使用靜態成員變量和靜態成員函數可以很輕松的解決這個問題
2.2 特性
根據上面的圖,我們可以得出以下幾點?
(1)靜態成員不屬于某個對象,而是屬于所有對象、屬于整個類,存放在靜態區
所以我們上面可以直接使用類名和作用域限定符來訪問靜態成員函數GetCount,當然也可以創建一個對象來訪問靜態成員函數,但是有點多此一舉了
(2)靜態成員變量必須在類外進行定義和初始化,不需要添加static,在類中聲明時才需要加
(3)靜態成員函數沒有隱式的this指針形參,所以不能訪問任何非靜態成員
(4)靜態成員也是類的成員,受public、protected和private訪問限定符的限制
像上面,因為_count是私有的,我們只能用函數來獲取它,如果它是公有的,我們也可以直接訪問
三、友元
當我們在類外定義了一個函數,想要訪問類中的私有成員變量怎么辦呢?這里就涉及到友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多用。
友元又分為:友元函數和友元類
3.1 友元函數
我們知道,cout和流插入運算符可以實現將內置類型打印的效果,那么假設我想將流插入運算符重載,讓自定義類型也可以使用呢?
我們試試在類中實現流插入運算符的重載函數
可以看到,實現的重載函數沒有起效
這是因為,成員函數的第一個變量是this指針,在重載函數中對應第一個變量的是第一個操作數
所以如果像這樣就可以正常運行了
但是這樣也太怪了吧,和平時用cout一點也不一樣,有沒有別的辦法呢?
我們只好在類外去實現流插入運算符的重載函數了,但是類外的函數又沒辦法訪問類內的私有成員
像這種必須定義在類外,但是又需要訪問類內的私有成員的函數,就需要友元來解決了
友元函數可以直接訪問類內的私有成員,需要在類的內部進行聲明,聲明時需要加friend關鍵字
需要說明以下幾點:
- 雖然友元函數可以訪問類的私有和保護成員,但不是類的成員函數
- 友元函數不能用const修飾
- 友元函數可以在類的任何地方聲明,不受類訪問限定符限制
- 一個函數可以同時是多個類的友元函數
- 友元函數的調用和普通函數一樣
3.2 友元類
和友元函數類似,我們也可以在類中聲明一個友元類
友元類的所有成員函數都是另一個類的友元函數,都可以訪問另一個類中的非公有成員
需要注意以下幾點:
- 友元關系是單向的,不具有交換性
比如上面,Date類是Time類的友元,所以可以直接在Date類中訪問Time類的私有成員變量;
但是不代表Time類是Date類的友元,不能在Time類中訪問Date類的私有成員變量
- 友元關系不能傳遞
例如A是B的友元,B是C的友元,不代表A就是C的友元了
- 友元關系不能繼承
這里在講到繼承后再給大家詳細介紹
四、內部類
4.1 概念
如果一個類定義在另一個類的內部,這個定義在內部的類就稱為內部類。
內部類是一個獨立的類,它不屬于外部類,我們更不能通過外部類的對象去訪問內部類的成員
外部類對內部類沒有任何優越的訪問權限。
內部類與外部類的關聯在于:
(1)內部類受外部類的類域限制
例如我們想創建一個內部類類型的變量,需要用作用域限定符
(2)內部類天生就是外部類的友元,但是外部類不是內部類的友元
4.2 特性
(1)內部類定義在外部類的public、protected和private中都是可以的
(2)內部類可以直接訪問外部類中的靜態成員,而不需要借助外部類的對象或類名
(3)外部類的大小不包括內部類
例如
可以看到,外部類A的大小并沒有包括內部類B,所以可以知道內部類的空間也是獨立的
五、匿名對象
有時候我們可能只需要調用一次某個類的成員函數,為此如果特意去創建一個對象的話就太麻煩了
這里就可以用到匿名對象。我們平時創建一個對象可能是這樣的:
如果要創建一個匿名對象的話,是這樣的:
顧名思義,匿名對象在創建的時候是不用取名字的。
匿名對象的特點在于,它的生命周期只在這一行,一旦程序走到了下一行,就會自動調用析構函數銷毀。
假設此時我們要調用一次A類中的Print函數,就可以用匿名對象去調用,而不用特意創建一個對象
對于各種一次性的對象創建,我們都可以使用匿名對象。
完.