1. 程式人生 > >C++類與物件詳解

C++類與物件詳解

一、什麼是類

1-1 程式設計的發展

  我們知道程式的發展經過了大概三個階段:面向機器的程式設計、面向過程(結構)程式設計、面向物件程式設計。其中面向機器的程式設計主要採用二進位制指令或者組合語言進行程式的編寫。這種方式對計算機來說是很容易理解的,但是對程式設計人員來說是很痛苦的,一般沒有經過特殊的訓練人員很難讀懂或者設計,因此進化出了面向過程的設計方式。
  面向過程這種方式跟我們正常思考問題的方式比較接近,比如我們需要洗衣服,根據我們的操作步驟來進行設計程式:取出衣服->倒水放洗衣粉->開始搓->晾衣服等等的步驟,進而對每一個步驟編寫函式進行模擬實現。我們使用的語言代表性的就是C語言。的確這種方式很大程度的方便了我們的程式編寫,但是隨著發展,我們需要描述越來越複雜的流程,工程也越來越龐大,當我們重新開始一個專案的時候往往需要從頭開始編寫程式碼,比如洗外套我們有一個程式,洗褲子也有一個程式,但是由於程式碼沒有重用或者共享,導致我們必須重頭開始編寫一個程式。如何才能加強程式碼的重用性、靈活性與擴充套件性呢?
  新的問題的提出便引出了我們的面向物件程式設計(OOP),這是一種計算機程式設計架構,其中有一條基本的原則就是程式是由單個能夠起到子程式作用的單元或者物件組合而成

。這樣為了整體運算,每個物件都能夠接受資訊、處理資料和向其他物件傳送資訊。每一個單元都可以運用到其他程式中。這樣OOP達到了軟體工程的三個主要目標:重用性、靈活性、擴充套件性代表就是我們的C++語言,當然C++語言也是相容面向過程的。

1-2 面向過程到面向物件

  面向物件的設計方式是怎樣實現我們的程式碼重用與擴充套件呢?我們知道在我們的面向過程方式中,我們經常是這樣做的:

 1. 對需要進行的功能設計對應的資料結構
 2. 設計需要完成這種功能的演算法

  這時候我們的程式結構是這樣的:程式=演算法+資料結構。所以當我們面對一個新問題時候往往需要重新設計對應的資料結構,重新設計完成這個功能的演算法。這樣程式碼重用性就比較低。
  當我們採用面向物件方式去設計程式時候,我們經常是這樣思考的:

 1. 這個整體物件有哪些子物件呢
 2. 每一個子物件有什麼屬性和功能呢
 3. 這些子物件是怎樣聯絡起來的

  這樣我們的設計出來的程式碼是這樣的:物件=演算法+資料結構;程式=物件+物件+物件+·······
  因此我們遇到新問題是,因為已經有各種已經完成子功能的物件,所以很方便的能夠將程式碼進行重用。
  面向物件中物件是基礎,而類便是我們用來實現這些物件的,因此類是面向物件的基礎。類描述了一組具有相同特性和相同行為的物件。

1-3 類的定義

  在C++程式中我們經常會將類的定義與其成員函式的定義分開,這樣也是為了方便閱讀與程式碼的編寫

1. 類定義可以看成類的外部介面,一般寫成.h檔案
2. 類成員函式定義可以看成類的內部實現,一般寫成.cpp檔案

  在程式碼中我們一般是這樣定義一個類的

  對於類的成員函式定義,我們一般使用如下格式:

返回值 類名::函式名(引數列表)
{
    函式體
}

  這裡需要說明的是,當函式前面沒有類名::時候,編譯器就會認為這是一個普通函式,因為我們並沒有說明這個函式是哪一個類的,同時這樣也就說明了這個函式的作用域,在類作用域裡面,一個類的成員函式對同一類的資料成員具有無限制的訪問許可權。
  對類的不同成員設定不同的訪問許可權就是進行類的封裝,這樣可以增強安全性和簡化程式設計,使用者不必瞭解具體的實現細節,而只需要通過外部介面,以特定的訪問許可權來使用累得成員。

1-4 建構函式、解構函式

  建構函式是類中一個比較重要的函式,主要用來建立和初始化物件,建構函式在物件建立時由系統自動呼叫。建構函式與類名相同,沒有返回值,預設無參形式。建構函式一般形式如下:

