C++基礎學習之物件和類(6)
物件和類
主要內容:
- 面向物件程式設計與過程性程式設計
- 類的定義和實現
- 公有類和私有類
- 類的資料成員
- 類方法
- 建立和使用類物件
- 類的建構函式和解構函式
- const成員函式
- this指標
- 建立物件陣列
- 類作用域
- 抽象資料型別
面對物件程式設計(oop)的特性:
- 抽象
- 封裝和資料隱藏
- 多型
- 繼承
- 程式碼的可重用性
面對物件程式設計
採用OOP的方法,首先從使用者的角度考慮物件——描述物件所需的資料以及描述使用者與資料互動所需的操作。完成對介面的描述後,需要確定如何實現介面和資料儲存。最後,使用新的設計方案創建出程式。
抽象和類
在計算中,為了根據資訊與使用者之間的介面來表示它,抽象是至關重要的。也就是說,將問題的本質特徵抽象出來,並根據特徵來描述解決方案。抽象是通往使用者定義型別的捷徑,在C++中,使用者定義型別指的是實現抽象介面的類設計。
型別
指定基本型別完成了三項工作:
- 決定資料物件需要的記憶體數量;
- 決定如何解釋記憶體中的位(long和float在記憶體中佔用的位數相同,但將它們轉換為數值的方法不同);
- 決定可使用資料物件執行的操作或方法。
C++中的類
類是一種將抽象轉換為使用者定義型別的C++工具,它將資料表示和操縱資料的方法組合成一個整潔的包。例如一個股票的類。
可對股票進行的操作:1.獲得股票 2.增持 3.賣出股票 4.更新股票價格 5.顯示關於所持股票的資訊。
儲存的股票資訊:1.公司名稱 2.所持股票的數量 3.每股的價格 4.股票總值。
然後就可以定義類了,一般來說,類規範由兩個部分組成:
1.類宣告:以資料成員的方式描述資料部分,以成員函式(被稱為方法)的方式描述公有介面。
2.類方法定義:描述如何實現類成員函式。
簡單的說,類宣告提供了類的藍圖,而方法定義則提供了細節。
為開發一個類並編寫使用它的程式,需要完成多個步驟。通常,C++程式設計師將介面(類定義)放在標頭檔案中,並將實現(類方法的程式碼)放在原始碼檔案中。
常用的方法是將類名首字母大寫。下面是一個類定義的例子:
// stock00.h -- Stock class interface
// version 00
#ifdef STOCK00_H_
#define STOCK00_H_
#include <string>
class Stock // class declaration
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() {total_val = shares * share_val; }
public:
void acquire(const std::string & co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
}; // note semicolon at the end
#endif
建立兩個物件(例項):
Stock sally;
stock solly;
1.訪問控制
關鍵字private和public描述了對類成員的訪問控制。使用類物件的程式都可以直接訪問公有部分,但只能通過公有成員函式(或友元函式)來訪問物件的私有成員。例如,要修改Stock類的shares成員,只能通過Stock的成員函式。因此,公有成員函式是程式和物件的私有成員之間的橋樑,提供了物件和程式之間的介面。防止程式直接訪問資料被稱為資料隱藏。C++還提供了第三個訪問控制關鍵字protected。
類設計儘可能將公有介面與實現細節分開。公有介面表示設計的抽象元件。將實現細節放在一起並將它們與抽象分開被稱為封裝。資料隱藏(將資料放在類的私有部分中)是一種封裝,將實現的細節隱藏在私有部分中,就像Stock類對set_tot()所做的那樣,也是一種封裝。封裝的另一個例子是,將類函式定義和類宣告放在不同的檔案中。
資料隱藏不僅可以防止直接訪問資料,還讓開發者(類的使用者)無需瞭解資料是如何被表示的。我們只需要知道成員函式接受什麼樣的引數以及返回什麼樣型別的值。原則是將實現細節從介面設計中分離出來。如果以後找到了更好的、實現資料表示或成員函式細節的方法,可以對這些細節進行修改,而無需修改程式介面,這使程式維護起來更容易。
2.控制對成員的訪問:公有還是私有
無論類成員是資料成員還是成員函式,都可以在類的共有部分或私有部分中宣告它。但由於隱藏資料是OOP主要的目標之一,因此資料項通常放在私有部分,組成類介面的成員函式放在公有部分;否則,就無法從程式中呼叫這些函式。正如Stock宣告所表明的,也可以把成員函式放在私有部分中。不能直接從程式中呼叫這種函式,但公有方法卻可以使用它們。通常,程式設計師使用私有成員函式來處理不屬於公有介面的實現細節。
不必在類宣告中使用關鍵字private,因為這是類物件的預設訪問控制:
class World
{
float mass; // private by default
char name[20]; // private by default
public:
void tellall(void);
...
}
但是有時為了強調資料隱藏的概念,也會顯示使用private。
實現類成員函式
還需要建立類描述的第二部分:為那些由類宣告中的原型表示的成員函式提供程式碼。成員函式定義與常規函式定義非常相似,它們有函式頭和函式體,也可以有返回型別和引數。但是它們還有兩個特殊的特徵:
- 定義成員函式時,使用作用域解析運算子(::)來標識函式所屬的類;
- 類方法可以訪問類的private元件。
首先,成員函式的函式頭使用作用域運算子解析(::)來指出函式所屬的類。例如,update()成員函式的函式頭如下:
void Stock::update(double price)
這種表示法意味著我們定義的update()函式時Stock類的成員。這不僅將update()標識為成員函式,還以為這我們可以將另一個類的成員函式也命名為update()。作用域解析運算子確定了方法定義對應的類的身份。我們說,識別符號update()具有類作用域,Stock類的其它成員函式不必使用作用域解析運算子,就可以使用update()方法,這是因為它們屬於用一個類,因此update()是可見的。然而,在類宣告和方法定義之外使用update()時,需要採取特殊的措施。
下面是類方法的實現:
// stock00.cpp -- impementing the Stock class
// version 00
#include <iostream>
#include "stock00.h"
void Stock::acquire(const str::string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative;"
<< company << "shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
void Stock::buy(long num, double price)
{
...
}
void Stock::sell(long num, double price)
{
...
}
void Stock::update(double price)
{
...
}
void Stock::show()
{
...
}
內聯方法:
其定義位於類宣告中的函式都將自動成為行內函數,因此Stock::set_tot()是一個行內函數。類宣告常將短小的成員函式作為行內函數。如果願意,也可以在類宣告之外定義成員函式,並使其成為行內函數。為此,只需要在類實現部分中定義函式時使用inline限定符即可:
class Stock
{
private:
...
void set_tot();
public:
...
}
inline void Stock::set_tot()
{
total_val = shares * share_val;
}
方法使用哪個物件
如何將類方法應用於物件。首先看看如何建立物件:
Stock kate, joe;
這將建立兩個Stock類物件,一個為kate,一個為joe。
使用物件的成員函式:
kate.show(); // the kate object calls the member function
joe.show(); // the joe object calls the member function
第一條語句呼叫kate物件的show()成員,這意味著show()方法將把shares解釋為kate.shares,將share_val解釋為kate.share_val。同樣,函式呼叫joe.show()使show()方法將shares和share_val分別解釋為joe.share和joe.share_val。
注意: 呼叫成員函式時,它將使用被用來呼叫它的物件的資料成員。
所建立的每個新物件都有自己的儲存空間,用於儲存其內部變數和類成員;但同一個類的所有物件共享一組類方法,即每種方法只有一個副本。例如,假設kate和joe都是Stock物件,則kate.shares將佔據一個記憶體塊,而joe.shares佔用另一個記憶體塊,但kate.show()和joe.show()都呼叫同一個方法,也就是說,它們將執行同一個程式碼塊,只是將這些程式碼塊用於不同的資料。在OOP中,呼叫成員函式被稱為傳送訊息,因此將同樣的訊息傳送給兩個不同的物件將呼叫同一個方法,但該方法被用於兩個不同的物件。
使用類
知道如何定義類及其方法後,建立一個程式來使用類,它建立並使用類物件。C++的目標是使得使用類與使用基本的內建型別(如int和char)儘可能相同。要建立類物件,可以宣告類變數,也可以使用new為類物件分配儲存空間。可以將物件作為函式的引數和返回值,也可以將一個物件賦給另一個。C++提供了一些工具,可用於初始化物件、讓cin和cout識別物件,甚至在相似的類物件之間進行自動型別轉換。
下面時一個類使用的示例程式:
// usestock0.cpp -- the client program
// compile with stock00.cpp
#include <iostream>
#include "stock00.h"
int main()
{
Stock fluffy_the_cat;
fluffy_the_cat.acquire("NanoSmart", 20, 12.50);
fluffy_the_cat.show();
fluffy_the_cat.buy(15, 18.125);
fluffy_the_cat.show();
fluffy_the_cat.sell(400, 20.00);
fluffy_the_cat.show();
fluffy_the_cat.buy(300000, 40.125);
fluffy_the_cat.show();
fluffy_the_cat.sell(300000, 0.125);
fluffy_the_cat.show();
return 0;
}
要使用新型別,最關鍵的是要了解成員函式的功能,而不必考慮其實現細節。
修改類時一定要注意避免修改介面,只對成員函式內部進行修改,不要修改介面定義。
類的建構函式和解構函式
對於Stock類,還有一些其它工作要做。應為類提供被稱為建構函式和解構函式的標準函式。
C++的目標之一就是讓使用類物件就像使用標準型別一樣,所以還缺少一個對類物件進行初始化的過程,而常規的初始化語法不適用於類,因為類的資料部分的訪問狀態是私有的,所以程式不能直接訪問資料成員。程式只能通過成員函式來訪問資料成員,因此需要設計合適的成員函式,才能成功的將物件初始化。
一般來說,最好是在建立物件時對它進行初始化。C++提供了一個特殊的成員函式——類建構函式,專門用於構造新物件、將值賦給它們的資料成員。更準確的說,C++為這個成員函式提供了名稱和使用語法,而程式設計師需要提供方法定義。名稱與類名相同。例如,Stock類一個可能的建構函式是名為Stock()的成員函式。建構函式的原型和函式頭有一個有趣的特徵——雖然沒有返回值,但沒有被宣告為void型別。實際上,建構函式沒有宣告型別。
宣告和定義建構函式
下面建立Stock的建構函式。由於需要為Stock物件提供3個值,因此應為建構函式提供3個引數。(第4個值,total_val成員,是根據shares和share_val計算得到,所以不需要)程式設計師可能只想設定company成員,而將其它值設定為0;這可以使用預設引數來完成。因此原型如下:
// constructor prototype with some default arguments
Stock(const string & co, long n = 0, double pr = 0.0);
下面個是建構函式的另一種可能定義:
// constructor definition
Stock::Stock(const string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cerr << "Numer of share can't be negative;"
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
上述程式碼和前面的acquire()函式相同。區別在於,程式宣告物件時,將自動呼叫建構函式。
注意:
不可以將類成員名稱作為建構函式的引數名,為了避免這種混亂,一種常見的做法是在資料成員名前使用m_字首:
class Stock
{
private:
string m_company;
long m_shares;
...
}
另一種常見的做法是,在成員名種使用字尾_:
class Stock
{
private:
string company_;
long shares_;
...
}
使用建構函式
C++提供了兩種使用建構函式來初始化物件的方式。第一種方式是顯式的呼叫建構函式:
Stock food = Stock("World Cabbage", 250, 1.25);
另一種是隱式的呼叫建構函式:
Stock garment("Furry Mason", 50, 2.5);
這種格式更緊湊,它與下面的顯示呼叫等價:
Stock garment = Stock("Furry Mason",50, 2.5 );
每次建立類物件(甚至使用new動態分配記憶體)時,C++都使用類建構函式。下面是將建構函式與new一起使用的方法:
Stock *pstock = Stock("Electroshock Games", 18, 19.0);
這條語句建立一個Stock物件,將其初始化為引數提供的值,並將該物件的地址賦給pstock指標。在這種情況下,物件沒有名稱,但可以使用指標來管理該物件。
建構函式的使用方式不同於其它類方法,它無法被物件呼叫,因為在建構函式構造出物件之前,物件是不存在的。因此建構函式被用來建立物件, 而不能通過物件來呼叫。
預設建構函式
預設建構函式是在未提供顯式初始值時,用來建立物件的建構函式,也就是說,它是用於下面這種宣告的建構函式:
Stock fluffy_the_cat; // use the default constructor
也就是說,如果沒有提供任何建構函式,則C++將自動提供預設建構函式。定義預設建構函式有兩種方法。
第一種:給已有建構函式的所有引數提供預設值:
Stock(const string & co = "Error", int n = 0, double pr = 0.0);
第二種:通過函式過載來定義另一個建構函式——一個沒有任何引數的建構函式:
Stock();
// definition
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
宣告物件時有三種形式使用預設建構函式:
1. Stock first; // calls the default constructor implicity
2. Stock first = Stock(); // call it explicity
3. Stock *prelief = new Stock; // calls it implicity
解構函式
用建構函式建立物件後,程式設計師負責跟蹤該物件,知道其過期為止。物件過期時,程式將自動呼叫一個特殊的成員函式——解構函式。解構函式的名稱是在類名前加上~。因此,Stock類的解構函式為~Stock()。另外,和建構函式一樣,解構函式也可以沒有返回值和宣告型別。與建構函式不同的是,解構函式沒有引數,因此Stock解構函式的原型必須是這樣的:
~Stock();
由於Stock的解構函式不承擔任何重要的工作,因此可以將它編寫為不執行任何操作的函式:
Stock::~Stock()
{
}
如果程式設計師沒有提供解構函式,編譯器將隱式的宣告一個預設解構函式,並在發現導致物件被刪除的程式碼後,提供預設解構函式的定義。
如果建構函式使用了new,那麼必須提供使用delete的解構函式。