【本節目標】
1. 再談構造函數
2. Static成員
3. 友元
4. 內部類
5. 再次理解封裝
1. 再談構造函數?
1.1 構造函數體賦值
在創建對象時,編譯器通過調用構造函數,給對象中各個成員變量一個合適的初始值。
#include <iostream>
using namespace std;
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 初始化列表
初始化列表:以一個冒號開始,接著是一個以逗號分隔的數據成員列表,每個"成員變量"后面跟一個放在括 號中的初始值或表達式。
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成員變量
自定義類型成員(且該類沒有默認構造函數時)
class A
{
public:A(int a):_a(a){}
private:int _a;
};
class B
{
public:
//初始化列表,對象成員定義的位置B(int a, int& ref):_aobj(a), _ref(ref), _n(10){}
private:A _aobj;// 沒有默認構造函數int& _ref;// 引用const int _n; // const
};
int main()
{int x = 1;//對象整體定義B bb(10,x);return 0;
}
為什么const 類型與引用必須在列表中初始化,因為它們有一個共同特征,必須在定義的時候初始化。而我們知道類里寫成員變量的地方,寫的是成員變量的聲明并非定義。因此成員變量只能在列表處初始化,在函數體類的是賦值并非初始化。因為初始化只能初始化一次,而構造函數體 內可以多次賦值。
這里再回述一下默認構造函數,什么叫默認構造函數呢?不傳參的都叫默認構造函數。例如編譯器自己生成的,無參的,全缺省的。因此如果自定義類型沒有默認構造函數時我們需要像上面一樣,顯示的調用自定義類型的構造函數。(也就是手動傳參)
當然有默認構造函數時我們也能在初始化列表上面寫,只不過如果在初始化列表上面寫了,就不用缺省值了。例如下面這樣,這里用的是10并非1。(但不能多次寫,因為 每個成員變量在初始化列表中最多只能出現一次)
class A
{
public:A(int a=1):_a(a){}
private:int _a;
};
class B
{
public:
//初始化列表,對象成員定義的位置B(int a, int& ref):_aobj(a), _ref(ref), _n(10){}
private:A _aobj;// 沒有默認構造函數int& _ref;// 引用const int _n; // const
};
int main()
{int x = 1;//對象整體定義B bb(10,x);return 0;
}
3. 盡量使用初始化列表初始化,因為不管你是否使用初始化列表,對于自定義類型成員變量,一定會先使 用初始化列表初始化。
class Time
{
public:Time(int hour = 0):_hour(hour){cout << "Time()" << endl;}
private:int _hour;
};class Date
{
public:Date(int day){}private:int _day;Time _t;
};int main()
{Date d(1);
}
注意:1.初始化列表和函數體賦值,一般是可以相互配合著使用的。
? ? ? ? ?2.就算初始化列表上什么都沒寫成員變量也會走初始化列表,因為這是它定義的地方。
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();
}//A.輸出1 1
//B.程序崩潰
//C.編譯不通過
//D.輸出1 隨機值
答案為D,因為編譯器是按照聲明的順序初始化的,也就是這里先初始化_a2,再初始化_a1。因此在初始化列表里,用_a1來初始化_a2是錯誤的,_a1是隨機值初始化了_a2。
因此為了防止這種錯誤,我們寫代碼時,盡量聲明與定義相同順序。
1.3 explicit關鍵字
構造函數不僅可以構造與初始化對象,對于接收單個參數的構造函數,還具有類型轉換的作用。接收單個參 數的構造函數具體表現:
1. 構造函數只有一個參數
2. 構造函數有多個參數,除第一個參數沒有默認值外,其余參數都有默認值
3. 全缺省構造函數
class Date
{
public:// 1. 單參構造函數,沒有使用explicit修飾,具有類型轉換作用// explicit修飾構造函數,禁止類型轉換---explicit去掉之后,代碼可以通過編譯Date(int year):_year(year){}/*// 2. 雖然有多個參數,但是創建對象時后兩個參數可以不傳遞,沒有使用explicit修飾,具有類型轉換作用// explicit修飾構造函數,禁止類型轉換explicit Date(int year, int month = 1, int day = 1): _year(year), _month(month), _day(day){}*/Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2022);// 用一個整形變量給日期類型對象賦值// 實際編譯器背后會用2023構造一個無名對象,最后用無名對象給d1對象進行賦值d1 = 2023;//這是一個隱式類型轉換,整形轉換成自定義類型// 將1屏蔽掉,2放開時則編譯失敗,因為explicit修飾構造函數,禁止了單參構造函數類型轉換的作用
}
int main()
{Test();return 0;
}
上述代碼可讀性不是很好,用explicit修飾構造函數,將會禁止構造函數的隱式轉換。
這里的隱式類型轉換與不同類型的賦值十分相像,都是創建一個為左操作數的類型臨時變量(這里是用右操作數的值來創建的),再把臨時變量拷貝構造給左操作數。這里隱式類型轉換也是一樣的,2023這個值創建一個為Date類型的臨時變量,再把臨時變量拷貝構造給d1。
void Test()
{Date d1(2022);d1 = 2023;//這是一個隱式類型轉換,整形轉換成自定義類型}
現在的編譯器一般都會優化這個過程,例如用2023直接構造一個日期類對象。
class Date
{
public:Date(int year):_year(year){cout << "Date(int year)" << endl;}Date(const Date& yy):_year(yy._year){cout << "Date(const Date & yy)" << endl;}Date& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1 = 2023;//這是一個隱式類型轉換,整形轉換成自定義類型}
int main()
{Test();return 0;
}
在同一個表達式上編譯器一般都會優化連續構造。?
2. static成員
2.1 概念
聲明為static的類成員稱為類的靜態成員,用static修飾的成員變量,稱之為靜態成員變量;用static修飾的 成員函數,稱之為靜態成員函數。靜態成員變量一定要在類外進行初始化
面試題:實現一個類,計算程序中創建出了多少個類對象。
class A{public:A() { ++_scount; }A(const A& t) { ++_scount; }~A() { --_scount; }static int GetACount() { return _scount; }private:static int _scount;};int A::_scount = 0;void TestA(){cout << A::GetACount() << endl;A a1, a2;A a3(a1);cout << A::GetACount() << endl;}int main(){TestA();return 0;}
2.2 特性
1. 靜態成員為所有類對象所共享,不屬于某個具體的對象,存放在靜態區
2. 靜態成員變量必須在類外定義,定義時不添加static關鍵字,類中只是聲明
3. 類靜態成員即可用 類名::靜態成員 或者 對象.靜態成員 來訪問
4. 靜態成員函數沒有隱藏的this指針,不能訪問任何非靜態成員
5. 靜態成員也是類的成員,受public、protected、private 訪問限定符的限制
【問題】
1. 靜態成員函數可以調用非靜態成員函數嗎?
2. 非靜態成員函數可以調用類的靜態成員函數嗎?
3. 友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多 用。
友元分為:友元函數和友元類
3.1 友元函數
問題:現在嘗試去重載operator<<,,然后發現沒辦法將operator<<重載成成員函數。因為cout的輸出流對象和隱含的this指針在搶占第一個參數的位置。this指針默認是第一個參數也就是左操作數了。但是實際使用中cout需要是第一個形參對象,才能正常使用。所以要將operator<<重載成全局函數。但又會導致類外沒辦法訪問成員,此時就需要友元來解決。operator>>同理。
#include <iostream>
using namespace std;
class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){
}// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常規調用// 因為成員函數第一個參數一定是隱藏的this,所以d1必須放在<<的左側ostream & operator<<(ostream & _cout){_cout << _year << "-" << _month << "-" << _day << endl;return _cout;}private:int _year;int _month;int _day;
};
友元函數可以直接訪問類的私有成員,它是定義在類外部的普通函數,不屬于任何類,但需要在類的內部聲 明,聲明時需要加friend關鍵字。
說明:
友元函數可訪問類的私有和保護成員,但不是類的成員函數
友元函數不能用const修飾
友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制
一個函數可以是多個類的友元函數
友元函數的調用與普通函數的調用原理相同
3.2 友元類
友元類的所有成員函數都可以是另一個類的友元函數,都可以訪問另一個類中的非公有成員。
?友元關系是單向的,不具有交換性。
比如上述Time類和Date類,在Time類中聲明Date類為其友元類,那么可以在Date類中直接訪問Time 類的私有成員變量,但想在Time類中訪問Date類中私有的成員變量則不行。
友元關系不能傳遞
如果B是A的友元,C是B的友元,則不能說明C時A的友元。
友元關系不能繼承,在繼承位置再給大家詳細介紹。
class Time
{
friend class Date;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;
};
4. 內部類
概念:如果一個類定義在另一個類的內部,這個內部類就叫做內部類。內部類是一個獨立的類,它不屬于外 部類,更不能通過外部類的對象去訪問內部類的成員。外部類對內部類沒有任何優越的訪問權限。
注意:內部類就是外部類的友元類,參見友元類的定義,內部類可以通過外部類的對象參數來訪問外部類中 的所有成員。但是外部類不是內部類的友元。
特性:
1. 內部類可以定義在外部類的public、protected、private都是可以的。
2. 注意內部類可以直接訪問外部類中的static成員,不需要外部類的對象/類名。
3. sizeof(外部類)=外部類,和內部類沒有任何關系。
class A
{
private:static int k;int h;
public:class B // B天生就是A的友元{public:void foo(const A& a){cout << k << endl;//OKcout << a.h << endl;//OK}};};
int A::k = 1;int main()
{A::B b;b.foo(A());return 0;
}
如果sizeof(A)就可以發現A類只占四個字節,究其原因是k是在靜態區不算在對象類,由于B類沒有定義對象,因此雖然看起來在A類里,其實并不占用A類空間。因此只算h的大小。可以回顧之前所學的B類在沒創建對象的時候只是起到一個圖紙的效果。但是要訪問B類就需要突破A類的限制,因此要用域作用限定符。如果還用了訪問限定符,設為了私有那么還要突破訪問限定符才能訪問。
5. 再次理解類和對象
現實生活中的實體計算機并不認識,計算機只認識二進制格式的數據。如果想要讓計算機認識現實生活中的 實體,用戶必須通過某種面向對象的語言,對實體進行描述,然后通過編寫程序,創建對象后計算機才可以 認識。比如想要讓計算機認識洗衣機,就需要:
1. 用戶先要對現實中洗衣機實體進行抽象---即在人為思想層面對洗衣機進行認識,洗衣機有什么屬性,有 那些功能,即對洗衣機進行抽象認知的一個過程
2. 經過1之后,在人的頭腦中已經對洗衣機有了一個清醒的認識,只不過此時計算機還不清楚,想要讓計 算機識別人想象中的洗衣機,就需要人通過某種面相對象的語言(比如:C++、Java、Python等)將洗衣 機用類來進行描述,并輸入到計算機中
3. 經過2之后,在計算機中就有了一個洗衣機類,但是洗衣機類只是站在計算機的角度對洗衣機對象進行 描述的,通過洗衣機類,可以實例化出一個個具體的洗衣機對象,此時計算機才能洗衣機是什么東西。
4. 用戶就可以借助計算機中洗衣機對象,來模擬現實中的洗衣機實體了。 在類和對象階段,大家一定要體會到,類是對某一類實體(對象)來進行描述的,描述該對象具有那些屬性, 那些方法,描述完成后就形成了一種新的自定義類型,才用該自定義類型就可以實例化具體的對象。