C++面向對象編程(一):基於對象(無成員指針)

在面向對象編程中,類(Class)對象(Object)是兩個非常重要和基本的概念,類(Class)包含成員數據和實現行爲的函數,當然還提供構造函數來創建對象。如果是一些需要手動釋放內存的語言,例如C++,還提供析構函數來幫助釋放內存空間;如果是一些有垃圾回收機制的語言,比如Java,就不需要提供析構函數來釋放內存,內存釋放交給系統來管理。而對象(Object)是類的實例,每次創建一個對象都有不同的標識符來表示不同的對象,雖然對象中的數據有些是相同的,但它們是否相同根據標識符來判斷的。

關於數據成員與函數

在C++中,Class有兩個經典的分類:

  • Class without pointer member (complex複數類)
  • Class with pointer member (string字符串類)

一個是類的數據成員不含指針,一個是類的數據成員含指針complex類來講述數據成員不含指針。

complex class

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


string類來講述成員數據含指針。

string class

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。它們之間的語法有一點不同,一個是用尖括號<>來專門包含系統文件和標準庫,另一個是用雙引號""來包含自定義的類和文件。

使用預處理中的文件包含,能夠將一個大文件分離到各種不同職責類的頭文件和實現文件。這樣不僅減少文件體積而無需加載無用的代碼,提供編譯速度;還能夠提高代碼的複用性。

C++ Programs

擴展文件名(extension file name)不一定是.hcpp,有可能是.hpp或其他擴展文件名。

Header(頭文件)防衛式聲明

頭文件經常#include其他頭文件,甚至一個頭文件可能被多次包含進同一個源文件。爲了避免重複包含,使用大寫的預處理器變量以及其他語句來處理。預處理器變量有兩種狀態:未定義和已定義;定義預處理器變量和檢測其狀態所用的預處理指示不同。

#define指示表示定義一個預處理變量,而ifndef指示檢測預處理器變量是否未定義;如果未定義,那麼跟在其後的所有指示都被處理,如果已經被定義,那麼跟在其後的所有指示會跳過不處理。 部分示例代碼如下:

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 (非成員函數定義)
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(訪問級別)

上面有兩個關鍵字publicprivate來標明數據成員和成員函數的訪問級別,public表示類的外部能夠訪問類裏面的數據或函數,而private表示類的外部不能訪問類裏面的數據和函數,只允許類內部來訪問;通常使用private來修飾數據成員來封裝數據,不讓類外部的數據輕易訪問。如果類外部的數據想訪問,就定義一些public的accessors方法來暴露給外部接口來訪問。

Constructor(構造函數)

如果你使用類來創建對象並初始化數據成員,就需要定義構造函數complex類的構造函數定義如下:

// 使用初始化列表 (推薦使用)
complex (double r = 0, double i = 0)
    : re(r), im(i)
{}

// 使用函數體 (不推薦使用)
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類這個例子,使用其中一種方式會讓最終效果一樣,但有些情況只能使用初始化列表。看下面這個例子:

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或引用類型的成員,不管哪種類型,都必須在構造函數初始化列表中進行初始化。

所以上面那個例子應該改爲:

ConstRef::ConstRef(int ii)
    :  i(ii), ci(ii), ri(ii)  {}

建議:使用構造函數初始化列表,而不是函數體來初始化數據成員。

重載(Overloaded)函數

在設計構造函數創建對象時,可能需要不同參數來創建對象,這時需要重載函數

重載函數:出現在相同作用域中兩個函數,如果有相同的名字形參表不同,則稱爲重載函數。

就我們這個complex類的構造函數而言,有兩個構造函數:

complex (double r, double i)
    : re(r), im(i) {}

complex (double r) : re(r), im(0) {}

第一個是有兩個參數的構造函數,第二個是隻有一個參數的構造函數,雖然它們的函數名相同,但由於它們的參數不同,C++編譯器能夠分辨出兩個不同的函數,從而調用對應的構造函數。當使用complex c1(2, 3)創建對象時,對應會調用第一個構造函數。而當使用complex c2(2)創建對象時,對應會調用第二個構造函數。

