C++ 類的基礎操作函數

C++ 類

C++相比於C語言,擴展的第一個特性就是其封裝思想,或者說面向對象編程。封裝思想是指對用戶只知道其接口以及怎麼使用這個對象,而無需知道其實現,而封裝的實現方法就是類。而類,便是狹義的對象(廣義的對象包括基本內置類型)。

class,struct

–C結構體擴展而來的類,this指針
我們可以通過寫一個類來定義屬於自己的複合類型(基於其他類型而定義的類型),C++的類相比C語言的結構體增加了成員函數。也就是說C++的類成員由 成員對象,成員函數 兩者構成。這種擴展其實是通過一個傳地址給this指針實現的。
在講解this指針前我們先對一個類對象的大小進行sizeof運算,我們可以發現:對於一個空的類(不含任何成員),其大小爲一個指針的大小,即一個字節。而對於一個非空的類,其大小等於所有成員變量的大小和。也就是說成員函數始終沒有佔據對象的內存。所以從根本上C++的類對象與C結構體的存儲是沒有區別的。
C++類的所有成員函數沒有存儲在類對象的內存,所有的對象共用同一成員函數。
那問題就出現了:既然成員函數不在對象的內存,那通過.運算符是如何訪問到成員函數的?
當我們通過一個對象a以.運算符的方式調用成員函數時,實際上會進行的是將對象a的地址傳給成員函數的第一個隱藏參數this指針。

a.print();
等價於:
A::print(&a);

this指針默認是一個指針常量,一經綁定就只能指向對象a。通過這種方式就能實現,成員函數不在對象內存,但是卻可以邏輯上實現通過對象調取了成員函數。
伴隨着this指針的存在,當我們在成員函數中調用一個對象成員變量名時,如果成員函數中沒有單獨聲明這個同名變量,那麼我們可以直接使用成員變量名,而無需再通過對象去調取成員變量。因爲它會隱式的使用this指向的成員。

知識點:const成員函數

class A
{
 int f() const;
};
int A:: f() const
{}

我們可以通過以上形式聲明一個const成員函數,需要注意的是const在聲明和定義時都需要寫,因爲可以通過加const的方式重載成員函數。我們都知道重載是通過改寫函數參數實現的。這裏的重載實現方式也不例外。加了const的成員函數會將第一個隱式參數this指針的類型聲明成指向常量的指針常量,也就是說在原本指針常量的基礎上將其再附加成常量指針。所以不能在const成員函數內修改成員變量,因爲this指針是一個常量指針!

類的成員函數
類的成員函數可以定義在類內部,也可以定義在類外部。定義在類外部,需要使用::域解析符來指出這個名字所屬的作用域,定義在類內部的成員函數默認爲內聯函數。

構造函數與析構函數

構造函數與析構函數是每個類的最基本成員函數。構造函數負責對象成員的初始化,而析構函數負責對象銷燬前的完善工作。
1.構造函數沒有返回值類型
因爲語法上默認爲構造函數返回一個臨時對象。注意void也不行,因爲就算是void,我們還能使用return。
2.構造函數和析構函數都不允許被寫成const成員函數。 error:構造函數或析構函數上不允許使用類型限定符
3.析構函數也沒有返回值類型
當能夠返回一個值的時候,我們就應該對這個值進行處理,但是若要處理這個值,就必須顯式調用析構函數,這種做法顯然是不正確的。
思考?
構造函數也可以顯式調用,析構函數也可以顯式調用,方法如下:

	A a;
	a.A::A();
	a.A::~A();

那麼爲什麼我們不要顯式調用呢?
因爲:
構造函數在對象生成時會自動隱式調用,如果再顯式調用一次,如果隱式調用時,構造函數爲對象分配了內存,當再次顯式調用,之前那片內存就泄露了。同理,析構函數在對象生命週期結束之前被自動調用,如果構造函數中存在釋放內存的操作,當再次顯式調用,就會導致程序崩潰。雖然我們不應該通過一個對象顯式調用構造函數與析構函數,但是我們可以直接調用構造函數得到一個臨時對象。但是仍然不能直接調用析構函數!

