1. 程式人生 > 實用技巧 >C++面向物件程式設計(上)

C++面向物件程式設計(上)

C++面向物件程式設計(上)

C語言是一種基於過程的程式語言,C++在此基礎上發展而成,保留了C的絕大部分的功能和執行機制。同時增加了面向物件的機制。C++面向物件的程式設計,除了主函式,其他的函式基本都在類中,只有通過類才能呼叫類中的函式。程式的基本單元是類,程式面對的是一個個類和物件。

☆面向過程的程式設計(Process oriented programming),也稱為結構化程式設計(Structured programming),有時會被視為是指令式程式設計(Imperative programming)的同義語。。編寫程式可以說是這樣一個過程:從系統要實現的功能入手把複雜的任務分解成子任務,把子任務再分解成更簡單的任務,層層分解來完成。可以採用函式(function)或呼叫(procedure)一步步細化呼叫實現。程式流程結構可分為循序(sequence)、選擇(selection)及重複(repetition)或迴圈(loop)。

☆面向物件程式設計(Object Oriented Programming),是圍繞著問題域中的物件(Object)來設計,物件包含屬性、方法。物件則指的是類的例項。它將物件作為程式的基本單元,將程式和資料封裝其中,以提高軟體的重用性、靈活性和擴充套件性,物件裡的程式可以訪問及經常修改物件相關聯的資料。在面向物件程式程式設計裡,計算機程式會被設計成彼此相關的物件。

面向物件程式設計的基本概念概述

物件(Object)

物件是指客觀存在的事物(可以是看得見摸得著的,也可以是看不見摸不著的),由一組屬性和方法構成。

物件 = 屬性 + 方法

在面向物件程式設計中,物件之間也需要聯絡,我們稱作物件的互動。

屬性(Property)是用來描述物件的外部特徵。

屬性的引用方法為:

物件名.屬性名 = 屬性值 或 變數名 = 物件名.屬性名

方法(Method)是對某物件接收訊息後所採取的操作的描述,它表明了一個物件所具有的行為能力。

呼叫物件的方法為:

物件名.方法名[引數列表]

類(Class)

(1)類是具有共同特徵的物件的抽象。

(2)類是對具有共同屬性和行為的一類事物的抽象描述。 共同的屬性被描述為類的資料成員,共同行為被描述為類的成員函式。

類是物件之上的抽象,是物件的模板;物件是類的具體化,稱為類的例項(instance)。類可以有子類,也可以有父類,從而形成層次關係。

面向物件程式設計的三大特徵:

封裝是基礎,繼承是關鍵,多型是補充,而多型又必須存在於繼承的環境中,多型的實現受到繼承性的支援。

1)封裝(Encapsulation):

對物件的封裝指的是把它一部分屬性和功能對外界遮蔽,這樣就做到了把物件的內部實現和外部行為分割開來。封裝的好處:實現各個物件間的相對獨立和資訊隱蔽。封裝(Encapsulation)是面向物件程式設計最基本的特性。把資料(屬性)和函式(操作)合成一個整體,這在計算機世界中是用類(class)和物件(object)實現的。

2)繼承(Inheritance)

繼承是是面向物件的程式中兩個類之間的一種關係,即一個類可以從另一個類(即它的父類)繼承狀態屬性和行為方法。繼承父類的類稱為子類。

繼承是指這樣一種能力:它可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴充套件。被繼承的類稱為“基類”、“父類”或“超類”。通過繼承建立的新類稱為“子類”或“派生類”。

3)多型性(polymorphism)

不同繼承關係的類物件去呼叫同一函式(方法)時,產生了不同的行為(功能)。

多型性又被稱為“ 一個名字,多個方法”。

實現多型,有二種方式:

覆蓋:是指子類重新定義父類的虛擬函式的做法。

過載:是指允許存在多個同名函式,而這些函式的引數表不同(或許引數個數不同,或許引數型別不同,或許兩者都不同)。

C++類
在C++中,每個物件都由資料和函式組成,資料體現了屬性,函式體現了行為,也可以稱之為方法。類是物件的抽象,而物件則是類的特例(類的例項化)。