當然,重載函數的概念不僅僅是用在構造函數,而應用在所有類型的函數,包括內聯函數和普通的函數。

Inline(內聯)函數

對於一些簡單操作,我們有時將它定義爲函數,例如:

// find longer of two strings
const string& shorterString(const string& s1, const string& s2)
{
    return s1.size() < s2.size() ? s1 : s2;
}

這樣做的話,有幾點好處

  • 使用函數可以確保統一的行爲,並可以測試。
  • 閱讀和理解函數shorterString的調用,要比讀一條用等價的條件表達式取代函數調用更加容易理解
  • 如果需要做任何修改,修改函數要比逐條修改條件表達式更加容易。
  • 函數可以重用,不必爲其他應用重寫代碼。

但簡短的shorterString函數有個潛在的缺點:就是調用函數比求解條件表達式要慢的多,因爲調用函數一般都要做以下工作:

  1. 調用前要先保存寄存器,並在返回時恢復
  2. 複製實參
  3. 程序轉向一個新位置執行。

內聯函數避免函數調用的開銷

如果使用內聯函數,就可以避免函數調用的開銷。編譯器會將內聯函數在程序中每個調用點“內聯地”展開。假設我們將shorterString定義爲內聯函數,則調用:

cout << shorterString(s1, s2) << endl;

在編譯時就會展開爲:

cout << s1.size() < s2.size() ? s1 : s2 << endl;

內聯函數放在頭文件

內聯函數應該在頭文件定義,這一點不同於其他函數,這樣編譯器才能在調用點內聯展開函數代碼。內聯機制適用於只有幾行且經常被調用的代碼,如果代碼行數或操作太多,即使你使用inline關鍵字來修飾函數,編譯器也不會將它看作爲內聯函數。

Const(常量)成員函數

每個成員函數都有一個額外的、隱形的形參this,在調用成員函數時,形參this初始化爲調用函數的對象地址。爲了理解成員函數的調用,請看complex類這個例子:

complex c1 (2, 4);   // create object
cout << c1.real() << endl;  // access const function real

編譯器就會重寫real函數的調用:

complex::real(&c1);

在這個調用中,在real函數的參數表中,有個this指針指向c1對象。如果在成員函數聲明的形參表後面加入const關鍵字,那麼const改變隱含this形參的類型,即隱含的this形參是一個指向c1對象的const complex*類型指針。因此,real函數對成員變量re所做操作是隻能訪問,而不能修改。同理,imag成員函數也是。

參數傳遞: pass by value vs. pass by reference

每次調用函數時,所傳遞的實參將會初始化對應的形參;參數傳遞有兩種方式:一種是值傳遞,另一種就是引用傳遞。如果形參是使用值傳遞,那麼複製實參的值;如果形參是引用傳遞,那麼它只是實參的別名。看complex類這個例子中定義一個函數:

complex& operator+= (const complex& );

&符號放在complex類後面,則表示調用函數式是使用引用傳遞來傳遞數據。爲什麼使用引用傳遞而不使用值傳遞呢?

值傳遞的侷限性

  • 當需要在函數中修改實參的值時
  • 當傳遞的實參是大型對象時,複製對象所付出的時間和存儲空間代價比較大
  • 當沒有辦法實現對象複製時

參數傳遞選擇

  • 優先考慮引用傳遞(const),避免複製
  • 當在函數中處理後的結果是使用局部變量來存儲,而不是形參的引用參數,使用值傳遞來返回。

Friend(友元)

在某些情況下,允許特定的非成員函數訪問一個類的私有成員,同時仍然阻止一般的訪問。例如,被重載的操作符,如輸入或輸出操作符,經常需要訪問類的私有數據成員,這些操作不可能爲類的成員;然而,儘管不是類的成員,它們仍是類的“接口組成部分”。

友元機制允許一個類將對其非公有成員的訪問權授予指定的函數或類。友元的聲明以關鍵字friend開始,它只能出現在類定義的內部。

