在物件導向程式設計中,類(Class)和物件(Object)是兩個非常重要和基本的概念,類(Class)包含成員資料和實現行為的函式,當然還提供建構函式來建立物件。如果是一些需要手動釋放記憶體的語言,例如C++,還提供解構函式來幫助釋放記憶體空間;如果是一些有垃圾回收機制的語言,比如Java,就不需要提供解構函式來釋放記憶體,記憶體釋放交給系統來管理。而物件(Object)是類的例項,每次建立一個物件都有不同的識別符號來表示不同的物件,雖然物件中的資料有些是相同的,但它們是否相同根據識別符號來判斷的。
關於成員資料與函式
在C++中,Class有兩個經典的分類:
- Class without pointer member (complex複數類)
- Class with pointer member (string字串類)
一個是類的成員資料不含指標,一個是類的成員資料含指標。complex類來講述成員資料不含指標。
complex類有兩個資料成員:實部和虛部,它們的資料型別都是double,而不是指標;它還定義對複數的基本操作:加、減、乘、除、共軛和正弦等。
string類來講述成員資料含指標。
string類有一個資料成員:字元指標s,它指向一串字元;它還定義對字串的操作:拷貝,輸出,附加,插入等。
Object-Based(基於物件) vs. Object-Oriented(物件導向)
類的設計主要分兩類,基於物件和麵向物件:
- Object-Based:面對的是單個class的設計
- Object-Oriented:面向的是多個classes的設計,class與class之間是有關係的:繼承、組合或委託
大家先了解一下這兩個概念,後面會有詳細介紹。
C++程式碼基本形式
C/C++程式都有一個函式入口:main函式。當執行main函式時,大多數都會用到標準庫(iostream)和自定義的類(complex),所以用檔案包含指令#include <iostream>來包含I/O標準庫,#include “complex.h”來包含自定義類complex。它們之間的語法有一點不同,一個是用尖括號<>來專門包含系統檔案和標準庫,另一個是用雙引號””來包含自定義的類和檔案。
使用預處理中的檔案包含,能夠將一個大檔案分離到各種不同職責類的標頭檔案和實現檔案。這樣不僅減少檔案體積而無需載入無用的程式碼,提供編譯速度;還能夠提高程式碼的複用性。
擴充套件檔名(extension file name)不一定是.h或cpp,有可能是.hpp或其他擴充套件檔名。
Header(標頭檔案)防衛式宣告
標頭檔案經常#include其他標頭檔案,甚至一個標頭檔案可能被多次包含進同一個原始檔。為了避免重複包含,使用大寫的前處理器變數以及其他語句來處理。前處理器變數有兩種狀態:未定義和已定義;定義前處理器變數和檢測其狀態所用的預處理指示不同。
#define指示表示定義一個預處理變數,而ifndef指示檢測前處理器變數是否未定義;如果未定義,那麼跟在其後的所有指示都被處理,如果已經被定義,那麼跟在其後的所有指示會跳過不處理。 部分示例程式碼如下:
complex.h標頭檔案
1 2 3 4 5 6 7 |
#ifndef __MYCOMPLEX__ #define __MYCOMPLEX__ //Class Declaration ...... #endif |
complex-test.c測試檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <iostream> #include "complex.h" using namespace std; ostream& operator << (ostream& os, const complex& x) { return os << '(' << real (x) << ',' << imag (x) << ')'; } int main() { complex c1(2, 1); complex c2(4, 0); cout << c1 << endl; cout << c2 << endl; cout << c1+c2 << endl; cout << c1-c2 << endl; cout << c1*c2 << endl; cout << c1 / 2 << endl; cout << conj(c1) << endl; cout << (c1 += c2) << endl; cout << (c1 == c2) << endl; cout << (c1 != c2) << endl; cout << +c2 << endl; cout << -c2 << endl; cout << (c2 - 2) << endl; cout << (5 + c2) << endl; return 0; } |
Class的宣告
首先給出complex類宣告的程式碼,然後逐步來解析各個部分,示例程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
// forward declarations (前置宣告) class complex; complex& __doapl (complex* ths, const complex& r); // class declarations (類宣告) class complex { public: complex (double r = 0, double i = 0): re (r), im (i) { } complex& operator += (const complex&); complex& operator -= (const complex&); double real () const { return re; } double imag () const { return im; } private: double re, im; friend complex& __doapl (complex *, const complex&); }; // no-member function definition (非成員函式定義) inline complex& __doapl (complex* ths, const complex& r) { ths->re += r.re; ths->im += r.im; return *ths; } // class definition (類定義) // operator overloading (成員函式-操作符過載) inline complex& complex::operator += (const complex& r) { return __doapl (this, r); } // operator overloading(非成員函式-操作符過載) inline double imag (const complex& x) { return x.imag (); } inline double real (const complex& x) { return x.real (); } inline complex operator + (const complex& x, const complex& y) { return complex (real (x) + real (y), imag (x) + imag (y)); } inline complex operator + (const complex& x, double y) { return complex (real (x) + y, imag (x)); } inline complex operator + (double x, const complex& y) { return complex (x + real (y), imag (y)); } |
更加詳細的示例程式碼下載地址:C++物件導向高階程式設計
Access Level(訪問級別)
上面有兩個關鍵字public和private來標明資料成員和成員函式的訪問級別,public表示類的外部能夠訪問類裡面的資料或函式,而private表示類的外部不能訪問類裡面的資料和函式,只允許類內部來訪問;通常使用private來修飾資料成員來封裝資料,不讓類外部的資料輕易訪問。如果類外部的資料想訪問,就定義一些public的accessors方法來暴露給外部介面來訪問。
Constructor(建構函式)
如果你使用類來建立物件並初始化資料成員,就需要定義建構函式。complex類的建構函式定義如下:
1 2 3 4 |
// 使用初始化列表 (推薦使用) complex (double r = 0, double i = 0) : re(r), im(i) {} |
或
1 2 3 4 5 6 |
// 使用函式體 (不推薦使用) complex (double r = 0, double i = 0) { re = r; im = i; } |
在定義建構函式時,需要指定類名complex,資料成員(double r = 0, double i = 0)作為引數和函式體,但並不需要返回值,它還為引數設定預設值(r = 0, i =0)。但有一個問題值得注意:究竟在哪裡初始化資料成員呢?大多數的C++程式設計師都會在建構函式函式體來初始化,但有經驗的C++程式設計師都會使用初始化列表。
從概念上講,建構函式分為兩個階段執行:(1)使用初始化列表來初始化階段;(2)普通的計算階段,也就是建構函式的函式體中所有的語句。雖然complex類這個例子,使用其中一種方式會讓最終效果一樣,但有些情況只能使用初始化列表。看下面這個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; } ConstRef::ConstRef(int ii) { i = ii; // ok ci = ii; // error: 不能給一個const賦值 ri = i; // error: 不能繫結到其他物件,ri已經被初始化過 } |
注意:沒有預設建構函式的類資料成員,以及const或引用型別的成員,不管哪種型別,都必須在建構函式初始化列表中進行初始化。
所以上面那個例子應該改為:
1 2 |
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(ii) {} |
建議:使用建構函式初始化列表,而不是函式體來初始化資料成員。
過載(Overloaded)函式
在設計建構函式建立物件時,可能需要不同引數來建立物件,這時需要過載函式。
過載函式:出現在相同作用域中兩個函式,如果有相同的名字而形參表不同,則稱為過載函式。
就我們這個complex類的建構函式而言,有兩個建構函式:
1 2 3 4 |
complex (double r = 0, double i = 0) : re(r), im(i) {} complex (double r) : re(r), im(0) {} |
第一個是有兩個引數的建構函式,第二個是沒有任何引數的建構函式,雖然它們的函式名相同,但由於它們的引數不同,C++編譯器能夠分辨出兩個不同的函式,從而呼叫對應的建構函式。當使用complex c1(2, 3)建立物件時,對應會呼叫第一個建構函式。而當使用complex c2(2)建立物件時,對應會呼叫第二個建構函式。
當然,過載函式的概念不僅僅是用在建構函式,而應用在所有型別的函式,包括行內函數和普通的函式。
Inline(內聯)函式
對於一些簡單操作,我們有時將它定義為函式,例如:
1 2 3 4 5 |
// find longer of two strings const string& shorterString(const string& s1, const string& s2) { return s1.size() < s2.size() ? s1 : s2; } |
這樣做的話,有幾點好處:
- 使用函式可以確保統一的行為,並可以測試。
- 閱讀和理解函式shorterString的呼叫,要比讀一條用等價的條件表示式取代函式呼叫更加容易理解。
- 如果需要做任何修改,修改函式要比逐條修改條件表示式更加容易。
- 函式可以重用,不必為其他應用重寫程式碼。
但簡短的shorterString函式有個潛在的缺點:就是呼叫函式比求解條件表示式要慢的多,因為呼叫函式一般都要做以下工作:
- 呼叫前要先儲存暫存器,並在返回時恢復
- 複製實參
- 程式轉向一個新位置執行。
行內函數避免函式呼叫的開銷
如果使用行內函數,就可以避免函式呼叫的開銷。編譯器會將行內函數在程式中每個呼叫點“內聯地”展開。假設我們將shorterString定義為行內函數,則呼叫:
1 |
cout << shorterString(s1, s2) << endl; |
在編譯時就會展開為:
1 |
cout << s1.size() < s2.size() ? s1 : s2 << endl; |
行內函數放在標頭檔案
行內函數應該在標頭檔案定義,這一點不同於其他函式,這樣編譯器才能在呼叫點內聯展開函式程式碼。內聯機制適用於只有幾行且經常被呼叫的程式碼,如果程式碼行數或操作太多,即使你使用inline關鍵字來修飾函式,編譯器也不會將它看作為行內函數。
Const(常量)成員函式
每個成員函式都有一個額外的、隱形的形參this,在呼叫成員函式時,形參this初始化為呼叫函式的物件地址。為了理解成員函式的呼叫,請看complex類這個例子:
1 2 |
complex c1 (2, 4); // create object cout << c1.real() << endl; // access const function real |
編譯器就會重寫real函式的呼叫:
1 |
complex::real(&c1); |
在這個呼叫中,在real函式的參數列中,有個this指標指向c1物件。如果在成員函式宣告的形參表後面加入const關鍵字,那麼const改變隱含this形參的型別,即隱含的this形參是一個指向c1物件的const complex*型別指標。因此,real函式對成員變數re所做操作是隻能訪問,而不能修改。同理,imag成員函式也是。
引數傳遞: pass by value vs. pass by reference
每次呼叫函式時,所傳遞的實參將會初始化對應的形參;引數傳遞有兩種方式:一種是值傳遞,另一種就是引用傳遞。如果形參是使用值傳遞,那麼複製實參的值;如果形參是引用傳遞,那麼它只是實參的別名。看complex類這個例子中定義一個函式:
1 |
complex& operator+= (const complex& ); |
將&符號放在complex類後面,則表示呼叫函式式是使用引用傳遞來傳遞資料。為什麼使用引用傳遞而不使用值傳遞呢?
值傳遞的侷限性
- 當需要在函式中修改實參的值時
- 當傳遞的實參是大型物件時,複製物件所付出的時間和儲存空間代價比較大
- 當沒有辦法實現物件複製時
引數傳遞選擇
- 優先考慮引用傳遞(const),避免複製
- 當在函式中處理後的結果是使用區域性變數來儲存,而不是形參的引用引數,使用值傳遞來返回。
Friend(友元)
在某些情況下,允許特定的非成員函式訪問一個類的私有成員,同時仍然阻止一般的訪問。例如,被過載的操作符,如輸入或輸出操作符,經常需要訪問類的私有資料成員,這些操作不可能為類的成員;然而,儘管不是類的成員,它們仍是類的“介面組成部分”。
友元機制允許一個類將對其非公有成員的訪問權授予指定的函式或類。友元的宣告以關鍵字friend開始,它只能出現在類定義的內部。
以complex類為例,它有一個友元函式__doapl:
1 |
friend complex& __doapl (complex*, const complex&); |
由於它引數是complex類,在函式內部需要訪問到complex類的私有資料re和im,雖然可以通過real()和imag()函式來訪問,但是如果直接訪問re和im兩個資料成員,就能提高程式執行速度。
重要提示: 相同class的各個objects互為friends(友元)
(Operator Overloading)操作符過載
C語言的操作符只能應用在基本資料型別,例如:整形、浮點型等。但C++的基本組成單元是類,如果對類的物件也能進行加減乘除等操作符運算,那麼操作起來比呼叫函式更加方便。因此C++提供操作符過載來支援類與類之間也能使用操作符來運算。
操作符過載是具有特殊名稱的函式:關鍵字operator後接需要定義的操作符符號。像任意其他函式一樣,操作符過載具有返回值和形參表。
如果想操作符過載,有兩種選擇:
- 成員函式的操作符過載
- 非成員函式的操作符過載
兩者之間有什麼不同呢?對於成員函式的操作符過載,每次呼叫成員函式時,都會有一個隱含this形參,限定為第一個運算元,而this指標的資料型別是固定的,就是該類型別。而非成員函式的操作符過載,形參表比成員函式靈活,第一個形參不再限死為this形參,而是可以是其他型別的形參。下面我們分別通過兩個例子來看看為什麼這樣選擇。
成員函式的操作符過載
1 2 3 4 |
complex c1(2, 1); complex c2 (5); c2 += c1; |
上面程式碼建立兩個complex物件c1和c2,然後使用+=操作符來進行相加賦值操作。我們站在設計API角度來思考,如果過載操作符+=的話,需要提供兩個引數(complex&, complex&),但由於第一個引數型別是complex&跟成員函式this形參一樣,所以優先考慮成員函式。
complex類過載+=操作符:
1 |
complex& operator += (const complex&); |
而程式碼實現放在類宣告外面:
1 2 3 4 5 |
inline complex& complex::operator += (const complex& r) { return __doapl (this, r); } |
非成員函式的操作符過載
1 2 3 4 5 6 |
complex c1(2, 1); complex c2; c2 = c1 + c2; c2 = c1 + 5; c2 = 7 + c1; |
上面程式碼建立兩個complex物件c1和c2,然後使用+操作符進行相加操作。
其中有一個c2 = 7 + c1程式碼片段,第一運算元是double,而不是complex。所以如果還是使用成員函式的話,編譯器會報錯,因為成員函式的第一個形參型別是complex而不是double。最後我們選擇的是使用非成員函式來實現,而不是成員函式。
非成員函式過載+操作符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
inline double imag (const complex& x) { return x.imag (); } inline double real (const complex& x) { return x.real (); } inline complex operator + (const complex& x, const complex& y) { return complex (real (x) + real (y), imag (x) + imag (y)); } inline complex operator + (const complex& x, double y) { return complex (real (x) + y, imag (x)); } inline complex operator + (double x, const complex& y) { return complex (x + real (y), imag (y)); } |
Temp Object(臨時物件)
1 2 3 4 5 |
inline complex operator + (const complex& x, const complex& y) { return complex (real (x) + real (y), imag (x) + imag (y)); } |
上面用非成員函式實現+操作符過載時,計算後結果沒有使用引用形參來儲存,而是使用一種特殊物件叫臨時物件來儲存,它是一個區域性變數。語法是typename(data),typename表示型別,data表示傳入的資料。
總結
當設計一個C++類的時候,需要思考一下問題:
- 首先要考慮它是基於物件(單個類)還是物件導向(多個類)的設計
- 類由資料成員和成員函式組成;一般來說,資料成員的訪問許可權應該設定為private,以防止類的外部隨意訪問修改資料。如果類的外部想訪問資料,類可以定義資料成員的setter和getter。由於getter是不會改變資料成員的值,所以用const關鍵字修飾函式,防止getter函式修改資料
- 考慮完資料成員之後,然後考慮函式的設計。要建立物件,需要在類中定義建構函式。建構函式的引數一般是所有的私有資料成員,而要初始化資料成員,一般採用初始化列表,而不使用建構函式的函式體。
- 而對於一般的函式,在引數設計中,除了考慮變數名和資料型別之外,還要考慮引數傳遞、是否使用const修飾和有沒有預設值等,引數傳遞優先考慮引用傳遞(避免複製開銷),而不是值傳遞,返回值也是一樣。當在函式體內處理完結果之後,沒使用引用形參來儲存結果的話,可以使用臨時物件儲存並返回結果。有些函式實現只有幾個操作的簡短程式碼,將實現程式碼放在標頭檔案,設定函式為inline。
- 當過載操作符時,可以使用兩種方式來實現:成員函式和非成員函式。當第一個運算元是固定的類型別,優先使用成員函式,否則就使用非成員函式。
暫時總結這麼多,後續還有其他C++物件導向程式設計的總結,會繼續補充!!!