C++中使用關鍵字 class 來定義類, 其基本形式如下:
class 類名
{
public:
//公共的行為或屬性

private:
//公共的行為或屬性
};
說明:
1)類名 需要遵循一般的命名規則;
2)public 與 private 為屬性/方法限制的關鍵字, private 表示該部分內容是私密的, 不能被外部所訪問或呼叫, 只能被本類內部訪問; 而 public 表示公開的屬性和方法, 外界可以直接訪問或者呼叫。
一般來說,類的屬性成員都應設定為private, public只留給那些被外界用來呼叫的函式介面, 但這並非是強制規定, 可以根據需要進行調整;
3)結束部分的有分號。

例、定義一個點(Point)類, 具有以下屬性和方法:
屬性: x座標, y座標
方法: 1.設定x,y的座標值; 2.輸出座標的資訊。
class Point
{
public:
void setPoint(int x, int y);
void printPoint();

private:
int xPos;
int yPos;
};

C++類成員函式的定義
一個類的資料和函式統稱為類的成員。類成員函式的實現有兩種方式:。
1、在類定義時定義成員函式
成員函式的實現可以在類定義時同時完成, 如程式碼:
#include <iostream>
using namespace std;
class Point
{
public:
void setPoint(int x, int y) //實現setPoint函式
{
xPos = x;
yPos = y;
}

void printPoint() //實現printPoint函式
{
cout<< "x = " << xPos << endl;
cout<< "y = " << yPos << endl;
}

private:
int xPos;
int yPos;
};

int main()
{
Point M; //用定義好的類建立一個物件 點M
M.setPoint(10, 20); //設定 M點 的x,y值
M.printPoint(); //輸出 M點 的資訊

return 0;
}

執行之,參見下圖:

2、在類外定義成員函式
在類外定義成員函式通過在類內進行宣告, 然後在類外通過作用域操作符 :: 進行實現, 形式如下:
返回型別 類名::成員函式名(引數列表)
{
//函式體
}

將上例程式碼改用在類外定義成員函式的程式碼:
#include <iostream>
using namespace std;
class Point
{
public:
void setPoint(int x, int y); //在類內對成員函式進行宣告
void printPoint();

private:
int xPos;
int yPos;
};

void Point::setPoint(int x, int y) //通過作用域操作符 '::' 實現setPoint函式
{
xPos = x;
yPos = y;
}

void Point::printPoint() //實現printPoint函式
{
cout<< "x = " << xPos << endl;
cout<< "y = " << yPos << endl;
}

int main()
{
Point M;//用定義好的類建立一個物件 點M
M.setPoint(10, 20); //設定 M點 的x,y值
M.printPoint(); //輸出 M點 的資訊

return 0;
}

執行之,參見下圖:

類物件的建立(類的例項化)
將一個類定義並實現後, 就可以用該類來建立物件了, 建立的過程如同 int、char 等基本資料型別宣告一個變數一樣簡單,建立一個類的物件稱為該類的例項化,格式:
類名 物件名;
如上面的例子中的:Point M;

類物件成員的使用
通過 物件名.公有函式名(引數列表); 的形式就可以呼叫該類物件所具有的方法——成員函式, 通過 物件名.公有資料成員; 的形式可以訪問物件中的資料成員。
如上面的例子中的:M.setPoint(10, 20);

C++建構函式與解構函式
1、建構函式
建構函式的作用
C++中的建構函式類似於Python中的 __init__ 方法。建構函式主要用來在建立物件時完成對物件屬性的一些初始化等操作, 當建立物件時, 物件會自動呼叫它的建構函式。一般來說, 建構函式有以下三個方面的作用:
①給建立的物件建立一個識別符號;
②為物件資料成員開闢記憶體空間;
③完成物件資料成員的初始化。

預設建構函式
當用戶沒有顯式的去定義建構函式時, 編譯器會為類生成一個預設的建構函式, 稱為 "預設建構函式", 預設建構函式不能完成物件資料成員的初始化, 只能給物件建立一識別符號, 併為物件中的資料成員開闢一定的記憶體空間。