complex類爲例,它有一個友元函數__doapl

friend complex& __doapl (complex*, const complex&);

由於它參數是complex類,在函數內部需要訪問到complex類的私有數據reim,雖然可以通過real()imag()函數來訪問,但是如果直接訪問reim兩個數據成員,就能提高程序運行速度。

重要提示: 相同class的各個objects互爲friends(友元)

(Operator Overloading)操作符重載

C語言的操作符只能應用在基本數據類型,例如:整形、浮點型等。但C++的基本組成單元是類,如果對類的對象也能進行加減乘除等操作符運算,那麼操作起來比調用函數更加方便。因此C++提供操作符重載來支持類與類之間也能使用操作符來運算。

操作符重載是具有特殊名稱的函數:關鍵字operator後接需要定義的操作符符號。像任意其他函數一樣,操作符重載具有返回值和形參表。

如果想操作符重載,有兩種選擇:

  • 成員函數的操作符重載
  • 非成員函數的操作符重載

兩者之間有什麼不同呢?對於成員函數的操作符重載,每次調用成員函數時,都會有一個隱含this形參,限定爲第一個操作數,而this指針的數據類型是固定的,就是該類類型。而非成員函數的操作符重載,形參表比成員函數靈活,第一個形參不再限死爲this形參,而是可以是其他類型的形參。下面我們分別通過兩個例子來看看爲什麼這樣選擇。

成員函數的操作符重載

complex c1(2, 1);
complex c2(5);

c2 += c1;

上面代碼創建兩個complex對象c1和c2,然後使用+=操作符來進行相加賦值操作。我們站在設計API角度來思考,如果重載操作符+=的話,需要提供兩個參數(complex&, complex&),但由於第一個參數類型是complex&跟成員函數this形參一樣,所以優先考慮成員函數。

complex類重載+=操作符:

complex& operator += (const complex&);

而代碼實現放在類聲明外面:

inline complex&
complex::operator += (const complex& r)
{
    return __doapl (this, r);
}

非成員函數的操作符重載

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。最後我們選擇的是使用非成員函數來實現,而不是成員函數。

非成員函數重載+操作符:

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));
}

上面用非成員函數實現+操作符重載時,計算後結果沒有使用引用形參來保存,而是使用一種特殊對象叫臨時對象來保存,它是一個局部變量。語法是typename(data),typename表示類型,data表示傳入的數據。

總結

當設計一個C++類的時候,需要思考一下問題:

  • 首先要考慮它是基於對象(單個類)還是面向對象(多個類)的設計
  • 類由數據成員成員函數組成;一般來說,數據成員的訪問權限應該設置爲private,以防止類的外部隨意訪問修改數據。如果類的外部想訪問數據,類可以定義數據成員的settergetter。由於getter是不會改變數據成員的值,所以用const關鍵字修飾函數,防止getter函數修改數據
  • 考慮完數據成員之後,然後考慮函數的設計。要創建對象,需要在類中定義構造函數。構造函數的參數一般是所有的私有數據成員,而要初始化數據成員,一般採用初始化列表,而不使用構造函數的函數體。
  • 而對於一般的函數,在參數設計中,除了考慮變量名和數據類型之外,還要考慮參數傳遞、是否使用const修飾和有沒有默認值等,參數傳遞優先考慮引用傳遞(避免複製開銷),而不是值傳遞,返回值也是一樣。當在函數體內處理完結果之後,沒使用引用形參來存儲結果的話,可以使用臨時對象存儲並返回結果。有些函數實現只有幾個操作的簡短代碼,將實現代碼放在頭文件,設置函數爲inline
  • 重載操作符時,可以使用兩種方式來實現:成員函數和非成員函數。當第一個操作數是固定的類類型,優先使用成員函數,否則就使用非成員函數。

暫時總結這麼多,後續還有其他C++面向對象編程的總結,會繼續補充!!!

擴展閱讀

極客班[C++系統工程師教程]
C++ Primer

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章