class 類名
{
    public : 類名();
}

注意:建構函式預設是無參的,但是是可以過載的,也就是我們可以設定有引數的建構函式。
  建構函式一般有三個功能:

1. 分配空間,即在記憶體中分類該物件的使用空間
2. 構造結構,構造整個類的結構
3. 初始化,即對類中的屬性進行初始化

  與建構函式相反的就是我們的解構函式,解構函式主要完成物件刪除前的一些清理工作,在物件生存週期結束時候系統自動呼叫,然後釋放物件的空間。解構函式在類名前新增~,沒有返回值,沒有引數,與建構函式不同,解構函式不能重構。解構函式一般形式如下:

class 類名
{
    public ~類名();
}

二、記憶體管理

2-1 記憶體分佈

  我們知道我們類在構造之後,會在記憶體中建立空間來儲存各類資料,當類生存週期結束之後,系統會呼叫解構函式回收空間,我們的類有常量、變數、函式等等,這些資料是怎樣在記憶體中儲存的呢?
  C++程式的記憶體一般分為四個區,如圖:

  需要非常注意的是:堆中和棧中的資料回收是不一樣的,操作堆記憶體,如果分配了記憶體,就有責任回收,否則會造成記憶體洩露,函式中在棧區分配的區域性變數,在函式結束時,會自動回收與釋放空間

2-2堆和棧

2-2-1 堆和棧區別

  為了方便儲存,記憶體區域分為堆和棧,兩者具有不同的特徵來儲存不同的資料。
  一般來說堆空間相對其他記憶體空間比較空間,因此會給程式帶來很大的自由度來分配空間。但是也不是說堆的記憶體是可以隨意申請的,一般使用堆的空間情況有以下幾種:

 - 直到執行時才能知道需要多少物件空間
 - 不知道物件的生存週期到底有多長
 - 直到執行時才知道一個物件需要多少記憶體空間

  而與堆不同的是,建立程式時,編譯器準確的直到棧內儲存了所有的資料長度以及存在的時間。由於棧的FILO特性,棧指標下移,就會建立新的記憶體,上移會釋放對應空間,因此是可以精準操作棧內的資料。
  舉例如下:

2-2-2 new與delete

  malloc與free是C++/C語言標準庫函式,new/delete是C++的運算子。注意一個是庫函式,一個是運算子。;兩個有什麼區別呢?對於庫函式來說,在分配堆記憶體時候,根本不關心這個物件是什麼,只關心需要分配多少空間,而操作符來說,在分配時候記憶體大小跟類物件是相關的,而且在分配記憶體時候回撥用類的建構函式。
  他們都用來申請動態記憶體和釋放記憶體。既然new用來申請記憶體,而且建構函式可以有引數,所以跟在new 後面得類型別也是可以有引數的,格式如下:

類名 *變數名 = new 類名(···);
類名 *變數名 = new 類名[元素個數];

  我們發現我們可以申請類物件陣列,但是必須注意的是從堆中分配類物件陣列時候,只能呼叫無參的預設建構函式,不能呼叫有參的建構函式,一旦類中沒有預設的建構函式不能分配類陣列
  前面說了,堆中申請的記憶體空間,需要手動回收,因此採用delete來釋放空間,會呼叫類的解構函式。格式如下:

delete 變數名;delete[] 變數名;
變數名=null             變數名=null

三、拷貝建構函式

  拷貝建構函式是一種特殊的建構函式,其形參為本類的物件引用。其本質是用一個物件去構造另一個物件,或者說用另一個物件值初始化一個新的構造物件,格式如下:

class 類名
{
    public 類名(形參)          //建構函式
    public 類名(類名 &物件名)   //拷貝建構函式
}
//拷貝建構函式的實現
類名::類名(類名 &物件名)
{
    函式體
}

  舉一個例子:

  由上面例子我們發現一個有趣的現象就是:拷貝建構函式裡面我們直接訪問了類中的私有成員(pt.m_mx),其他情況是無法使用”.“來訪問的
  拷貝建構函式具有以下特點:

 - 如果程式中沒有為類宣告一個拷貝建構函式,則編譯器自己生成一個預設的拷貝建構函式
 - 這個預設的拷貝建構函式執行功能是,把初始值物件中的每個資料成員值,都複製到新建立的物件中
 - 在預設拷貝建構函式中,拷貝的策略是逐個成員依次拷貝

  因為拷貝建構函式是將本類的物件傳遞進去了,這個時候就會出現一個問題。
  當類的建構函式在堆上分配了一個資源時候,常見的就是字串賦值,而拷貝建構函式是複製資源的,如果只是簡單的拷貝的話,就會導致兩個物件同時擁有一個資源,當一個物件釋放了該資源,另一個物件再使用的時候就會出現問題。

  這個問題怎麼解決呢?顯然拷貝建構函式不能簡單的拷貝該資源,需要手動編寫拷貝建構函式,不能使用預設的拷貝建構函式。需要在拷貝資源的時候將資源複製一份,這樣兩個物件就會指向不同的資源。這就是淺拷貝和深拷貝的區別
  拷貝建構函式經常在以下進行使用

 - 當用類的一個物件去初始化該類的另一個物件時
 - 若函式的形參是類的物件,呼叫函式時,實參賦值給形參,系統自動呼叫拷貝建構函式
 - 當函式返回值是類的物件時,系統自動呼叫拷貝建構函式。

四、類的組合

4-1什麼是類的組合

  什麼是類的組合呢,簡單的來說類的組合就是類的成員資料是另一個類的物件,這樣就可以在已有的抽象基礎上實現更加複雜的抽象

4-2類組合的建構函式

  當我們的類進行組合之後,怎樣對類進行初始化呢?一般來說不僅要負責對本類中的型別成員進行初始賦值,也要對物件成員進行初始話。宣告格式如下:

類名::類名(物件成員所需的形參,本類成員形參):物件1(引數),物件2(引數)
{
    本類初始化
}

  舉例如下:

  當類組合之後,就會遇到一個新的問題,我們知道每一個類都有建構函式與解構函式,當類組合的時候,建構函式與解構函式的呼叫順序是怎樣的呢?

 1. 建構函式的呼叫順序
      1. 呼叫內嵌物件的建構函式(按照內嵌時的宣告順序,先宣告者先構造)
      2. 呼叫本類建構函式

 2. 解構函式的呼叫順序
     1. 呼叫本類解構函式
     2. 呼叫內嵌物件的解構函式(按照內嵌時的宣告順序,先宣告的先析構)

  這裡需要說明的是若呼叫預設的建構函式,則內嵌物件的初始化也將呼叫預設的建構函式

五、繼承

5-1 繼承與派生

  既然是面向物件的,我們的顯示世界就是一個物件的世界,那麼我們也應該描述現實世界物件間的關係,比如繼承,為了描述這種類之間的關係,C++中也是引入了繼承的概念
  繼承是C++中一個重要的機制,該機制自動的為一個類提供來自另一個類的操作和資料結構,這使得我們只需要在新類中定義已有類沒有的成分來建立新類。繼承就是為了程式碼的重用和擴充
  繼承一般的定義格式如下:

class 派生類:public 父類
{
    類的實現
}

  既然類中的成員具有不同的訪問許可權,而類繼承也有不同的繼承方式(一般類繼承分為公有繼承、保護繼承、私有繼承),那麼類繼承之後,也應該具有不同的訪問許可權,不同的派生情況與類成員的訪問許可權如下:

5-2 繼承時的建構函式、解構函式

  既然基類的成員能夠被繼承,那麼基類的一些特殊函式怎麼辦呢?
  基類的建構函式是不被繼承的,派生類是需要自己生命自己的建構函式。但是在建構函式時候,只需要對本類中新增成員進行初始化,對繼承來的基類成員初始化,自動呼叫基類的建構函式完成。
  派生類的建構函式形式如下:

派生類名::派生類名(基類所需的形參,本類所需的形參):基類名(引數表)
{
    本類成員初始化
}

  舉例如下:

  一旦牽扯到多各類,那麼就必須說明每個類的呼叫順序。對於繼承情況下,建構函式的呼叫順序如下:

 - 呼叫基類的建構函式,呼叫順序是按照他們被繼承時的宣告順序(從左到右)
 - 呼叫成員物件的建構函式,呼叫順序按照他們在類中宣告的順序
 - 派生類的建構函式

  舉例如下:

  與建構函式相同,解構函式也不能被繼承,派生類自行宣告,宣告方法與一般類的解構函式相同。值得注意的是,這裡並不需要顯示的呼叫基類的解構函式,系統會自動隱式呼叫,同時解構函式的呼叫順序與建構函式相反。
  舉例如下:

