在C++的學習中,可以把類當作一個模具,類所例項化出來的物件就是根據這個模具所產生的實體,物件可以看作是我們自己建立的一個新的資料型別。本文主要介紹了類物件通過拷貝函式進行初始化,分析了類物件的記憶體模型,以及通過this指標來實現更復雜的功能。最後介紹了解構函式的基礎知識。
1、建構函式
首先,由於類只是一個模板,因此我們在定義類時無法對成員變數初始化,比如下面程式碼就是錯誤的:
class circle{
public:
int m_L = 20; // Error:不允許使用資料成員初始值設定項
};
因此,初始化只能發生在類建立物件的過程中,但是由於訪問許可權的原因,無法在類外訪問某些成員變數,因此下面這種做法有時候是無效的:
circle C1; // 例項化一個物件 C1
C1.m_L = 20; // 通過建立的物件,來給對應變數初始化,但是如果m_L是private訪問許可權,則失效
為了解決這個問題,讓程式設計師能像使用標準資料型別一樣適用物件,在類內提供了一個特殊的成員函式——“建構函式”,專門用於在建立物件時初始化類物件。之所以說它特殊,是因為C++已經自動為建構函式提供了名稱和使用語法,程式設計師只需要提供方法的定義即可,即:類名(形參列表)
。具體來說,建構函式的定義如下:
class circle{
public:
int m_L;
public: //成員函式(方法)
circle(const int a) //通過建構函式對成員變數進行賦值
{
m_L = a;
}
};
circle C1(20); //呼叫格式
cout << "C1.m_L:" << C1.m_L <<endl;
看上去似乎很簡單,但是由於建構函式也是函式,因此所有C++中的形參傳遞方式,函式特性以及函式呼叫方法都能用於建構函式。 如前文所講,C++會自動給類新增一個空的建構函式,但是如果自己在類中實現了有參建構函式,編譯器便不再提供無參建構函式。舉例如下:
class circle{
public:
int m_L;
public: //成員函式(方法)
circle(const int a) //通過建構函式對成員變數進行賦值
{
m_L = a;
}
};
circle C1(20); //呼叫格式正確,能夠通過建構函式賦值
circle C2; //錯誤,自己定義了有參建構函式,不再提供無參建構函式
建構函式可以過載,接著上面的例子,如果過載一個空的建構函式,那麼兩個呼叫格式都正確:
class circle{
public:
int m_L;
public: //成員函式(方法)
circle(){} // 空建構函式
circle(const int a) //通過建構函式對成員變數進行賦值
{
m_L = a;
}
};
circle C1(20); //呼叫格式正確,能夠通過建構函式賦值
circle C2; //正確,可以通過過載的空建構函式實現初始化
建構函式的引數不僅可以是標準資料型別,也可以是類。眾所周知,在數值作為函式引數進行傳遞的時候,會重新拷貝出來一份資料作為引數傳遞用完即銷燬,這種方式不僅浪費了記憶體空間,而且無法修改原始資料。為了結合這兩者之間的優點,於是經常採取引用作為函式的引數。雖然引用是指標的一種特殊情況,但是指標太過於靈活,並且引用在形式上引用與普通的變數地用法並沒有什麼區別,因此使用起來更加方便。如果不清楚引用和指標地區別,請移步上一篇文章:C++中的訪問許可權 - ZhiboZhao - 部落格園 (cnblogs.com)
class circle{
public:
int m_L;
public: //成員函式(方法)
circle(){} // 空建構函式
circle(const int a) //通過建構函式對成員變數進行賦值
{
m_L = a;
}
//引用作為函式引數傳遞,並用const修飾,節省空間的同時避免修改原資料
circle(const circle& sub_circle)
{
m_L = sub_circle.m_L;
}
};
circle C1(20); //呼叫格式正確,能夠通過建構函式賦值
circle C2(C1); //正確,可以通過拷貝建構函式進行初始化
2、C++類的記憶體模型
C++中,一個類包括:
- 成員變數:靜態成員變數和普通成員變數
- 成員函式:靜態成員函式和普通成員函式
雖然為了整合,我們將其寫到一個類裡面,但是隻有普通成員變數真正屬於類的物件,類的所有物件共享一份靜態成員函式,靜態成員變數和普通成員函式。結合前面的內容 記憶體四區之程式碼區,全域性區,棧區和堆區 - ZhiboZhao - 部落格園 (cnblogs.com),我大致畫出了記憶體模型,如下圖所示:
為了進一步理解,我們舉例如下:
2.1 只定義成員函式
class person{
public:
// 定義一個空的建構函式
person(int m_age, int m_ID){
}
};
person p1(10, 20);
cout << "p1 所佔的空間為:" << sizeof(p1) << endl;
輸出結果為:
p1 所佔的空間為:1
這個題目在《劍指offer》一書中也提到過,由空類例項化出來的物件所佔的記憶體空間是1個而不是0個位元組,因為編譯其給物件 p1
分配了一個地址,來表示不同的物件儲存在不同的地址空間,因此佔用1個位元組。
2.2 往空類中新增靜態成員變數
class person{
static int age; //靜態成員變數,存在全域性區,不屬於類物件的一部分
static int ID; //靜態成員變數,存在全域性區,不屬於類物件的一部分
public:
// 定義一個空的建構函式
person(int m_age, int m_ID){
}
};
person p1(10, 20);
cout << "p1 所佔的空間為:" << sizeof(p1) << endl;
輸出結果為:
p1 所佔的空間為:1
當向類中加入了成員函式與靜態成員變數時,類的例項化物件仍然只佔用1個位元組的空間,足以證明這些函式和變數並不是類物件的一部分。
2.3 再加入非靜態成員變數
class person
{
static int age; //靜態成員變數,存在全域性區,不屬於類物件的一部分
static int ID; //靜態成員變數,存在全域性區,不屬於類物件的一部分
int a; //非靜態成員變數,存在棧區,屬於類物件的一部分
public:
person(int m_age, int m_ID){
}
};
輸出結果為:
p 所佔的空間為:4
因此當向類中加入了非靜態成員變數時,類的例項化物件佔用4個位元組的空間,可以說明,非靜態變數屬於類物件的一部分。綜上:同一個類所有例項化出來的物件共享同一份靜態成員變數,所以一改全改。既然同一個類的不同物件共享同一份成員函式,那麼成員函式怎麼區分該訪問哪個物件的普通成員變數呢?
3、this指標
接著上一小節的問題,this指標為上述問題提供了一個完美的解決方案,它指向用來呼叫成員函式的物件(被當作引數隱式地傳遞給成員函式),我們通過一張圖來理解它:
此外,this指標的另一個用途是當成員函式需要返回物件時,用 return *this
; 或者 return this
,這種做法能夠實現鏈式程式設計。比如:
p2.addPerson(p1).addPerson(p1);
首先,物件 p2
呼叫成員函式 addPerson(p1)
,其返回值繼續呼叫 addPerson(p1)
,此時返回值就必須也是 person
型別才可以,因此使用 this
指標可以完成需求。先來看第一個例子:
class person{
public:
int age;
person(int age)
{
this->age = age; // this指標區分呼叫者
}
// 返回值為person型別,且引數加上了const限制,防止修改原資料
person addPerson(const person& p)
{
this->age += p.age; // 主要實現兩個類物件年齡的相加
return *this; // 由於返回值是person,因此返回 *this
}
};
person p1(20);
person p2(10);
person p3 = p2.addPerson(p1).addPerson(p1);
cout << "p1 age:" << p1.age << endl;
cout << "p2 age:" << p2.age << endl;
cout << "p3 age:" << p3.age << endl;
首先,通過建構函式分別對 p1,p2
賦了初值,然後 p2
呼叫函式 addPerson(p1)
修改自身的變數 age
。**由於函式通過值傳遞的方式返回 person
型別,所以將整個 person
型別複製了一份返回,返回值繼續呼叫 addPerson(p1)
, **最後的結果賦值給了新的物件 p3
。所以輸出結果為:
p1 age:20
p2 age:30
p3 age:50
但是如果函式 addPerson()
修改為:
person& addPerson(const person& p)
{
this->age += p.age; // 主要實現兩個類物件年齡的相加
return *this; // 雖然返回值是person&,返回值的型別也是 *this
}
person p1(20);
person p2(10);
person p3 = p2.addPerson(p1).addPerson(p1);
cout << "p1 age:" << p1.age << endl;
cout << "p2 age:" << p2.age << endl;
cout << "p3 age:" << p3.age << endl;
與上例唯一的區別就在於返回值的型別變成了引用,那麼每次返回的就變成了該物件本身,而非在值傳遞中拷貝出來的那一份資料。那麼輸出就變成了:
p1 age:20
p2 age:50
p3 age:50
4、解構函式
用建構函式建立物件後,程式負責跟蹤該物件,知道其過期為止。當物件過期時,程式自動呼叫解構函式完成清理工作。與建構函式一樣,C++預設提供了一個空的解構函式,定義為:~類名( )
。由於開闢在棧區的變數程式會自動釋放,因此不需要解構函式執行清理工作,但是當程式設計師在堆區開闢空間時,需要手動執行清理工作,這時候需要解構函式來釋放堆區記憶體。比如:
~person()
{
// 在解構函式內寫入需要執行的程式碼
cout << "呼叫解構函式" << endl;
}
person p1(20);
person p2(10); // 在生命週期結束後自動呼叫解構函式執行清理工作
輸出為:
呼叫解構函式
呼叫解構函式