在面向?qū)ο缶幊讨校?a target="_blank" rel="nofollow">類(Class)和對象(Object)是兩個非常重要和基本的概念,類(Class)包含成員數(shù)據(jù)和實(shí)現(xiàn)行為的函數(shù),當(dāng)然還提供構(gòu)造函數(shù)來創(chuàng)建對象。如果是一些需要手動釋放內(nèi)存的語言,例如C++,還提供析構(gòu)函數(shù)來幫助釋放內(nèi)存空間;如果是一些有垃圾回收機(jī)制的語言,比如Java,就不需要提供析構(gòu)函數(shù)來釋放內(nèi)存,內(nèi)存釋放交給系統(tǒng)來管理。而對象(Object)是類的實(shí)例,每次創(chuàng)建一個對象都有不同的標(biāo)識符來表示不同的對象,雖然對象中的數(shù)據(jù)有些是相同的,但它們是否相同根據(jù)標(biāo)識符來判斷的。
關(guān)于數(shù)據(jù)成員與函數(shù)
在C++中,Class有兩個經(jīng)典的分類:
- Class without pointer member (complex復(fù)數(shù)類)
- Class with pointer member (string字符串類)
一個是類的數(shù)據(jù)成員不含指針,一個是類的數(shù)據(jù)成員含指針。complex類來講述數(shù)據(jù)成員不含指針。

complex類有兩個數(shù)據(jù)成員:實(shí)部和虛部,它們的數(shù)據(jù)類型都是double,而不是指針;它還定義對復(fù)數(shù)的基本操作:加、減、乘、除、共軛和正弦等。
string類來講述成員數(shù)據(jù)含指針。

string類有一個數(shù)據(jù)成員:字符指針s,它指向一串字符;它還定義對字符串的操作:拷貝,輸出,附加,插入等。
Object-Based(基于對象) vs. Object-Oriented(面向?qū)ο?
類的設(shè)計主要分兩類,基于對象和面向?qū)ο螅?/p>
- Object-Based:面對的是單個class的設(shè)計
- Object-Oriented:面向的是多個classes的設(shè)計,class與class之間是有關(guān)系的:繼承、組合或委托
大家先了解一下這兩個概念,后面會有詳細(xì)介紹。
C++代碼基本形式
C/C++程序都有一個函數(shù)入口:main函數(shù)。當(dāng)執(zhí)行main函數(shù)時,大多數(shù)都會用到標(biāo)準(zhǔn)庫(iostream)和自定義的類(complex),所以用文件包含指令#include <iostream>來包含I/O標(biāo)準(zhǔn)庫,#include "complex.h"來包含自定義類complex。它們之間的語法有一點(diǎn)不同,一個是用尖括號<>來專門包含系統(tǒng)文件和標(biāo)準(zhǔn)庫,另一個是用雙引號""來包含自定義的類和文件。
使用預(yù)處理中的文件包含,能夠?qū)⒁粋€大文件分離到各種不同職責(zé)類的頭文件和實(shí)現(xiàn)文件。這樣不僅減少文件體積而無需加載無用的代碼,提供編譯速度;還能夠提高代碼的復(fù)用性。

