C++面向?qū)ο缶幊?一):基于對象(無成員指針)

在面向?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 class

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


string類來講述成員數(shù)據(jù)含指針。

string class

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ù)用性。

C++ Programs

擴(kuò)展文件名(extension file name)不一定是.hcpp,有可能是.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)鍵字publicprivate來標(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ù)一般都要做以下工作:

  1. 調(diào)用前要先保存寄存器,并在返回時恢復(fù)
  2. 復(fù)制實(shí)參
  3. 程序轉(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ù)reim,雖然可以通過real()imag()函數(shù)來訪問,但是如果直接訪問reim兩個數(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ù)成員的settergetter。由于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>

擴(kuò)展閱讀

極客班[C++系統(tǒng)工程師教程]
C++ Primer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容