類應包含的基本操作:

編譯器能夠幫忙合成的合成版本操作包括:
1.合成構造函數
2.合成析構函數
3.合成拷貝構造函數
4.合成拷貝賦值運算符
我們又將拷貝構造函數,拷貝賦值運算符,析構函數稱爲拷貝控制成員,因爲當你需要寫其中一者時,你就應該也重寫其他兩者。因爲三者總是相互關聯的。

構造函數之——初始化列表

因爲在調用構造函數時進行初始化,當進入構造函數函數體的時候初始化就結束,所以如果我們的有些成員是const,引用,或者沒有默認構造函數的類對象(需要提供參數初始化)時,就需要進行初始化,而不能依靠進入函數體的賦值。
例:

class A{
	private:
		int m;
		int &n;
		const int o;
	public:
		A(int _m,int _n,int _o):m(_m),n(_n),o(_o){
		}
};
class B{
	private:
		int b;
		A a;
	public:
		B(int _b,int _m,int _n,int _o):b(_b),a(_m,_n,_o){
		}
};

注意;成員初始化的順序與它定義的順序有關,而與它在初始化列表中出現的的順序無關:

int i;
int j;
A(int _a):j(_a),i(j){}
//例如在以上情況的時候,會先進行i的初始化,這時候就會導致出錯。
//爲避免以上錯誤的出現,我們應該儘量保證初始化的順序與定義的順序相同。
//而且應該儘量避免用一個成員去初始化另一個成員。

拷貝構造函數

定義:第一個參數是自身類型的引用,且其他任何參數都有默認值。
1.引用可以是常量引用,也可以是非常量引用。一般常寫爲常量引用。但必須是引用!因爲在傳參時會隱式調用拷貝構造函數,如果拷貝構造函數的參數不是引用,則會遞歸調用拷貝構造函數,這在邏輯上肯定時錯誤的。
2.拷貝構造函數不同於合成構造函數,當有任一非拷貝構造函數存在,這並不會影響編譯器幫助合成拷貝構造函數,你只能通過自己編寫拷貝構造函數才能夠告訴編譯器不需要合成拷貝構造函數。並且有時候你就算自己寫了拷貝構造函數,編譯器爲了優化可能也會爲你生成一個合成拷貝構造函數。

知識點: 拷貝初始化 與 直接初始化
我們需要注意的是:拷貝構造函數並不是只能用於拷貝初始化,拷貝初始化與直接初始化的區別在於是否調用一次類型轉換,而與調用的構造函數類型無關。

string str(10,'.');		//直接初始化
string str = "peking university" //拷貝初始化  注意此時的=不是賦值的=,因爲其是定義時初始化

直接初始化完成的步驟是:
直接找與傳入參數相匹配的構造函數
拷貝初始化完成的步驟是:
如果就是自身類型,則直接調用拷貝構造函數,這個時候與直接初始化其實沒有區別。如果不是自身類型,則先根據實參匹配一個構造函數,經過類型轉換生成一個臨時對象,再調用拷貝構造函數賦值。但這時調用的拷貝構造函數不是你自己寫的拷貝構造函數,而是編譯器合成版本,就算你自己寫了,編譯器也會幫你優化生成這個合成版本。

從上面步驟我們可以看出:直接初始化也可以調用拷貝構造函數,但是拷貝初始化一定會調用拷貝構造函數。另外,還有幾個地方也一定會調用拷貝構造函數:①非引用對象作爲形參②非引用對象作爲函數返回值 這兩種情況都是通過調用拷貝構造函數生成一個臨時對象。

合成拷貝構造函數 --什麼時候要自己寫拷貝構造函數?
出現需要考慮淺拷貝和深拷貝的情況。編譯器合成的拷貝構造函數的工作方式是逐位賦值,將除了static成員以外的所有成員逐位拷貝,對於對象則調用其拷貝構造函數。所以當構造函數中出現指針指向有分配的資源時,爲避免拷貝構造函數調用後,兩個指針指向同一片區域,應該自定義深拷貝構造函數。