5-3 重寫父類方法

  一般父類定義的方法只能操作父類的成員,當子類繼承之後,呼叫父類的方法是隻能操作繼承來自父類的成員,當子類也想進行相應的操作,此時呼叫父類的方法已經不能夠完成了,這時候需要子類重寫父類的方法。
  執行時編譯器會根據我們的呼叫情況來呼叫父類還是子類的方法。舉列子如下:

六、多型與虛擬函式

6-1什麼是多型

  現實情況下,我們經常需要實現某一種功能,就是對於同一個函式,當不同的物件去執行時候,會得到不同的結果,這樣便極大的方便了我們的程式編寫。
  為了實現這個功能,我們需要採用C++的一種重要機制,多型。所謂的多型性就是發出同樣的訊息被不同的型別物件接受時可能導致完全不同的行為。
  多型性的實現依靠的是一種動態繫結的技術。什麼是繫結呢?繫結就是程式自身彼此關聯的過程,確定程式中的操作呼叫與執行該操作程式碼間的關係。
  因此繫結就有兩種,靜態繫結與動態繫結。靜態繫結也叫做靜態聯編,出現在編譯階段,動態繫結也叫做動態聯編,是出現在程式的執行過程,在程式執行時才確定需要呼叫的函式

6-2 虛擬函式

  為了證明某個函式具有多型性,我們便用關鍵字virtual關鍵字來將其標記,使用該標記的函式我們程式虛擬函式。虛擬函式一般宣告如下:

virtual 返回值 函式名(引數列表)

  對於虛擬函式我們有兩點需要說明的:

 1. virtual 只能用來說明類宣告中的原型,不能用在函式的實現時
 2. 基類中宣告的虛擬函式,不管派生類是否宣告嗎,同原型函式都自動轉換為虛擬函式

  舉例如下:

  虛擬函式是怎樣實現的呢?一般虛擬函式的實現是通過一張虛擬函式表來實現的,簡稱為V-Table。虛擬函式表主要是一個類的虛擬函式地址表,當用父類的指標來操作一個子類時,虛擬函式表就是一張地圖,來指明實際應該呼叫的函式

  需要值得說明的是:
1. 編譯器將虛擬函式表的指標存放在物件例項開頭,通過物件的地址即可得到這張虛擬函式表,遍歷其函式指標可呼叫相應的函式
2. 虛擬函式按其宣告順序放於表中,父類虛擬函式放在子類虛擬函式前面
  前面說了只有將函式用virtual進行修飾編譯器才會進行動態聯編來進行編譯,否則編譯器並不知道是不是要後期編譯。可以說虛擬函式的本質是覆蓋不是過載宣告

  根據虛擬函式的特徵,有幾點需要說明的是:

 1. 只有類成員函式才能說明為虛擬函式。虛擬函式僅適用於繼承關係的類物件
 2. 靜態成員函式不能使虛擬函式,因為靜態成員函式不受限於某個物件
 3. 行內函數、建構函式不能使虛擬函式
 4. 解構函式可以使虛擬函式,通常也就是虛擬函式

6-3 純虛擬函式與抽象類

  在虛擬函式的基礎上我們還有一種虛擬函式叫做純虛擬函式,就是當基類當前並不具體實現的一個成員函式,留給派生類去實現,相當於給子類保留了一個位置,以便派生類來實現。
  純虛擬函式一般定義如下:

Vitural 型別 函式名(引數表)=0

  可以發現,將虛擬函式等於0就是純虛擬函式。包含純虛擬函式的類就是抽象類。
  抽象類就是為了抽象和設計的目的而宣告的,將有關的資料和行為組織在一個繼承層次的結構中,保證派生類具有的要求行為。對於一個暫時無法實現的函式,可以宣告為純虛擬函式,留給派生類來實現。
  對於抽象類必須說明

 1. 抽象類只能作為基類來使用
 2. 不能宣告抽象類的物件,哪怕是實現了部分純虛擬函式的類

特此宣告:
  本文章為原創文章,歡迎喜歡熱愛本文章的人進行討論,如有不足肯定大家提出。非常感謝!!!同時若轉載請宣告出處,謝謝!!!