【本節目標】
- 再談構造函數
- Static成員
- 友元
- 內部類
- 匿名對象
- 拷貝對象時的一些編譯器優化
- 再次理解封裝
1. 再談構造函數
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 成員變量、引用成員變量,必須使用初始化列表進行初始化。而且對于自定義類型的成員變量,使用初始化列表可以避免一次默認構造和一次賦值操作,提高效率。
構造函數的函數體:
構造函數的函數體是在初始化列表之后執行的代碼塊,用于對成員變量進行賦值操作。
特點:
- 賦值時機: 構造函數的函數體是在成員變量已經完成初始化(默認初始化或使用初始化列表初始化)之后執行的,因此是一種先創建后賦值的操作。
- 適用場景: 適用于需要在構造對象時進行一些額外的計算或邏輯處理,然后再對成員變量進行賦值的情況。
注:拷貝構造同樣也有初始化列表
拓展_1:
在同時有缺省參數和傳遞構造函數參數的時候,會優先使用傳遞的參數進行初始化。
拓展_2:
注:單參數構造函數的隱式類型轉換
A a = i ;
老版本:首先對i進行調用構造函數創建一個臨時的類對象,再將這個對象淺拷貝給類對象a的一個過程。
新版本:編譯器進行優化,對a進行直接初始化。
拓展_3:
代碼解釋:
-
臨時對象的創建: 當執行 const A& ref = 5; 時,常量數字 5 作為參數調用 A 的構造函數 A(int i),創建了一個 A 類型的臨時對象。這個臨時對象是右值,因為它沒有一個具體的、持久的名稱,并且在表達式結束后原本會被銷毀。
-
常量左值引用綁定臨時對象: const A& ref 是一個常量左值引用,它可以綁定到這個臨時對象上,并且將臨時對象的生命周期延長到和 ref 的生命周期一致。所以,在 ref 的作用域內,我們可以通過 ref 安全地訪問臨時對象的成員函數,如 ref.print()。
拓展_4:
在單參數的基礎上,多參數的傳參加上了花括號進行區分。
拓展_5:
拓展_6:
在 C++ 中,成員函數調用時:
同類對象調用: 若形參為 const A&,因同類不同對象可互訪成員,仍能訪問該對象成員。
不同類對象調用: 當形參是 const A&,被調用的 A 類成員函數需為 const 成員函數,以確保調用時權限一致,避免修改 const 對象狀態。
易混淆重點:
- 不同類調用時,若以 const 引用接收對象,調用其成員函數,無論有無返回值,都需該函數為 const 成員函數以確保權限一致。
注:const對象只能調用const函數成員
案例演示:
拓展_7:
產生后果:
注:頭文件中兩個不同類如果后續在聲明中需要調用,則需要注意先后順序。
初始化列表和函數體內賦值混合使用場景:
【注意】
-
每個成員變量在初始化列表中只能出現一次(初始化只能初始化一次)
-
類中包含以下成員,必須放在初始化列表位置進行初始化:
- const 成員變量: const 成員變量在初始化后不能再被賦值,因此必須在初始化列表中進行初始化。
- 引用成員變量: 引用必須在定義時進行初始化,因此也必須在初始化列表中進行初始化。
- 沒有默認構造函數的類類型成員變量: 如果類的成員變量是一個沒有默認構造函數的類類型,那么必須在初始化列表中顯式調用該類的構造函數進行初始化。
-
盡量使用初始化列表初始化,因為不管你是否使用初始化列表,對于自定義類型成員變量,
一定會先使用初始化列表初始化。 -
成員變量在類中聲明次序就是其在初始化列表中的初始化順序,與其在初始化列表中的先后次序無關
課堂練習:
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 隨機值 --> 正確
1.3 explicit關鍵字
構造函數不僅可以構造與初始化對象,對于單個參數或者除第一個參數無默認值其余均有默認值
的構造函數,還具有類型轉換的作用。
class Date
{
public:// 1. 單參構造函數,沒有使用explicit修飾,具有類型轉換作用// 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修飾構造函數,禁止了單參構造函數類型轉換的作用
}
用explicit修飾構造函數,將會禁止構造函數的隱式轉換。
2. static成員
2.1 概念
聲明為static的類成員稱為類的靜態成員,用static修飾的成員變量,稱之為靜態成員變量;用static修飾的成員函數,稱之為靜態成員函數。靜態成員變量一定要在類外進行初始化。
面試題:實現一個類,計算程序中創建出了多少個類對象。
注:如果聲明和定義分開,則靜態成員需要在定義中聲明。
輸出結果:
注:靜態函數才能去調用靜態成員
通過各種形式訪問public
靜態成員:
靜態成員核心特性:
- 類級作用域: 屬于類本身而非實例,所有對象共享唯一副本
- 內存獨立: 存儲于靜態存儲區(全局 / 靜態區),生命周期與程序同步
訪問方式:
-
- 推薦: 類名::成員名(編譯期綁定)
-
- 允許: 對象名.成員名 或 指針->成員名(即使指針為空)
-
- 原理: 不依賴對象實例,通過類元信息直接尋址
注: 類外面不可以訪問private權限成員。
示例:
class MyClass {
public:static int s_val; // 聲明static void s_func() { /* ... */ }
};int MyClass::s_val = 0; // 必須類外初始化int main() {MyClass* ptr = nullptr;ptr->s_val = 42; // 合法,空指針訪問靜態成員MyClass::s_func(); // 更安全的訪問方式
}
問:當我們將靜態成員放入private權限
時,該通過什么辦法拿到靜態成員數據
我們可以通過靜態成員函數,在靜態成員函數內調用靜態成員變量,這樣我們就可以通過類在外面訪問靜態成員函數拿到靜態成員變量的數據。
示例:
畫圖詳解:
拓展_1:
靜態成員函數中不能訪問非靜態成員變量
(只允許訪問靜態成員變量)
,因為靜態成員函數沒有this指針,所以無法指向對象類的私有成員。
拓展_2:
當我們不能使用關鍵字的情況下如何實現:1+2+…+n?
示例:
通過創建裝有n個元素的Sum自定義類型數組,就可以調用n次構造函數,實現累加效果。
2.2 特性
- 靜態成員為所有類對象所共享,不屬于某個具體的對象,存放在靜態區
- 靜態成員變量必須在類外定義,定義時不添加static關鍵字,類中只是聲明
- 類靜態成員即可用 類名::靜態成員 或者 對象.靜態成員 來訪問
- 靜態成員函數沒有隱藏的this指針,不能訪問任何非靜態成員
- 靜態成員也是類的成員,受public、protected、private 訪問限定符的限制
3. 友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多用。
友元分為:友元函數和友元類。
3.1 友元函數
問題:現在嘗試去重載operator<<,然后發現沒辦法將operator<<重載成成員函數。因為cout的輸出流對象和隱含的this指針在搶占第一個參數的位置。 this指針默認是第一個參數也就是左操作數了。但是實際使用中cout需要是第一個形參對象,才能正常使用。所以要將operator<<重載成全局函數。但又會導致類外沒辦法訪問成員,此時就需要友元來解決。operator>>同理。
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關鍵字。
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;
}
注:
接收參數需要嚴格符合類型對齊要求
比如:cout << d1 --> 接收形參:ostream& _cout, const Date& d。
【注意】
- 友元函數可訪問類的私有和保護成員,但不是類的成員函數
- 友元函數不能用const修飾
- 友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制
- 一個函數可以是多個類的友元函數
- 友元函數的調用與普通函數的調用原理相同
友元函數對比普通函數核心區別在于友元函數接受的指定類可以訪問private權限成員。
3.2 友元類
友元類的所有成員函數都可以是另一個類的友元函數,都可以訪問另一個類中的非公有成員。
-
友元關系是單向的,不具有交換性。
比如上述Time類和Date類,在Time類中聲明Date類為其友元類,那么可以在Date類中直接
訪問Time類的私有成員變量,但想在Time類中訪問Date類中私有的成員變量則不行。 -
友元關系不能傳遞
如果C是B的友元, B是A的友元,則不能說明C時A的友元。 -
友元關系不能繼承,在繼承位置再給大家詳細介紹。
class Time
{friend class Date;// 聲明日期類為時間類的友元類,則在日期類中就直接訪問Time類中的私有成員變量
public:Time(int hour = 0, int minute = 0, int second = 0): _hour(hour), _minute(minute), _second(second){}void Print_time(){cout << _hour << "/" << _minute << "/" << _second << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
public:Date(int year = 2025, int month = 2, int day = 28):_year(year), _month(month), _day(day), _t(21, 29, 10){}void Print(){_t.Print_time();cout << _year << "/" << _month << "/" << _day << " - " <<_t._hour << "/" << _t._minute << "/" << _t._second << endl;}
private:int _year;int _month;int _day;Time _t;
};int main()
{Date d;d.Print();return 0;
}
【注意】
友元類和普通類的區別:
訪問權限:
- 普通類: 只能訪問其他類的
public
成員,遵循封裝原則,保證數據安全。 - 友元類: 可訪問授權類的所有成員,包括
private
和protected
成員,打破封裝限制。
關系性質:
- 普通類: 類間相對獨立,通過公共接口交互。
- 友元類: 與授權類存在特殊信任關系,關系單向,破壞一定封裝性。
代碼維護:
- 普通類: 封裝性好,修改內部實現通常不影響其他類,可維護性高。
- 友元類: 授權類成員變化可能影響友元類,耦合度高,維護較復雜。
4. 內部類
概念:如果一個類定義在另一個類的內部,這個內部類就叫做內部類。 內部類是一個獨立的類,
它不屬于外部類,更不能通過外部類的對象去訪問內部類的成員。外部類對內部類沒有任何優越
的訪問權限。
注意:內部類就是外部類的友元類,參見友元類的定義,內部類可以通過外部類的對象參數來訪
問外部類中的所有成員。但是外部類不是內部類的友元。
特性:
- 內部類可以定義在外部類的public、protected、private都是可以的。
- 注意內部類可以直接訪問外部類中的static成員,不需要外部類的對象/類名。
- sizeof(外部類)=外部類,和內部類沒有任何關系。
計算類大小:
輸出結果:
注: B類受A類域和訪問限定符的限制,其實他們是兩個獨立的類。
如何定義B類:
當我們的內部類處于private
權限時,我們通過以下辦法定義B類:
通過內部類概念實現:1+2+…n的計算:
輸出結果:
4.1 內部類在 public 權限下的核心特性
- 可見性與創建
- 內部類在
public
權限下對外部完全可見,可通過Outer::Inner
直接創建對象。 - 外部類成員函數可直接使用內部類名創建對象(無需類作用域限定符)。
示例:
- 成員訪問規則
- 內部類→外部類
-
- 內部類可直接訪問外部類的
public
/protected
/private
成員(嵌套作用域特權)。
- 內部類可直接訪問外部類的
注:前提是內部類成員函數接收外部類類型。
-
- 訪問非靜態成員需通過外部類對象 / 引用 / 指針傳遞。
-
外部類→內部類
-
- 外部類成員函數內通過實例化內部類對象可自由訪問內部類的
public
成員,若內部類成員為private
,需通過friend
聲明授權。
- 外部類成員函數內通過實例化內部類對象可自由訪問內部類的
- 參數傳遞的注意事項
-
內部類成員函數若需調用外部類的非
const
成員,形參必須為非const
引用 / 指針(如Outer&
)。 -
若僅需調用
const
成員,可使用const Outer&
,但此時無法修改外部類對象。
- 靜態成員的訪問
- 內部類的靜態成員可通過Outer::Inner::StaticMember直接訪問,無需創建對象。
注:如果靜態成員為private權限,則受類規則限制,我們不能直接進行訪問,我們可以通過定義靜態成員函數返回靜態成員變量。
4.2 內部類在 private 權限下的核心特性
示例:
- 可見性限制
-
外部不可見: 內部類的可見性是由
public或者private權限
決定的,所以private權限的內部類無法被外部直接訪問或實例化。 -
封裝性: 內部類的實現細節完全隱藏,外部代碼只能通過外部類的公共接口間接使用內部類。
- 訪問關系(容易混淆)
-
外部類對于內部類的訪問依舊受制于訪問權限,可以訪問內部類的public權限下的成員,private權限的成員變量則需要通過外部類調用內部類自身public權限下成員函數才能
間接訪問
。 -
內部類可以類比是外部類天然的友元類(
但外部類并不是內部類的友元
),可以直接調用外部類的各個權限下的成員變量或者成員函數。但本質上內部類能特殊訪問外部類是源自于它是外部類的成員類。 -
若內部類聲明為static,則無法直接訪問外部類的非靜態成員(需通過對象訪問)。
- 靜態成員的特殊規則
private以及public權限下的內部類
都可以訪問外部類的靜態成員變量或者函數,因為靜態成員變量以及函數是屬于類的并不是屬于對象,所以對于內部類來說都可以通過類名+作用域限定符號進行訪問。
注: 內部類也可以可直接訪問外部類靜態成員,如同訪問自身成員。并不一定需要顯示訪問。
- 不過,普通類不能直接通過 “類名 + 作用域限定符” 的方式去訪問另一個類中 private 權限下的靜態成員,仍然要
遵循類的訪問權限規則
。也就是說,只有類自身以及它的友元(友元類或者友元函數)才可以訪問這些 private 權限的靜態成員。
總結:
將內部類聲明為 private`是 C++ 中實現封裝的重要手段,它通過限制可見性和控制訪問權限,確保外部類的接口簡潔性,并將內部類的實現細節完全隱藏。這種設計尤其適用于工具類、輔助類或僅服務于外部類的底層邏輯。
-
同類對象
可以自由訪問彼此的私有成員(包括變量和函數),這是 C++ 封裝性的體現。 -
外部代碼
無法直接訪問私有成員
,必須通過類的 public 接口或友元機制。
對比:
從宏觀設計角度來看:將 ?private 內部類視為外部類的“子成員”?,而 ?public 內部類視為“獨立但關聯的組件”?。
5. 匿名對象
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl}~A(){cout << "~A()" << endl;}
private:int _a;
};class Solution {
public:int Sum_Solution(int n) {//...return n;}};int main(){A aa1;// 不能這么定義對象,因為編譯器無法識別下面是一個函數聲明,還是對象定義//A aa1();// 但是我們可以這么定義匿名對象,匿名對象的特點不用取名字,// 但是他的生命周期只有這一行,我們可以看到下一行他就會自動調用析構函數A();A aa2(2);// 匿名對象在這樣場景下就很好用,當然還有一些其他使用場景,這個我們以后遇到了再說Solution().Sum_Solution(10);return 0;}
6. 編譯器優化
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}A& operator=(const A& aa){cout << "A& operator=(const A& aa)" << endl;if (this != &aa){_a = aa._a;}return *this;}~A(){cout << "~A()" << endl;}
private:int _a;
};void f1(A aa)
{}A f2()
{A aa;return aa;
}int main()
{// 傳值傳參A aa1;f1(aa1);cout << endl;// 傳值返回f2();cout << endl;// 隱式類型,連續構造+拷貝構造->優化為直接構造f1(1);// 一個表達式中,連續構造+拷貝構造->優化為一個構造f1(A(2));cout << endl;// 一個表達式中,連續拷貝構造+拷貝構造->優化一個拷貝構造A aa2 = f2();cout << endl;// 一個表達式中,連續拷貝構造+賦值重載->無法優化aa1 = f2();cout << endl;return 0;
}
示例:
輸出結果:
在形參上我們使用了實參的拷貝類型,如果我們要使用引用類型的話就會出現問題。
如下報錯:
結論:由于2和3案例中實參是臨時變量,一個臨時的變量是無法使用引用類型的;如果強行使用引用類型,則需要使用通過const 類名
引用進行延長生命周期。
拓展_1:
通過匿名對象以及缺省參數的使用可以在調用函數的時候不傳入任何實參也能調用函數。
拓展_2:
拓展_3:
本章完~