Question:
爲什麼使用引用作爲函數參數,或者返回值類型會提高程序運行效率?
因爲省去了調用拷貝構造函數這一操作。

拷貝賦值運算符

重載運算符的本質是一個函數,例如拷貝賦值運算符其實是重載名爲operator=的函數。
拷貝賦值運算符函數的返回值類型應該爲左側運算對象的引用。合成拷貝運算符的工作原理與合成拷貝構造函數一樣,不過前者用於賦值,而後者是用於初始化,並且後者不具有返回值。

析構函數

1.定義一個析構函數
名字與類名相同,在前面加 ~ ,沒有參數和返回值,一個類最多隻能有一個析構函數。
(~ 是取反運算符,加上取反運算符是爲了與構造函數所區別)。
2.析構函數的作用
析構函數在類對象生命期結束時自動被調用。析構函數用於對對象消亡前做善後工作
(返回值如果是一個臨時對象,那麼這個臨時對象的消亡是在臨時對象存在的那句語句執行完之後消亡)
3.合成析構函數
如果沒有定義析構函數,編譯器生成析構函數,這個析構函數基本什麼也不做
如果定義了析構函數,編譯器就不合成析構函數。

疑問1:如何理解善後工作?
疑問2:爲什麼要自己編寫析構函數
疑問3:爲什麼要編譯器要生成析構函數?
疑問4:如何理解合成析構函數只做一些特定工作?

對於以上疑問的解答:
疑問1與疑問2:
析構函數的作用只是提供一個在對象刪除前可以釋放這個對象所佔有的資源的機會。比如你在類中用new申請了一片內存,當類的生命期結束了,是不會釋放堆中的內存。析構函數就爲我們提供了一個在 清除指向這片區域的指針(如果清除了指針,就找不到那片空間了,就會導致生成內存碎片,或者說內存泄露)之前 用delete釋放這片空間的機會。所以如果你的類只是一些基本內置類型,這時候其實析構函數什麼也不做。注意:不是說類的成員的內存是在堆裏面析構函數就會爲你做什麼,這還是需要你自己用delete釋放。這是第一種有用的缺省析構函數。還有第二種就是,你的類是一個封閉類,這個封閉類中還含有另一個成員對象。當封閉類消亡時,析構函數會爲你調用成員對象的析構函數。並且:將delete 寫在析構函數中,編譯器自動幫你調用,還能防止遺忘,導致內存泄露。
疑問3:
這是滿足編譯器的需求,滿足標準,而並不是滿足程序員的需要。就算缺省析構函數在你的情況下真的沒有用,爲滿足編譯,編譯器也會爲你生成。
注意
就算你寫了自己的析構函數,編譯器也會爲你合成一個析構函數。運行時,會先調用自定義析構函數,再調用合成的析構函數。
疑問4:
合成的析構函數只會做一件事:調用成員對象類或者基類對象的析構函數。繼承體系中存在虛函數時,應該爲你的類添寫虛析構函數。

構造函數

構造函數的作用就是初始化成員。和析構函數一樣,構造函數也被希望是隱式自動調用的,我們不應該通過對象再去顯式調用構造函數。合成版本的構造函數也同樣沒做什麼事情。他完成的工作就兩個:一是調用其他成員對象或者基類的構造函數,二是滿足編譯器需要。但與析構函數不同的是,如果你自己寫了任何構造函數,編譯器就不再生成合成構造函數版本。

成員對象和封閉類的初始化

(重點內容)
Time a{
time b;
}
a是封閉類 b是成員對象。
1.任何生成封閉類對象的語句,都要讓編譯器明白,對象中的成員對象,是如何初始化的。
什麼意思?如果成員對象的構造函數不是默認構造函數,是需要參數的。而編譯器並不能知道參數是什麼,且不能生成默認構造函數。就會導致編譯失敗。
具體做法就是:通過封閉類的構造函數初始化列表。