擴(kuò)展文件名(extension file name)不一定是.h或cpp,有可能是.hpp或其他擴(kuò)展文件名。
Header(頭文件)防衛(wèi)式聲明
頭文件經(jīng)常#include其他頭文件,甚至一個頭文件可能被多次包含進(jìn)同一個源文件。為了避免重復(fù)包含,使用大寫的預(yù)處理器變量以及其他語句來處理。預(yù)處理器變量有兩種狀態(tài):未定義和已定義;定義預(yù)處理器變量和檢測其狀態(tài)所用的預(yù)處理指示不同。
#define指示表示定義一個預(yù)處理變量,而ifndef指示檢測預(yù)處理器變量是否未定義;如果未定義,那么跟在其后的所有指示都被處理,如果已經(jīng)被定義,那么跟在其后的所有指示會跳過不處理。 部分示例代碼如下:
complex.h頭文件
#ifndef __MYCOMPLEX__
#define __MYCOMPLEX__
//Class Declaration
......
#endif
complex-test.c測試文件
#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類聲明的代碼,然后逐步來解析各個部分,示例代碼如下:
// 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 (非成員函數(shù)定義)
inline complex&
__doapl (complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}
// class definition (類定義)
// operator overloading (成員函數(shù)-操作符重載)
inline complex&
complex::operator += (const complex& r)
{
return __doapl (this, r);
}
// operator overloading(非成員函數(shù)-操作符重載)
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));
}
更加詳細(xì)的示例代碼下載地址:C++面向?qū)ο蟾呒壘幊?/a>
Access Level(訪問級別)
上面有兩個關(guān)鍵字public和private來標(biāo)明數(shù)據(jù)成員和成員函數(shù)的訪問級別,public表示類的外部能夠訪問類里面的數(shù)據(jù)或函數(shù),而private表示類的外部不能訪問類里面的數(shù)據(jù)和函數(shù),只允許類內(nèi)部來訪問;通常使用private來修飾數(shù)據(jù)成員來封裝數(shù)據(jù),不讓類外部的數(shù)據(jù)輕易訪問。如果類外部的數(shù)據(jù)想訪問,就定義一些public的accessors方法來暴露給外部接口來訪問。
Constructor(構(gòu)造函數(shù))
如果你使用類來創(chuàng)建對象并初始化數(shù)據(jù)成員,就需要定義構(gòu)造函數(shù)。complex類的構(gòu)造函數(shù)定義如下:
// 使用初始化列表 (推薦使用)
complex (double r = 0, double i = 0)
: re(r), im(i)
{}
或
// 使用函數(shù)體 (不推薦使用)
complex (double r = 0, double i = 0)
{
re = r;
im = i;
}
在定義構(gòu)造函數(shù)時,需要指定類名complex,數(shù)據(jù)成員(double r = 0, double i = 0)作為參數(shù)和函數(shù)體,但并不需要返回值,它還為參數(shù)設(shè)置默認(rèn)值(r = 0, i =0)。但有一個問題值得注意:究竟在哪里初始化數(shù)據(jù)成員呢?大多數(shù)的C++程序員都會在構(gòu)造函數(shù)函數(shù)體來初始化,但有經(jīng)驗(yàn)的C++程序員都會使用初始化列表。
從概念上講,構(gòu)造函數(shù)分為兩個階段執(zhí)行:(1)使用初始化列表來初始化階段;(2)普通的計算階段,也就是構(gòu)造函數(shù)的函數(shù)體中所有的語句。雖然complex類這個例子,使用其中一種方式會讓最終效果一樣,但有些情況只能使用初始化列表??聪旅孢@個例子:
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已經(jīng)被初始化過
}
注意:沒有默認(rèn)構(gòu)造函數(shù)的類數(shù)據(jù)成員,以及const或引用類型的成員,不管哪種類型,都必須在構(gòu)造函數(shù)初始化列表中進(jìn)行初始化。
所以上面那個例子應(yīng)該改為:
ConstRef::ConstRef(int ii)
: i(ii), ci(ii), ri(ii) {}
建議:使用構(gòu)造函數(shù)初始化列表,而不是函數(shù)體來初始化數(shù)據(jù)成員。
重載(Overloaded)函數(shù)
在設(shè)計構(gòu)造函數(shù)創(chuàng)建對象時,可能需要不同參數(shù)來創(chuàng)建對象,這時需要重載函數(shù)。
重載函數(shù):出現(xiàn)在相同作用域中兩個函數(shù),如果有相同的名字而形參表不同,則稱為重載函數(shù)。
就我們這個complex類的構(gòu)造函數(shù)而言,有兩個構(gòu)造函數(shù):
complex (double r, double i)
: re(r), im(i) {}
complex (double r) : re(r), im(0) {}
第一個是有兩個參數(shù)的構(gòu)造函數(shù),第二個是只有一個參數(shù)的構(gòu)造函數(shù),雖然它們的函數(shù)名相同,但由于它們的參數(shù)不同,C++編譯器能夠分辨出兩個不同的函數(shù),從而調(diào)用對應(yīng)的構(gòu)造函數(shù)。當(dāng)使用complex c1(2, 3)創(chuàng)建對象時,對應(yīng)會調(diào)用第一個構(gòu)造函數(shù)。而當(dāng)使用complex c2(2)創(chuàng)建對象時,對應(yīng)會調(diào)用第二個構(gòu)造函數(shù)。
當(dāng)然,重載函數(shù)的概念不僅僅是用在構(gòu)造函數(shù),而應(yīng)用在所有類型的函數(shù),包括內(nèi)聯(lián)函數(shù)和普通的函數(shù)。
Inline(內(nèi)聯(lián))函數(shù)
對于一些簡單操作,我們有時將它定義為函數(shù),例如:
// find longer of two strings
const string& shorterString(const string& s1, const string& s2)
{
return s1.size() < s2.size() ? s1 : s2;
}
這樣做的話,有幾點(diǎn)好處:
- 使用函數(shù)可以確保統(tǒng)一的行為,并可以測試。
- 閱讀和理解函數(shù)
shorterString的調(diào)用,要比讀一條用等價的條件表達(dá)式取代函數(shù)調(diào)用更加容易理解。 - 如果需要做任何修改,修改函數(shù)要比逐條修改條件表達(dá)式更加容易。
- 函數(shù)可以重用,不必為其他應(yīng)用重寫代碼。
但簡短的shorterString函數(shù)有個潛在的缺點(diǎn):就是調(diào)用函數(shù)比求解條件表達(dá)式要慢的多,因?yàn)檎{(diào)用函數(shù)一般都要做以下工作:
- 調(diào)用前要先保存寄存器,并在返回時恢復(fù)
- 復(fù)制實(shí)參
- 程序轉(zhuǎn)向一個新位置執(zhí)行。
內(nèi)聯(lián)函數(shù)避免函數(shù)調(diào)用的開銷
如果使用內(nèi)聯(lián)函數(shù),就可以避免函數(shù)調(diào)用的開銷。編譯器會將內(nèi)聯(lián)函數(shù)在程序中每個調(diào)用點(diǎn)“內(nèi)聯(lián)地”展開。假設(shè)我們將shorterString定義為內(nèi)聯(lián)函數(shù),則調(diào)用:
cout << shorterString(s1, s2) << endl;
在編譯時就會展開為:
cout << s1.size() < s2.size() ? s1 : s2 << endl;
內(nèi)聯(lián)函數(shù)放在頭文件
內(nèi)聯(lián)函數(shù)應(yīng)該在頭文件定義,這一點(diǎn)不同于其他函數(shù),這樣編譯器才能在調(diào)用點(diǎn)內(nèi)聯(lián)展開函數(shù)代碼。內(nèi)聯(lián)機(jī)制適用于只有幾行且經(jīng)常被調(diào)用的代碼,如果代碼行數(shù)或操作太多,即使你使用inline關(guān)鍵字來修飾函數(shù),編譯器也不會將它看作為內(nèi)聯(lián)函數(shù)。
Const(常量)成員函數(shù)
每個成員函數(shù)都有一個額外的、隱形的形參this,在調(diào)用成員函數(shù)時,形參this初始化為調(diào)用函數(shù)的對象地址。為了理解成員函數(shù)的調(diào)用,請看complex類這個例子:
complex c1 (2, 4); // create object
cout << c1.real() << endl; // access const function real
編譯器就會重寫real函數(shù)的調(diào)用:
complex::real(&c1);
在這個調(diào)用中,在real函數(shù)的參數(shù)表中,有個this指針指向c1對象。如果在成員函數(shù)聲明的形參表后面加入const關(guān)鍵字,那么const改變隱含this形參的類型,即隱含的this形參是一個指向c1對象的const complex*類型指針。因此,real函數(shù)對成員變量re所做操作是只能訪問,而不能修改。同理,imag成員函數(shù)也是。
參數(shù)傳遞: pass by value vs. pass by reference
每次調(diào)用函數(shù)時,所傳遞的實(shí)參將會初始化對應(yīng)的形參;參數(shù)傳遞有兩種方式:一種是值傳遞,另一種就是引用傳遞。如果形參是使用值傳遞,那么復(fù)制實(shí)參的值;如果形參是引用傳遞,那么它只是實(shí)參的別名???code>complex類這個例子中定義一個函數(shù):
complex& operator+= (const complex& );
將&符號放在complex類后面,則表示調(diào)用函數(shù)式是使用引用傳遞來傳遞數(shù)據(jù)。為什么使用引用傳遞而不使用值傳遞呢?
值傳遞的局限性
- 當(dāng)需要在函數(shù)中修改實(shí)參的值時
- 當(dāng)傳遞的實(shí)參是大型對象時,復(fù)制對象所付出的時間和存儲空間代價比較大
- 當(dāng)沒有辦法實(shí)現(xiàn)對象復(fù)制時
參數(shù)傳遞選擇
- 優(yōu)先考慮引用傳遞(const),避免復(fù)制
- 當(dāng)在函數(shù)中處理后的結(jié)果是使用局部變量來存儲,而不是形參的引用參數(shù),使用值傳遞來返回。
Friend(友元)
在某些情況下,允許特定的非成員函數(shù)訪問一個類的私有成員,同時仍然阻止一般的訪問。例如,被重載的操作符,如輸入或輸出操作符,經(jīng)常需要訪問類的私有數(shù)據(jù)成員,這些操作不可能為類的成員;然而,盡管不是類的成員,它們?nèi)允穷惖摹敖涌诮M成部分”。
友元機(jī)制允許一個類將對其非公有成員的訪問權(quán)授予指定的函數(shù)或類。友元的聲明以關(guān)鍵字friend開始,它只能出現(xiàn)在類定義的內(nèi)部。
以complex類為例,它有一個友元函數(shù)__doapl:
friend complex& __doapl (complex*, const complex&);
由于它參數(shù)是complex類,在函數(shù)內(nèi)部需要訪問到complex類的私有數(shù)據(jù)re和im,雖然可以通過real()和imag()函數(shù)來訪問,但是如果直接訪問re和im兩個數(shù)據(jù)成員,就能提高程序運(yùn)行速度。
重要提示: 相同class的各個objects互為friends(友元)
(Operator Overloading)操作符重載
C語言的操作符只能應(yīng)用在基本數(shù)據(jù)類型,例如:整形、浮點(diǎn)型等。但C++的基本組成單元是類,如果對類的對象也能進(jìn)行加減乘除等操作符運(yùn)算,那么操作起來比調(diào)用函數(shù)更加方便。因此C++提供操作符重載來支持類與類之間也能使用操作符來運(yùn)算。
操作符重載是具有特殊名稱的函數(shù):關(guān)鍵字
operator后接需要定義的操作符符號。像任意其他函數(shù)一樣,操作符重載具有返回值和形參表。
如果想操作符重載,有兩種選擇:
- 成員函數(shù)的操作符重載
- 非成員函數(shù)的操作符重載
兩者之間有什么不同呢?對于成員函數(shù)的操作符重載,每次調(diào)用成員函數(shù)時,都會有一個隱含this形參,限定為第一個操作數(shù),而this指針的數(shù)據(jù)類型是固定的,就是該類類型。而非成員函數(shù)的操作符重載,形參表比成員函數(shù)靈活,第一個形參不再限死為this形參,而是可以是其他類型的形參。下面我們分別通過兩個例子來看看為什么這樣選擇。
成員函數(shù)的操作符重載
complex c1(2, 1);
complex c2(5);
c2 += c1;
上面代碼創(chuàng)建兩個complex對象c1和c2,然后使用+=操作符來進(jìn)行相加賦值操作。我們站在設(shè)計API角度來思考,如果重載操作符+=的話,需要提供兩個參數(shù)(complex&, complex&),但由于第一個參數(shù)類型是complex&跟成員函數(shù)this形參一樣,所以優(yōu)先考慮成員函數(shù)。
complex類重載+=操作符:
complex& operator += (const complex&);
而代碼實(shí)現(xiàn)放在類聲明外面:
inline complex&
complex::operator += (const complex& r)
{
return __doapl (this, r);
}
非成員函數(shù)的操作符重載
complex c1(2, 1);
complex c2;
c2 = c1 + c2;
c2 = c1 + 5;
c2 = 7 + c1;
上面代碼創(chuàng)建兩個complex對象c1和c2,然后使用+操作符進(jìn)行相加操作。
其中有一個c2 = 7 + c1代碼片段,第一操作數(shù)是double,而不是complex。所以如果還是使用成員函數(shù)的話,編譯器會報錯,因?yàn)槌蓡T函數(shù)的第一個形參類型是complex而不是double。最后我們選擇的是使用非成員函數(shù)來實(shí)現(xiàn),而不是成員函數(shù)。
非成員函數(shù)重載+操作符:
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(臨時對象)
inline complex
operator + (const complex& x, const complex& y)
{
return complex (real (x) + real (y), imag (x) + imag (y));
}
上面用非成員函數(shù)實(shí)現(xiàn)+操作符重載時,計算后結(jié)果沒有使用引用形參來保存,而是使用一種特殊對象叫臨時對象來保存,它是一個局部變量。語法是typename(data),typename表示類型,data表示傳入的數(shù)據(jù)。
總結(jié)
當(dāng)設(shè)計一個C++類的時候,需要思考一下問題:
- 首先要考慮它是基于對象(單個類)還是面向?qū)ο?/strong>(多個類)的設(shè)計
- 類由數(shù)據(jù)成員和成員函數(shù)組成;一般來說,數(shù)據(jù)成員的訪問權(quán)限應(yīng)該設(shè)置為
private,以防止類的外部隨意訪問修改數(shù)據(jù)。如果類的外部想訪問數(shù)據(jù),類可以定義數(shù)據(jù)成員的setter和getter。由于getter是不會改變數(shù)據(jù)成員的值,所以用const關(guān)鍵字修飾函數(shù),防止getter函數(shù)修改數(shù)據(jù) - 考慮完數(shù)據(jù)成員之后,然后考慮函數(shù)的設(shè)計。要創(chuàng)建對象,需要在類中定義構(gòu)造函數(shù)。構(gòu)造函數(shù)的參數(shù)一般是所有的私有數(shù)據(jù)成員,而要初始化數(shù)據(jù)成員,一般采用初始化列表,而不使用構(gòu)造函數(shù)的函數(shù)體。
- 而對于一般的函數(shù),在參數(shù)設(shè)計中,除了考慮變量名和數(shù)據(jù)類型之外,還要考慮參數(shù)傳遞、是否使用const修飾和有沒有默認(rèn)值等,參數(shù)傳遞優(yōu)先考慮引用傳遞(避免復(fù)制開銷),而不是值傳遞,返回值也是一樣。當(dāng)在函數(shù)體內(nèi)處理完結(jié)果之后,沒使用引用形參來存儲結(jié)果的話,可以使用臨時對象存儲并返回結(jié)果。有些函數(shù)實(shí)現(xiàn)只有幾個操作的簡短代碼,將實(shí)現(xiàn)代碼放在頭文件,設(shè)置函數(shù)為
inline。 - 當(dāng)重載操作符時,可以使用兩種方式來實(shí)現(xiàn):成員函數(shù)和非成員函數(shù)。當(dāng)?shù)谝粋€操作數(shù)是固定的類類型,優(yōu)先使用成員函數(shù),否則就使用非成員函數(shù)。
暫時總結(jié)這么多,后續(xù)還有其他C++面向?qū)ο缶幊痰目偨Y(jié),會繼續(xù)補(bǔ)充?。。?/p>