建構函式的特點
無論是使用者自定義的建構函式還是預設建構函式都主要有以下特點:
①在物件被建立時自動執行;
②建構函式的函式名與類名相同;
③沒有返回值型別、也沒有返回值;
④建構函式不能被顯式呼叫。

建構函式的顯式定義
由於在大多數情況下我們希望在物件建立時就完成一些對成員屬性的初始化等工作, 而預設建構函式無法滿足我們的要求, 所以我們需要顯式定義一個建構函式來覆蓋掉預設建構函式以便來完成必要的初始化工作, 當用戶自定義建構函式後編譯器就不會再為物件生成預設建構函式。

在建構函式的特點中我們提到, 建構函式的名稱必須與類名相同, 並且沒有返回值型別和返回值, 看一個建構函式的定義:
#include <iostream>

using namespace std;

class Point
{
public:
Point() //宣告並定義建構函式
{
cout<<"自定義的建構函式被呼叫...\n";
xPos = 100; //利用建構函式對資料成員 xPos, yPos進行初始化
yPos = 100;
}
void printPoint()
{
cout<<"xPos = " << xPos <<endl;
cout<<"yPos = " << yPos <<endl;
}

private:
int xPos;
int yPos;
};

int main()
{
Point M; //建立物件M
M.printPoint();

return 0;
}

執行之,參見下圖:

說明:

在Point類的 public 成員中我們定義了一個建構函式 Point() , 可以看到這個Point建構函式並不像 printPoint 函式有個void型別的返回值, 這正是建構函式的一特點。在建構函式中, 我們輸出了一句提示資訊, "自定義的建構函式被呼叫...", 並且將物件中的資料成員xPos和yPos初始化為100。
在 main 函式中, 使用 Point 類建立了一個物件 M, 並呼叫M物件的方法 printPoint 輸出M的屬性資訊, 根據輸出結果看到, 自定義的建構函式被呼叫了, 所以 xPos和yPos 的值此時都是100, 而不是一個隨機值。
建構函式的定義也可放在類外進行。

帶有引數的建構函式
在上個示例中實在建構函式的函式體內直接對資料成員進行賦值以達到初始化的目的, 但是有時候在建立時每個物件的屬性有可能是不同的, 這種直接賦值的方式顯然不合適。不過建構函式是支援向函式中傳入引數的, 所以可以使用帶引數的建構函式來解決該問題。
#include <iostream>
using namespace std;

class Point
{
public:
Point(int x = 0, int y = 0) //帶有預設引數的建構函式
{
cout<<"自定義的建構函式被呼叫...\n";
xPos = x; //利用傳入的引數值對成員屬性進行初始化
yPos = y;
}
void printPoint()
{
cout<<"xPos = " << xPos <<endl;
cout<<"yPos = " << yPos <<endl;
}

private:
int xPos;
int yPos;
};

int main()
{
Point M(10, 20); //建立物件M並初始化xPos,yPos為10和20
M.printPoint();

Point N(200); //建立物件N並初始化xPos為200, yPos使用引數y的預設值0
N.printPoint();

Point P; //建立物件P使用建構函式的預設引數
P.printPoint();

return 0;
}

執行之,參見下圖:

說明:
在這個示例中的建構函式 Point(int x = 0, int y = 0) 使用了引數列表並且對引數進行了預設引數設定為0。在 main 函式中共建立了三個物件 M, N, P。
M物件不使用預設引數將M的座標屬性初始化10和20;
N物件使用一個預設引數y, xPos屬性初始化為200;
P物件完全使用預設引數將xPos和yPos初始化為0。

物件中的一些資料成員除了在建構函式體中進行初始化外還可以通過呼叫初始化表來進行完成, 要使用初始化表來對資料成員進行初始化時使用 : 號進行調出, 例如:
Point(int x = 0, int y = 0):xPos(x), yPos(y)  //使用初始化表
{
cout<<"呼叫初始化表對資料成員進行初始化!\n";
}
在 Point 建構函式頭的後面, 通過單個冒號 : 引出的就是初始化表, 初始化的內容為 Point 類中int型的 xPos 成員和 yPos成員, 其效果和 xPos = x; yPos = y; 是相同的。

