1. 再談構造函數(構造函數的2個深入使用技巧)
1.1 構造函數體賦值
在創建對象時,編譯器通過調用構造函數,給對象中各個成員變量一個合適的初始值。
雖然上述構造函數調用之后,對象中已經有了一個初始值,但是不能將其稱為對對象中成員變量的初始化。
【注意】
- 構造函數體中的語句只能將其稱為賦初值,而不能稱作初始化。
- 因為初始化只能初始化一次,而構造函數體內可以多次賦值。
1.2?初始化列表
引入:棧類、兩個棧實現的隊列。
假設棧不提供默認構造,只有需要傳參的構造。
報錯:數據成員popst不具備默認構造。
當Stack不提供默認構造——即顯式寫了一個帶參的非全缺省構造。MyQueue也就無法使用默認構造。這個時候就需要在MyQueue里面顯式地寫構造函數。在這個構造里顯式調用自定義類型的非默認構造。
問:這個構造函數應該怎么寫???自定義類型的調用構造函數怎么傳參???
答:帶初始化列表的構造函數。
?初始化列表 :以一個冒號開始,接著是一個以逗號分隔的數據成員列表,每個"成員變量"后面跟一個放在括號中的初始值或表達式。
class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};
c++中引入初始化列表,規則:冒號開始,逗號分割,賦值用括號——哪怕是內置類型,也不用賦值符號=,用括號。
作用:自定義類型成員不具備默認構造,是帶參的構造,就可以在初始化列表里顯式地傳給帶參構造。
初始化列表本質可以理解為,對象中每個成員變量定義的地方——在main函數中定義一個對象(對象整體定義),程序走過這一句,會調用它的構造函數,構造函數的初始化列表就是成員定義的地方(對象內每個成員自己定義的地方)。
在本例中:
初始化列表建議一個成員變量一行,代碼更整潔,全寫到一行太擁擠。
有了顯式實現的構造函數,就不需要聲明缺省值了,而且引用不太好給缺省參數,那為了整齊,可以選擇都不用給聲明缺省值。
【初始化列表】
- 位置:在函數聲明和函數體之間;
- 規則:冒號開始,逗號分割,括號賦值;
- 規定:一個成員只能有一個,最多有多少個成員就寫多少個,所有的成員都可以在初始化列表進行初始化;
- 一般類型成員可以不用一定要在初始化列表進行初始化,也可以在函數體內初始化;
- 有三類成員必須在初始化列表進行初始化:
- ①引用類型的成員
- ②const成員
——這兩個都必須在定義的時候初始化,初始化列表就是成員定義的地方。
(函數體內只能賦值)- ③沒有默認構造的自定義類型成員,只能顯式地(傳參)調用構造,構造函數在對象定義的時候被“隱式/顯式”調用。
- 注意:
- const成員可以給缺省值,就不需要在初始化列表寫出來了。
- 引用可以給缺省值,就不需要在初始化列表寫出來了。
- const變量的特點:必須在定義時初始化,因為只有一次初始化的機會,而成員變量都是在構造函數的初始化列表被定義。
- 引用的特點:必須在定義時初始化(不能先定義一個別名,但是不知道是誰的別名)
這就需要找到每個成員變量定義的地方——因為有些成員變量要在定義的時候做一些處理。
C++祖師爺就找到初始化列表這么個地方。
【注意】
1. 每個成員變量在初始化列表中只能出現一次(初始化只能初始化一次)
2. 類中包含以下成員,必須放在初始化列表位置進行初始化:
- 引用類型的成員變量(必須初始化)
- const成員變量(必須初始化)
- 自定義類型成員(且該類沒有默認構造函數時)
——就之前學習的知識而言,有些情況我們還是無法完成初始化(因為真正的初始化在初始化列表)
3. 盡量使用初始化列表初始化——因為不管你是否使用初始化列表,對于自定義類型成員變量,一定會先使用初始化列表初始化。
初始化列表,不管你有沒有顯式地寫出來,每個成員變量都會先走一遍。
(沒寫就會自動生成初始化列表,自動走)
C++難學的一個原因就是編譯器幫你做了太多看不見的事。
調試觀察構造函數的執行流程:
編譯器自動生成的初始化列表,會
- 對自定義類型的成員:調用默認構造——也只能調用默認構造。
(沒有默認構造就編譯報錯)- 對內置類型的成員:有缺省值用缺省值,沒有的話,值就不確定了——要看編譯器,有的編譯器會處理,有的不會處理。
構造函數:先走初始化列表 + 再走函數體
實踐中盡可能使用初始化列表初始化——因為不管你是不是使用初始化列表初始化,都一定會先走一遍初始化列表。
初始化列表不方便時,再使用函數體完成初始化(賦值)。
4. 成員變量在類中聲明次序就是其在初始化列表中的初始化順序,與其在初始化列表中的先后次序無關。
調試觀察構造函數的執行流程:
class MyQueue
{
public:MyQueue():_size(1) //顯式地寫出來,就不會走缺省值了, _pushst(10 + 1) //顯式地寫出來,調用構造時就自主傳參(就不使用默認構造的缺省參數值), _ptr((int*)malloc(40))//初始化列表的括號里的初始化內容比較自由,就像初始化(賦值)的=的右邊一樣,符合類型對應就行{//初始化列表不方便調用memset把40個字節的空間都初始化成0,就可以在函數體內完成memset(_ptr, 0, 40);//memset(_ptr, 1, 40); _ptr觀察到16843009——0x 0101 0101}private:// 聲明Stack _pushst;Stack _popst;// C++11的補丁:缺省值——給初始化列表用的——初始化列表顯式寫了,就不會用了int _size = 0;//const成員可以給缺省值,就不需要在初始化列表寫出來了//引用可以給缺省值,就不需要在初始化列表寫出來了const int _x = 10;int& ref = _size;int* _ptr;
};int main()
{MyQueue q;return 0;
}
例題:
程序崩潰:訪問野指針、空指針、數組越界訪問、……
編譯不通過:語法錯誤。
- 初始化列表初始化的順序,是成員變量聲明的順序,不是初始化列表中成員變量出現的順序。
對象模型(對象在內存中是怎么存的):對象在內存中是按成員變量聲明的順序,進行存儲的;初始化的時候也是按照這個順序,依次往后進行初始化的。
可以調試-內存觀察對象在內存中的存儲。
aa的地址就是_a2的地址,往后4字節就是_a1的地址
建議:初始化列表中的出現順序盡量和聲明順序保持一致——避免上述例題中出現的問題。
1.3?explicit關鍵字
1.3.1?單參構造的類型轉換調用
1.3.1.1?概念
第3個是隱式類型轉換:內置類型轉換成自定義類型。
由構造函數可知,A類的對象支持僅使用一個int類型去構造。
正是因為類型轉換會產生臨時對象,3才能賦值給到A類型的對象。
還有就是因為A類型支持用一個int類型的參數去構造——單參數的構造支持的內置類型轉換成自定義類型。
圖解說明:
臨時對象具有常性:
報錯原因不是因為跨類型了。引用的是3構造的臨時對象。這里沒有拷貝構造。
- 編譯器遇到連續的“構造+拷貝構造”->會優化為直接構造。
構造函數不僅可以構造與初始化對象,對于接收單個參數的構造函數,還具有類型轉換的作用。
接收單個參數的構造函數具體表現:
- 構造函數只有一個參數。
- 構造函數有多個參數,除第一個參數沒有默認值外,其余參數都有默認值。
- 全缺省構造函數。
class A
{
public://單參數構造函數//explicit A(int a) 不允許隱式類型轉換A(int a):_a(a){cout << "A(int a)" << endl;}//多參數構造函數——也支持隱式類型轉換,即使用{ }A(int a1, int a2):_a(0),_a1(a1),_a2(a2){}//顯式寫拷貝構造A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;int _a1;int _a2;
};int main()
{// 隱式類型轉換// 內置類型轉換為自定義類型// A aa4 = 3.3;——3.3先轉為int(警告:double轉為int可能丟失數據)// A aa5 = 'a';——'a'先轉為int// A aa6 = "abc";——報錯:字符串不能隱式類型轉換// raa 引用的是類型轉換中用3構造的臨時對象 —— 所以raa不能直接引用3,即A& raa = 3;,而是需要加一個constconst A& raa = 3;//正常調用多參數的構造A aaa1(1, 2);//賦值調用多參數的構造//1.不能直接 A aaa2 = 1, 2;//2.也不能 A aaa2 = (1, 2);——這個通過了。//因為調用了單參數的構造——小括號內理解為逗號表達式,值為2//C++11支持A aaa2 = { 1, 2 };(C++98不支持)A aaa2 = { 1, 2 };//理解為先用{1, 2}去構造,再拷貝構造//這里也不能直接引用,要加上constconst A& aaa3 = { 1, 2 };//——C++11之后還允許:// A aaa2{ 1, 2 };// const A& aaa3 { 1, 2 };// 即把賦值 = 省略掉,但是不建議,因為這種用法四不像return 0;
}
1.3.1.2?應用
來看棧類型。
改進就是引用傳參,并且加上const,避免引用傳參導致的改變形參直接影響實參。
同時也只有加上const,才能接收用int構造的臨時對象。
單參構造的隱式類型轉換實用性很強:
來看缺省值這里的應用——缺省值的4種給法。
class BB
{
public:BB(){}private:// 聲明缺省值——給初始化列表使用// 缺省值的給法// 1.缺省值可以直接給值int _b1 = 1;// 2.缺省值可以mallacint* _ptr = (int*)malloc(40);// 3.缺省值可以隱式類型轉換Stack _pushst = 10;A _a1 = 1;A _a2 = { 1,2 };// 4.缺省值可以給成員變量、全局變量A _a3 = _a2;
};
//相當于初始化列表
//BB()
// :_b1(1)
// ,_ptr((int*)malloc(40));
// ,_pushst(10);
// ,_a1(1);
// ,_a2({ 1,2 });
// ,_a3(_a2);
// {}
int main()
{BB bb;return 0;
}
1.3.2??多參構造的類型轉換調用
1.3.2.1??概念
C++11支持多參構造的類型轉換調用。
class A
{
public://單參數構造函數//explicit A(int a) 不允許隱式類型轉換A(int a):_a(a){cout << "A(int a)" << endl;}//多參數構造函數——也支持隱式類型轉換,即使用{ }A(int a1, int a2):_a(0),_a1(a1),_a2(a2){}//顯式寫拷貝構造A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;int _a1;int _a2;
};int main()
{// 隱式類型轉換// 內置類型轉換為自定義類型// A aa4 = 3.3;——3.3先轉為int(警告:double轉為int可能丟失數據)// A aa5 = 'a';——'a'先轉為int// A aa6 = "abc";——報錯:字符串不能隱式類型轉換// raa 引用的是類型轉換中用3構造的臨時對象 —— 所以raa不能直接引用3,即A& raa = 3;,而是需要加一個constconst A& raa = 3;//正常調用多參數的構造A aaa1(1, 2);//賦值調用多參數的構造//1.不能直接 A aaa2 = 1, 2;//2.也不能 A aaa2 = (1, 2);——這個通過了。//因為調用了單參數的構造——小括號內理解為逗號表達式,值為2//C++11支持A aaa2 = { 1, 2 };(C++98不支持)A aaa2 = { 1, 2 };//理解為先用{1, 2}去構造,再拷貝構造//這里也不能直接引用,要加上constconst A& aaa3 = { 1, 2 };//——C++11之后還允許:// A aaa2{ 1, 2 };// const A& aaa3 { 1, 2 };// 即把賦值 = 省略掉,但是不建議,因為這種用法四不像return 0;
}
1.3.2.1??應用
范例:
class A
{
public://單參數構造函數//explicit A(int a)A(int a):_a(a){cout << "A(int a)" << endl;}//多參數構造函數——也支持隱式類型轉換,即使用{ }A(int a1, int a2):_a(0),_a1(a1),_a2(a2){}//顯式寫拷貝構造A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;int _a1;int _a2;
};class Stack
{
public:void Push(const A& aa){//...往棧里頭存A類型的數據——即存自定義類型到棧內}//...
};int main()
{//使用多參構造來初始化AA aa1(1, 2);st.Push(aa1);//插入多參構造的A類對象st.Push({ 1,2 }); //按F11不會直接進入push,而是會先去到A的多參構造return 0;
}
1.3.3??explicit關鍵字
C++關鍵字——explicit。
功能:修飾構造函數,禁止隱式類型轉換。
上述代碼可讀性不是很好,用explicit修飾構造函數,將會禁止構造函數的隱式轉換。
2.?static成員
2.1?概念
類的靜態成員:聲明為static的類成員稱為類的靜態成員。
靜態成員變量:用static修飾的成員變量,稱之為靜態成員變量;
靜態成員函數:用static修飾的成員函數,稱之為靜態成員函數。
- 靜態成員變量只能在類外進行初始化
修正過后,輸出結果:
靜態成員變量_scount:
- 在靜態區,不存在對象中——所以對象的大小是8,而不是12。
- 不能給缺省值,因為缺省值是給初始化列表,它在靜態區不在對象中,不走初始化列表。
初始化列表是對對象存儲區域的依次初始化,靜態成員變量不在這里。 - 屬于所有整個類,屬于所有對象。
類實例化的對象在棧上,類的靜態成員變量在靜態區。
對象當中只存成員變量,不存成員函數(公共代碼區)、不存靜態成員變量(靜態區)。
- 只能在類外定義,初始化也就只能寫在類外面(類似于函數聲明和定義分離)
(無論是public還是private,都只能在類外定義)
公有的靜態成員變量可以在全局訪問。
私有的靜態成員變量只能在類里面訪問,在類外可以通過公有的 靜態成員函數 訪問。
class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }// 靜態成員函數static int GetCount(){//_a1 = 1;不能訪問普通成員變量——依靠this指針訪問的:this->_a1return _scount;}~A() { --_scount; }
private:// 聲明int _a1 = 1;int _a2 = 1;
public:static int _scount;
};int A::_scount = 0;int main()
{A aa1;cout << sizeof(aa1) << endl;//aa1._scount++;//cout << A::_scount << endl;//private私有不支持類外訪問//就可以通過公有的靜態成員函數訪問//靜態成員函數的兩種訪問方式cout << A::GetCount() << endl;cout << aa1.GetCount() << endl;return 0;
}
static修飾成員變量和成員函數的意義完全不同:
- 修飾變量:影響生命周期。
- 修飾全局函數:影響鏈接屬性。
- 修飾成員函數:沒有this指針——意味著只能訪問靜態成員。
(公有)靜態成員函數(沒有this指針)的兩種訪問方式:
- 和普通的成員函數一樣,由對象調用。
- 原因1:要突破類域,去類里面找函數的出處;
- 原因2:需要傳遞調用這個成員函數的對象的this指針
- 指明類域,直接調用。
2.2?應用
【面試題】統計A類型的對象,一共創建了多少個。
對象不是構造出來的,就是拷貝構造(特殊的構造)出來的。
- 在構造時++,在拷貝構造時++,在析構時--,就可以通過這個靜態成員變量獲取到當前還有多少個A類型的對象還存活著,還在用。
- 去掉在析構時--,就能統計一共創建了多少個A類型的對象。
示例1——統計累積對象數目:
class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }// 靜態成員函數static int GetCount(){//_a1 = 1;不能訪問普通成員變量——依靠this指針訪問的:this->_a1return _scount;}~A() { //--_scount; }
private:// 聲明int _a1 = 1;int _a2 = 1;
public:static int _scount;
};int A::_scount = 0;A func()
{A aa4;return aa4;
}int main()
{A aa1;A aa2;A aa3(aa1);func();cout << A::GetCount() << endl;return 0;
}
看似是4,結果卻是5。
(VS2019下是5,VS2022下是4——返回值優化,沒有拷貝構造臨時對象)
實例2——統計現存對象數目:
class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }// 靜態成員函數static int GetCount(){//_a1 = 1;不能訪問普通成員變量——依靠this指針訪問的:this->_a1return _scount;}~A() { --_scount; }
private:// 聲明int _a1 = 1;int _a2 = 1;
public:static int _scount;
};int A::_scount = 0;A func()
{A aa4;return aa4;
}int main()
{A aa1;A aa2;A aa3(aa1);func();cout << A::GetCount() << endl;return 0;
}
結果:3。
現存對象只有3個,aa4創建之后_scount++,析構之后_scount--。
不過返回時是否拷貝構造臨時對象(返回值優化后,傳值返回不會拷貝構造臨時對象)都不會改變_scount的值。
2.3?特性
1. 靜態成員為所有類對象所共享,不屬于某個具體的對象,存放在靜態區。
2. 靜態成員變量只能在類外定義,定義時不添加static關鍵字,類中只是聲明
3. 類的靜態成員可用 類名::靜態成員 或者 對象.靜態成員 來訪問。
4. 靜態成員函數沒有隱藏的this指針,不能訪問任何非靜態成員。
5. 靜態成員也是類的成員,受public、protected、private 訪問限定符的限制。
【問題】
1. 非靜態成員函數可以訪問靜態成員變量嗎?(可以)
2. 靜態成員函數可以調用非靜態成員函數嗎?(不可以)(調用非靜態成員函數需要傳this指針)
3. 非靜態成員函數可以調用類的靜態成員函數嗎?(可以)
非靜態函數可以調用靜態函數,哪怕靜態函數定義在后面。
- 因為類是一個整體,會在整個類里面全搜索。
- 全局的只能從上到下地搜索。
3.?友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多用。
友元分為:友元函數、友元類。
3.1?友元函數
問題:現在嘗試去重載operator<<,然后發現沒辦法將operator<<重載成成員函數。
因為cout的輸出流對象和隱含的this指針在搶占第一個參數的位置。this指針默認是第一個參數也就是左操作數了。但是實際使用中cout需要是第一個形參對象,才能正常使用。
所以要將operator<<重載成全局函數。但又會導致類外沒辦法訪問成員,此時就需要友元來解決。
operator>>同理。
- 友元函數可以直接訪問類的私有成員,它是定義在類外部的普通函數,不屬于任何類,但需要在類的內部聲明,聲明時需要加friend關鍵字。
【說明】
- 友元函數可訪問類的私有和保護成員,但不是類的成員函數
- 友元函數括號后不能用const修飾(const修飾*this,友元函數不是類的成員函數,沒有this指針)
- 友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制
- 一個函數可以是多個類的友元函數
- 友元函數的調用與普通函數的調用原理相同
3.2?友元類
- 友元類的所有成員函數都可以是另一個類的友元函數,都可以訪問另一個類中的非公有成員。
- 友元關系是單向的,不具有交換性。(互為友元的情況是很少的)
比如Time類和Date類,在Time類中聲明Date類為其友元類,那么可以在Date類中直接訪問Time類的私有成員變量,但想在Time類中訪問Date類中私有的成員變量則不行。
- 友元關系不能傳遞
如果B是A的友元,C是B的友元,不能說明C是A的友元。
- 友元關系不能繼承,在繼承位置再給大家詳細介紹。
///////////////////////////////////////////////////////////////////////////////友元類
class Time
{// 聲明友元——可以聲明在類里面的任何位置,一般喜歡聲明在最上面// Date是Time的友元// Date中可以訪問Time的私有// 但是Time中不能訪問Date的私有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){_t._hour++;}//設置時分秒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(外部類)=外部類,和內部類沒有任何關系。
- C++的內部類和外部類的關系:是兩個獨立的類;
- 聯系1:訪問B類會受到A類的類域的限制,需要指明A類的類域。
- 聯系2:訪問B類也會受到訪問限定符的限制——即如果不想人從外面使用這個類,可以設置成私有private ——即成為A的專屬類,只有A類里面能用B類
- 意義:B天生就是A的友元(類)——B天生能訪問A中的成員變量。
A aa;
?// 默認構造,未初始化內置類型成員變量,a.x = a.y = 隨機值
A()
?// 創建一個?值初始化 的臨時對象,對于 沒有用戶定義構造函數 的類(如A
),值初始化會將其成員變量零初始化。
改正后的程序結果:
- 用途:C++不太愛使用內部類,Java喜歡使用內部類
5. 匿名對象
- 用 類型(實參)定義出來的對象叫做匿名對象。(沒有對象名)
- 相比之前我們定義的 類型 對象名(實參)定義出來的叫有名對象。
- 匿名對象生命周期只在當前一行,一般臨時定義一個對象當前用一下即可,就可以定義匿名對象。
最后的兩個析構是main函數結束了,有名對象生命周期到了,被銷毀。
有名對象、匿名對象的區別:
- 有名對象的生命周期:當前作用域。
- 匿名對象的生命周期:當前命令行。
匿名對象的特點:一個即用即銷毀的對象。
匿名對象的使用場景:
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(1);A aa2(2);// 匿名對象在這樣場景下就很好?,當然還有?些其他使用場景,這個我們以后遇到了再說Solution().Sum_Solution(10);return 0;
}
6. 拷貝對象時的一些編譯器的優化
這里是按照VS2019環境下的優化來介紹的,VS2022開的優化比2019更大,不太利于理解。
VS2022作了跨行優化(突破型的優化)。
- 現代編譯器會為了盡可能提高程序的效率,在不影響正確性的情況下會盡可能減少一些傳參和傳返回值的過程中可以省略的拷貝。
- 如何優化C++標準并沒有嚴格規定,各個編譯器會根據情況自行處理。當前主流的相對新一點的編譯器對于連續一個表達式步驟中的連續拷貝會進行合并優化,有些更新更"激進"的編譯器還會進行跨行跨表達式的合并優化。
6.1?場景①-傳值傳參
測試代碼:
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}A(int a1, int a2){cout << "A(int a1, int a2)" << 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)
{}int main()
{// 構造A aa1;// 傳值傳參——拷貝構造f1(aa1);cout << endl; //先是形參aa的析構(此時f1函數結束)——局部對象的生命周期是當前函數作用域//換行后的隔一行是aa1的析構(此時main函數結束)return 0;
}
執行結果:
引用傳參,可以減少一個拷貝構造、一個析構。
引用傳參的注意事項:建議加上const,避免改變形參直接影響到實參。
- 同時加上const還有一個好處:可以支持傳匿名對象、臨時對象。
- 匿名對象、臨時對象都具有常性。(使用完就析構)
傳匿名對象和傳單參構造的隱式類型轉換的臨時對象,在這里是等價的。
實踐當中一般也不會傳匿名對象,更多的時候還是直接傳單參構造的隱式類型轉換更方便。
以上的例子還沒有涉及到編譯器的優化。
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}A(int a1, int a2){cout << "A(int a1, int a2)" << 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)
{}int main()
{// 構造A aa1;cout << endl;// 傳值傳參——拷貝構造f1(aa1);cout << endl;// 下面3個都是:一個表達式中,涉及的連續的“構造+拷貝構造”,部分編譯器認為中間構造出來的臨時對象白白浪費了,會優化成一個構造,跳過中間對象,直接去構造目標對象// 匿名對象——構造+拷貝構造f1(A(1));cout << endl;// 臨時對象——構造+拷貝構造f1(2);cout << endl;// 隱式類型轉換——構造+拷貝構造A aa2 = 3; cout << endl;A aa3; aa3 = 3; //隱式類型轉換:構造+拷貝構造cout << endl;return 0;
}
執行結果:
這3個構造后面的拷貝構造都被優化掉了,理論上應該有,實踐中沒有。
【注意】
- 不優化也正常,是編譯器根據自身的開發環境自主實現的。
- 但是一般較新的編譯器最少都會進行這個優化,甚至一些編譯器優化力度非常大。
6.2?場景②-傳值返回
經驗規律:一個表達式中,連續的“拷貝構造 + 拷貝構造” -> 優化為一個拷貝構造。
代碼驗證:
引用的形式接收,傳值返回的值——這里只作演示,不作深入討論,不建議這樣做,引用的是臨時對象其內容不太確定。
引用的是拷貝構造的臨時對象,沒有了一行當中連續的“拷貝構造+拷貝構造”,所以本來引用接收就會少一個拷貝構造,這里的引用接收就是打破了編譯器的優化,這里就不存在編譯的優化了。
本來這個&(引用符號)應該放在返回值那里,把傳值返回,變成引用返回,才能減少拷貝。
再看一下VS2022的處理:
引用返回反而不夠優化。
傳值返回反而優化力度大:
再來看看不優化的情況:
f2結束之前,aa銷毀之前先拷貝構造出一個臨時對象用于傳值返回,再調aa的析構。
臨時對象用作賦值,臨時對象最后被析構。
分行寫,導致拷貝構造變成了賦值重載,編譯器的優化沒了。
看看VS2022(默認Debug)的效果:
注:VS2019的Release版本也開了同樣的跨行優化。
Release下把構造的對象aa直接用作臨時對象,給ret2賦值,減少一次拷貝,這樣aa的生命周期以及不在f2了——賦值之后才析構。
Release下,編譯器進行語法分析,發現f2函數內:構造aa,拷貝構造臨時對象,拷貝構造ret。
不如在f2內最初構造的直接就是ret,直接一開始就操控ret這塊空間進行構造。
(aa和ret合并了,aa相當于ret的別名,底層是用指針實現的)
aa在換行之后才析構,而不是f2結束直接就析構,說明aa就是ret1。
VS2019的Release下的本來的Debug下的“構造+拷貝構造+拷貝構造”優化的“構造+拷貝構造”,再被優化成一個構造(跨行優化后合三為一)
- ① 不優化:不能用aa返回,因為棧幀銷毀,aa就沒了,所以要創建一個臨時對象,這個臨時對象一般比較小,會用寄存器存儲,如果比較大,會在兩個棧幀中間開一塊空間存。
- ② VS2019的Dubug等級優化:“構造+拷貝構造+拷貝構造” ==> “構造+拷貝構造”
- ③ VS2019的Release等級優化:“構造+拷貝構造+拷貝構造” ==> “構造”
(最高等級的優化)
【結論】
- 場景①-傳值傳參:連續的“構造 + 拷貝構造 ——> 優化為一個構造。
- 場景②-傳值返回:連續的“拷貝構造 + 拷貝構造 ——> 優化為一個拷貝構造。
7. 再次理解類和對象
現實生活中的實體計算機并不認識,計算機只認識二進制格式的數據。如果想要讓計算機認識現實生活中的實體,用戶必須通過某種面向對象的語言,對實體進行描述,然后通過編寫程序,創建對象后計算機才可以認識。比如想要讓計算機認識洗衣機,就需要:
1. 用戶先要對現實中洗衣機實體進行抽象---即在人為思想層面對洗衣機進行認識,洗衣機有什么屬性,有那些功能,即對洗衣機進行抽象認知的一個過程
2. 經過1之后,在人的頭腦中已經對洗衣機有了一個清醒的認識,只不過此時計算機還不清楚,想要讓計算機識別人想象中的洗衣機,就需要人通過某種面相對象的語言(比如:C++、Java、Python等)將洗衣機用類來進行描述,并輸入到計算機中
3. 經過2之后,在計算機中就有了一個洗衣機類,但是洗衣機類只是站在計算機的角度對洗衣機對象進行描述的,通過洗衣機類,可以實例化出一個個具體的洗衣機對象,此時計算機才能洗衣機是什么東西。
4. 用戶就可以借助計算機中洗衣機對象,來模擬現實中的洗衣機實體了。
在類和對象階段,大家一定要體會到,類是對某一類實體(對象)來進行描述的,描述該對象具有那些屬性,那些方法,描述完成后就形成了一種新的自定義類型,才用該自定義類型就可以實例化具體的對象。
C++的面向對象是在描繪這個世界,類就是對應現實世界的抽象類型,對象就是對應各個現實實體。
例——外賣系統: 三大類:騎手、商家、用戶。
每個類要設計哪些數據,取決于類本身,哪些數據是它最關注的核心。
例:
騎手:姓名、電話、當前位置、……
商家:坐標位置、菜品(種類、價格)、……
用戶:電話、住址、……
這重要的三個類別,實例化出n多個對象,對象和對象之間建立出關聯、關系,每個現實中的實體,都對應外賣平臺上的一個類別和對象。
8.?練習題
8.1?自然序列求和
8.1.1?一般解法:static靜態成員
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等關鍵字及條件判斷語句(A?B:C)_
OJ鏈接
把常見的方式全部限制了——有學習意義,沒有實用意義
- 循環(for、while)
- 遞歸(需要返回條件:if、else、三目)
- 公式(乘除法)
不論是循環還是遞歸,都是想走n次,將這n次的n個自然序列數字求和。
問:循環和遞歸禁用后,怎么走n次呢???
答:調用n次構造函數。——如何實現???
1. 定義一個數組,大小為n,每個元素是Sum對象,實現調用n次Sum構造
2. 在每次調用的構造里面,對靜態變量進行操作 ——為了保留操作結果,得到遞增的自然序列的每個數字。
3. 定義兩個私有的靜態變量。
4. 在構造函數中操作這兩個靜態變量:ret?+= i,i++。
5. 為了獲取求和結果ret——私有靜態變量,可以選擇定義一個公有靜態函數。
class Sum{
public:sum(){_ret += _i;++_i;}static int GetRet(){return _ret;}private:static int _i;static int _ret;
};int sum:: _i = 1;
int sum:: _ret = 0;class solution {
public:int Sum_solution(int n) {//變長數組Sum arr[n];return sum::GetRet();}
};
C99的變長數組,VS不支持 (VS只能new一個數組) OJ編譯器都比較新,比較全面,支持絕大多數的C++規則,所以一些在VS上跑不過的代碼,在OJ題上能跑過。
牛客OJ應該是用的g++(linux)
8.1.2?優化解法:內部類
改造成這個樣子:使得Sum只能用于Solution內部。
因為Sum是專門寫來解決Solution里面的這個問題的,不想給外面用,就可以把Sum放成Solution的內部類,并且設置成私有。
這樣改進,Sum里面也不用放這兩個靜態成員變量了,可以讓Solution來放:
- 好處1:Solution直接就能訪問這兩個成員變量
- 好處2:Sum作為Solution的內部類,也能訪問這兩個成員變量。
這樣就實現了更好的封裝:Sum類完全封死在Solution內部 ——只有Solution內部能夠創建Sum類的對象,外部無法創建Sum類的對象,減少干擾。
8.2?日期·是·今年的第幾天
計算日期到天數的轉換_OJ鏈接
思路1:把日期類copy過來,利用日期類的減法來做。
思路2:
1)定義年(int)、月(int)、日(int)
2)定義一個數組: 不是每個月的天數,而是1月到n月的累計天數(平年)
3)判斷閏年,修正算法(+1day)
8.3?日期差值
日期差值_OJ鏈接
8.4?打印日期
打印日期_OJ鏈接
8.5?累加天數
累加天數_OJ鏈接
9.?補充問題
9.1?析構順序
同一個函數棧幀內的對象,后定義的,先析構。先定義的,后析構。
先定義的,在棧的高地址端,后定義的,在棧的低地址端。
函數棧幀銷毀時,是從低地址→高地址的順序銷毀的——棧的特點是后進先出。
9.2?構造順序
總的結果:
調試觀察:
- 全局對象,在main函數之前構造;
- 局部的靜態對象,生命周期是全局的,f2第一次調用的時候構造(初始化);