#include<iostream>
using namespace std;
class minute{
	public:
	int _minute; 
	minute(int x):_minute(x){}
};
class hour{
	public:
	minute a;
	int _hour;
	public: hour(int x,int y):a(x),_hour(y){
	}//在初始化列表中使用minute的構造函數 
};
int main(){
	hour a(12,20);
	cout << a._hour<<" "<< a.a._minute << endl; 
	
}

封閉類構造函數和析構函數的執行順序:
1.封閉類對象生成時,先執行所有對象成員的構造函數,然後才執行封閉類的構造函數。

2.對象成員的構造函數調用次序和對象成員在類中的說明次序一致,與它們在成員初始化列表中出現的次序無關。

3.當封閉類的對象消亡時,先執行封閉類的析構函數,然後再執行成員對象的析構函數。次序和構造函數的調用次序相反。
總結:
先構造的後析構,後構造的先析構。
構造時先構造成員對象,再構造封閉類。
析構時先析構封閉類,再析構成員對象。

其他:

內聯函數

內聯函數的優缺點
作者:Re_i_am
來源:CSDN
原文:https://blog.csdn.net/j00362/article/details/50125265

Inline這個名稱就可以反映出它的工作方式,函數會在它所調用的位置上展開。這麼做可以消除函數調用和返回所帶來的開銷(寄存器存儲和恢復),而且,由於編譯器會把調用函數的代碼和函數本身放在一起優化,所以也有進一步優化代碼的可能。不過這麼做是有代價的,代碼會變長,這就意味着佔用更多的內存空間或者佔用更多的指令緩存。內核開發者通常把那些對時間要求比較高,而本身長度又比較短的函數定義成內聯函數。如果你把一個大塊頭的程序做成了內聯函數,卻不需要爭分奪秒,反而反覆調用它,這麼做就失去了內聯的意義了。

**總結:對於簡短的函數並且調用次數比較多的情況,適合使用內聯函數。**

  使用方法:定義一個內聯函數的時候,需要使用static作爲關鍵字,並且用inline限定它(沒試過,暫且留在這裏)。比如:

 static inline void dog(unsigned long tail_size);

 內聯函數必須在使用前就定義好,否則編譯器就沒法把這個函數展開。實踐中一般在頭文件中定義內聯函數。由於使用了static作爲關鍵字進行限制,所以在編譯時不會爲內聯函數單獨建一個函數體(這裏不太明白)。如果一個內聯函數僅僅在某個源文件中使用,那麼也可以把它定義在該文件開始的地方。

 注意:在內核中,爲了類型安全的原因,優先使用內聯函數而不是複雜的宏。

類的作用域 與 名字查找規則:

類的作用域包括了花括弧括起來的部分,在類的作用域之外,想要訪問類的成員只能通過對象,對象的引用或指針進行訪問。在外部,成員的名字被隱藏起來了,在內部,外面的名字被隱藏起來了。
名字查找規則:
首先,在名字所在塊尋找聲明語句,只在名字使用前尋找。
如果沒找到,再去外層作用域尋找。
全局作用域還未找到,程序報錯。

當在類的成員函數中尋找:
先在成員函數塊中尋找,也只尋找使用之前。
如果未找到,則在類的作用域中尋找,尋找整個塊。
外層再外層直到全局還未找到,程序報錯。

爲什麼在類中尋找整個塊?
因爲編譯器對類的處理是,先聲明整個類,當整個類都可見的時候,再處理函數的定義。所以在類的作用域中可以不考慮聲明次序,使用所有成員。但需要注意的是:在聲明類的時候,仍然需要遵守,先聲明後使用的規則。

典型代碼實例:
1.

typedef double Money;
 string bal;
 class Account{
 	public:
 		Money balance (){
 			return bal;
		 }
		 //Money 和 bal 爲 double類型 
	private:
		Money bal; //
 };
int main(){
	
}
int main(){
	int a = 12;
	if(a == 12){
		cout << a << endl;
		int a = 13;
		cout << a << endl;
	}
} 
//名字可以再次聲明,哪怕之前使用過。但需要作用域不同,不然是重複定義。
//但要注意 類型別名 不能重複聲明

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章