與在建構函式體內進行初始化不同的是, 使用初始化表進行初始化是在建構函式被呼叫以前就完成的。每個成員在初始化表中只能出現一次, 並且初始化的順序不是取決於資料成員在初始化表中出現的順序, 而是取決於在類中宣告的順序。
一些通過建構函式無法進行初始化的資料型別可以使用初始化表進行初始化, 如: 常量成員和引用成員, 這部分內容將在後面進行詳細說明。
使用初始化表對物件成員進行初始化的例子:
#include <iostream>

using namespace std;

class Point
{
public:
Point(int x = 0, int y = 0):xPos(x), yPos(y)
{
cout<<"呼叫初始化表對資料成員進行初始化!\n";
}

void printPoint()
{
cout<<"xPos = " << xPos <<endl;
cout<<"yPos = " << yPos <<endl;
}

private:
int xPos;
int yPos;
};

int main()
{
Point M(10, 20); //建立物件M並初始化xPos,yPos為10和20
M.printPoint();

return 0;
}

執行之,參見下圖:

2、解構函式
與建構函式相反, 解構函式是在物件被撤銷時被自動呼叫, 用於對成員撤銷時的一些清理工作, 例如在前面提到的手動釋放使用 new 或 malloc 進行申請的記憶體空間。解構函式具有以下特點:
解構函式函式名與類名相同, 緊貼在名稱前面用波浪號 ~ 與建構函式進行區分, 如: ~Point();
建構函式沒有返回型別, 也不能指定引數, 因此解構函式只能有一個, 不能被過載;
當物件被撤銷時解構函式被自動呼叫, 與建構函式不同的是, 解構函式可以被顯式的呼叫, 以釋放物件中動態申請的記憶體。
C++中的解構函式類似於Python中的 __del__ 方法。
當用戶沒有顯式定義解構函式時, 編譯器同樣會為物件生成一個預設的解構函式, 但預設生成的解構函式只能釋放類的普通資料成員所佔用的空間, 無法釋放通過 new 或 malloc 進行申請的空間, 因此有時我們需要自己顯式的定義解構函式對這些申請的空間進行釋放, 避免造成記憶體洩露。
解構函式的例子
#include <iostream>
#include <cstring>

using namespace std;

class Book
{
public:
Book( const char *name ) //建構函式
{
bookName = new char[strlen(name)+1];
strcpy(bookName, name);
}
~Book() //解構函式
{
cout<<"解構函式被呼叫...\n";
delete []bookName; //釋放通過new申請的空間
}
void showName() { cout<<"Book name: "<< bookName <<endl; }

private:
char *bookName;
};

int main()
{
Book CPP("C++ Primer");
CPP.showName();

return 0;

}

執行之,參見下圖:

說明:
程式碼中建立了一個 Book 類, 類的資料成員只有一個字元指標型的 bookName, 在建立物件時系統會為該指標變數分配它所需記憶體, 但是此時該指標並沒有被初始化所以不會再為其分配其他多餘的記憶體單元。在建構函式中, 我們使用 new 申請了一塊 strlen(name)+1 大小的空間, 也就是比傳入進來的字串長度多1的空間, 目的是讓字元指標 bookName 指向它, 這樣才能正常儲存傳入的字串。
在 main 函式中使用 Book 類建立了一個物件 CPP, 初始化 bookName 屬性為 "C++ Primer"。從執行結果可以看到, 解構函式被呼叫了, 這時使用 new 所申請的空間就會被正常釋放。

自然狀態下物件何時將被銷燬取決於物件的生存週期, 例如全域性物件是在程式執行結束時被銷燬, 自動物件是在離開其作用域時被銷燬。

如果需要顯式呼叫解構函式來釋放物件中動態申請的空間只需要使用 物件名.解構函式名(); 即可, 例如上例中要顯式呼叫解構函式來釋放 bookName 所指向的空間,只要: CPP.~Book();