跳轉到內容

C++ 程式設計/運算子/運算子過載

來自 Wikibooks,開放世界中的開放書籍

運算子過載

[編輯 | 編輯原始碼]

運算子過載(不太常見的術語為特設多型)是多型性(語言的面向物件特性的組成部分)的一種特殊情況,其中一些或所有運算子,如+, ===被視為多型函式,因此根據其引數的型別表現出不同的行為。運算子過載通常只是語法糖。它可以很容易地透過函式呼叫來模擬。

考慮此操作

add (a, multiply (b,c))

使用運算子過載允許以更簡潔的方式編寫它,如下所示

a + b * c

(假設該*運算子的優先順序高於+.)

運算子過載不僅可以提供美學上的優勢,因為語言允許在某些情況下隱式呼叫運算子。使用運算子過載的問題和批評在於,它允許程式設計師為運算子賦予完全自由的功能,而沒有施加一致性,而一致性可以始終滿足使用者/閱讀者的期望。使用<<運算子就是這個問題的一個例子。

// The expression
a << 1;

如果a是整型變數,則會返回a的值的兩倍,但如果a是輸出流,則會將“1”寫入它。由於運算子過載允許程式設計師更改運算子的通常語義,因此通常被認為是謹慎使用運算子過載的良好做法。

過載運算子就是為使用者定義型別賦予新的意義。這與定義函式的方式相同。基本語法如下(其中 @ 表示有效運算子)

return_type operator@(parameter_list)
{
    // ... definition
}

並非所有運算子都可以過載,不能建立新的運算子,也不能更改運算子的優先順序、結合性和元數(例如,! 不能過載為二元運算子)。大多數運算子可以過載為成員函式或非成員函式,但有些運算子必須定義為成員函式。運算子應僅在使用自然且明確無誤的情況下過載,並且應按預期執行。例如,將 + 過載為新增兩個複數是一個很好的用法,而將 * 過載為將物件推入向量則不被視為好的風格。

注意
運算子過載僅應在過載運算子的操作含義明確且對底層型別實用,並且它能提供比適當命名的函式呼叫更顯著的符號簡明性時使用。

一個簡單的訊息頭
// sample of Operator Overloading

#include <string>

class PlMessageHeader
{
    std::string m_ThreadSender;
    std::string m_ThreadReceiver;

    //return true if the messages are equal, false otherwise
    inline bool operator == (const PlMessageHeader &b) const
    {
        return ( (b.m_ThreadSender==m_ThreadSender) &&
                (b.m_ThreadReceiver==m_ThreadReceiver) );
    }

    //return true if the message is for name
    inline bool isFor (const std::string &name) const
    {
        return (m_ThreadReceiver==name);
    }

    //return true if the message is for name
    inline bool isFor (const char *name) const
    {
        return (m_ThreadReceiver==name);// since name type is std::string, it becomes unsafe if name == NULL
    }
};

注意
使用inline關鍵字在上面的示例中在技術上是多餘的,因為在類定義中定義的像這樣的函式是隱式內聯的。

運算子作為成員函式

[編輯 | 編輯原始碼]

除了必須是成員的運算子之外,運算子可以過載為成員函式或非成員函式。是否將運算子過載為成員由程式設計師決定。運算子通常在以下情況下過載為成員

  1. 更改左側運算元,或者
  2. 需要直接訪問物件的非公共部分。

當運算子被定義為成員時,顯式引數的數量減少了一個,因為呼叫物件被隱式地提供為運算元。因此,二元運算子接受一個顯式引數,而一元運算子不接受任何引數。對於二元運算子,左側運算元是呼叫物件,並且不會對其進行任何型別的強制轉換。這與非成員運算子形成對比,在非成員運算子中,左側運算元可以被強制轉換。

    // binary operator as member function
    //Vector2D Vector2D::operator+(const Vector2D& right)const [...]

    // binary operator as non-member function
    //Vector2D operator+(const Vector2D& left, const Vector2D& right)[...]

    // binary operator as non-member function with 2 arguments 
    //friend Vector2D operator+(const Vector2D& left, const Vector2D& right) [...]

    // unary operator as member function
    //Vector2D Vector2D::operator-()const {...}

    // unary operator as non-member function[...]
    //Vector2D operator-(const Vector2D& vec) [...]

可過載運算子

[編輯 | 編輯原始碼]
算術運算子
[編輯 | 編輯原始碼]
  • +(加法)
  • -(減法)
  • *(乘法)
  • /(除法)
  • %(模運算)

作為二元運算子,它們涉及兩個引數,這兩個引數不必是相同型別。這些運算子可以定義為成員函式或非成員函式。下面是一個示例,說明了對 2D 數學向量型別的加法進行過載。

Vector2D Vector2D::operator+(const Vector2D& right)
{
    Vector2D result;
    result.set_x(x() + right.x());
    result.set_y(y() + right.y());
    return result;
}

良好的風格是僅過載這些運算子以執行其慣用的算術運算。由於運算子已被過載為成員函式,因此它可以訪問私有欄位。

位運算子
[編輯 | 編輯原始碼]
  • ^(異或)
  • |(或)
  • &(與)
  • ~(補碼)
  • <<(左移,插入到流中)
  • >>(右移,從流中提取)

所有位運算子都是二元運算子,除了補碼運算子,它是單目運算子。應該注意,這些運算子的優先順序低於算術運算子,因此如果 ^ 被過載為求冪運算,則 x ^ y + z 可能不會按預期工作。值得特別注意的是移位運算子 << 和 >>。它們已在標準庫中過載以與流互動。在將這些運算子過載為與流一起工作時,應遵循以下規則

  1. 將 << 和 >> 過載為友元(以便它可以訪問流的私有變數,該變數按引用傳遞)
  2. (輸入/輸出會修改流,並且不允許複製)
  3. 運算子應返回接收到的流的引用(以允許鏈式操作,例如 cout << 3 << 4 << 5)
使用 2D 向量的一個例子
friend ostream& operator<<(ostream& out, const Vector2D& vec) // output
{
    out << "(" << vec.x() << ", " << vec.y() << ")";
    return out;
}

friend istream& operator>>(istream& in, Vector2D& vec) // input
{
    double x, y;
    // skip opening paranthesis
    in.ignore(1);

    // read x
    in >> x;
    vec.set_x(x);

    // skip delimiter
    in.ignore(2);

    // read y
    in >> y;
    vec.set_y(y);

    // skip closing paranthesis
    in.ignore(1);

    return in;
}
賦值運算子
[編輯 | 編輯原始碼]

賦值運算子 = 必須是成員函式,並且編譯器為使用者定義的類提供預設行為,使用其賦值運算子對每個成員進行賦值。對於僅包含變數的簡單類來說,這種行為通常是可以接受的。但是,當類包含對外部資源的引用或指標時,應過載賦值運算子(作為一般規則,只要需要解構函式和複製建構函式,就需要賦值運算子),否則,例如,兩個字串將共享相同的緩衝區,更改其中一個將更改另一個。

在這種情況下,賦值運算子應執行兩個職責

  1. 清理物件的舊內容
  2. 複製另一個物件的資源

對於包含原始指標的類,在進行賦值之前,賦值運算子應該檢查自賦值,自賦值通常不會生效(因為當物件的舊內容被擦除時,它們不能被複制回來重新填充物件)。自賦值通常是編碼錯誤的標誌,因此對於沒有原始指標的類,這種檢查通常被省略,因為雖然這個操作會浪費 CPU 週期,但它不會對程式碼產生其他影響。

示例
class BuggyRawPointer { // example of super-common mistake
    T *m_ptr;
    public:
    BuggyRawPointer(T *ptr) : m_ptr(ptr) {}
    BuggyRawPointer& operator=(BuggyRawPointer const &rhs) {
        delete m_ptr; // free resource; // Problem here!
        m_ptr = 0;
        m_ptr = rhs.m_ptr;
        return *this;
    };
};

BuggyRawPointer x(new T);
x = x; // We might expect this to keep x the same. This sets x.m_ptr == 0. Oops!

// The above problem can be fixed like so:
class WithRawPointer2 {
    T *m_ptr;
    public:
    WithRawPointer2(T *ptr) : m_ptr(ptr) {}
    WithRawPointer2& operator=(WithRawPointer2 const &rhs) {
        if (this != &rhs) {
            delete m_ptr; // free resource;
            m_ptr = 0;
            m_ptr = rhs.m_ptr;
        }
        return *this;
    };
};

WithRawPointer2 x2(new T);
x2 = x2; // x2.m_ptr unchanged.

賦值運算子過載的另一個常見用法是在類的私有部分宣告過載,而不定義它。因此任何嘗試進行賦值的程式碼都會在兩個方面失敗,首先是引用私有成員函式,其次是由於沒有有效的定義而無法連結。這適用於需要阻止複製的類,並且通常是在新增私有宣告的複製建構函式的情況下完成的。

示例
class DoNotCopyOrAssign {
    public:
        DoNotCopyOrAssign() {};
    private:
        DoNotCopyOrAssign(DoNotCopyOrAssign const&);
        DoNotCopyOrAssign &operator=(DoNotCopyOrAssign const &);
};

class MyClass : public DoNotCopyOrAssign {
    public:
        MyClass();
};

MyClass x, y;
x = y; // Fails to compile due to private assignment operator;
MyClass z(x); // Fails to compile due to private copy constructor.
關係運算符
[edit | edit source]
  • == (相等)
  • != (不相等)
  • > (大於)
  • < (小於)
  • >= (大於或等於)
  • <= (小於或等於)

所有關係運算符都是二元的,應該返回真或假。通常,所有六個運算子都可以基於比較函式或彼此,儘管這永遠不會自動完成(例如,過載 > 不會自動過載 < 以給出相反的結果)。但是,在標頭檔案 <utility> 中定義了一些模板;如果包含此標頭檔案,則只需過載運算子 == 和運算子 <,其他運算子將由 STL 提供。

邏輯運算子
[edit | edit source]
  • ! (非)
  • && (與)
  • || (或)

邏輯運算子 AND 用於評估兩個表示式以獲得單個關係結果。該運算子對應於布林邏輯運算 AND,如果運算元為真,則結果為真,否則為假。以下面板顯示了運算子評估表示式的結果。

運算子 ! 是單目運算子,&& 和 || 是二目運算子。需要注意的是,在正常使用中,&& 和 || 具有“短路”行為,其中可能不會評估右運算元,具體取決於左運算元。當過載時,這些運算子獲得函式呼叫優先順序,並且此短路行為會丟失。最好不要更改這些運算子。

示例
bool Function1();
bool Function2();

Function1() && Function2();

如果 Function1() 的結果為假,則不會呼叫 Function2()。

MyBool Function3();
MyBool Function4();

bool operator&&(MyBool const &, MyBool const &);

Function3() && Function4()

無論 Function3() 的呼叫結果如何,Function3() 和 Function4() 都會被呼叫。這會浪費 CPU 處理能力,更糟糕的是,與預設運算子的預期“短路”行為相比,它可能會產生令人驚訝的意外後果。考慮以下情況

extern MyObject * ObjectPointer;
bool Function1() { return ObjectPointer != null; }
bool Function2() { return ObjectPointer->MyMethod(); }
MyBool Function3() { return ObjectPointer != null; }
MyBool Function4() { return ObjectPointer->MyMethod(); }

bool operator&&(MyBool const &, MyBool const &);

Function1() && Function2(); // Does not execute Function2() when pointer is null
Function3() && Function4(); // Executes Function4() when pointer is null
複合賦值運算子
[edit | edit source]
  • += (加法賦值)
  • -= (減法賦值)
  • *= (乘法賦值)
  • /= (除法賦值)
  • %= (模賦值)
  • &= (AND 賦值)
  • |= (OR 賦值)
  • ^= (XOR 賦值)
  • <<= (左移賦值)
  • >>= (右移賦值)

複合賦值運算子應該過載為成員函式,因為它們會更改左側運算元。與所有其他運算子(除基本賦值運算子外)一樣,複合賦值運算子必須顯式定義,它們不會自動定義(例如,過載 = 和 + 不會自動過載 +=)。複合賦值運算子應該按預期工作:A @= B 應該等效於 A = A @ B。以下是二維數學向量型別的 += 的示例。

Vector2D& Vector2D::operator+=(const Vector2D& right)
{
    this->x += right.x;
    this->y += right.y;
    return *this;
}
自增和自減運算子
[edit | edit source]
  • ++ (自增)
  • -- (自減)

自增和自減有兩種形式,字首 (++i) 和字尾 (i++)。為了區分,字尾版本接受一個啞整數。自增和自減運算子最常被用作成員函式,因為它們通常需要訪問類中的私有成員資料。字首版本通常應該返回對已更改物件的引用。字尾版本應該只返回原始值的副本。在理想情況下,A += 1、A = A + 1、A++、++A 應該都使 A 具有相同的值。

示例
SomeValue& SomeValue::operator++() // prefix
{
    ++data;
    return *this;
}

SomeValue SomeValue::operator++(int unused) // postfix
{
    SomeValue result = *this;
    ++data;
    return result;
}

通常,為了方便維護,一個運算子是在另一個運算子的基礎上定義的,尤其是在函式呼叫很複雜的情況下。

SomeValue SomeValue::operator++(int unused) // postfix
{
    SomeValue result = *this;
    ++(*this); // call SomeValue::operator++()
    return result;
}
下標運算子
[edit | edit source]

下標運算子 [ ] 是一個運算子,它可以接受任意數量的引數(與函式呼叫非常類似),但通常只有一個。它也必須是成員函式(因此它最多隻能接受一個顯式引數,即索引)。下標運算子並不侷限於接受整數索引。例如,std::map 模板的下標運算子的索引與鍵的型別相同,因此它可能是字串等。下標運算子通常過載兩次;作為非常量函式(用於更改元素時),以及作為常量函式(用於僅訪問元素時)。

函式呼叫運算子
[edit | edit source]

函式呼叫運算子 ( ) 通常被過載以建立行為類似於函式的物件,或者用於具有主要操作的類。函式呼叫運算子必須是成員函式,但沒有其他限制 - 它可以與任何數量的任何型別的引數進行過載,並且可以返回任何型別。一個類也可以對函式呼叫運算子進行多個定義。

地址運算子、引用運算子和指標運算子
[edit | edit source]

這三個運算子,operator&()、operator*() 和 operator->() 可以被過載。通常,這些運算子只針對智慧指標或試圖模擬原始指標行為的類進行過載。指標運算子 operator->() 有一個額外的要求,即對該運算子的呼叫的結果必須返回一個指標,或者是一個具有過載 operator->() 的類。通常情況下,A == *&A 應該為真。

注意過載 operator& 會導致未定義的行為

ISO/IEC 14882:2003,第 5.3.1 節
可以獲取不完整型別的物件的地址,但如果該物件的完整型別是宣告 operator&() 作為成員函式的類型別,則行為未定義(並且不需要診斷)。
示例
class T {
    public:
        const memberFunction() const;
};

// forward declaration
class DullSmartReference;

class DullSmartPointer {
    private:
        T *m_ptr;
    public:
        DullSmartPointer(T *rhs) : m_ptr(rhs) {};
        DullSmartReference operator*() const {
            return DullSmartReference(*m_ptr);
        }
        T *operator->() const {
            return m_ptr;
        }
};

class DullSmartReference {
    private:
        T *m_ptr;
    public:
        DullSmartReference (T &rhs) : m_ptr(&rhs) {}
        DullSmartPointer operator&() const {
            return DullSmartPointer(m_ptr);
        }
        // conversion operator
        operator T() { return *m_ptr; }
};

DullSmartPointer dsp(new T);
dsp->memberFunction(); // calls T::memberFunction

T t;
DullSmartReference dsr(t);
dsp = &dsr;
t = dsr; // calls the conversion operator

這些都是非常簡化的示例,旨在展示如何過載運算子,而不是 SmartPointer 或 SmartReference 類的完整細節。通常,你不會想在同一個類中過載所有這三個運算子。

逗號運算子
[edit | edit source]

逗號運算子() , 可以被過載。語言逗號運算子具有從左到右的優先順序,運算子() 具有函式呼叫優先順序,因此請注意,過載逗號運算子有很多陷阱。

示例
MyClass operator,(MyClass const &, MyClass const &);

MyClass Function1();
MyClass Function2();

MyClass x = Function1(), Function2();

對於非過載的逗號運算子,執行順序將是 Function1()、Function2();使用過載的逗號運算子,編譯器可以先呼叫 Function1() 或 Function2()。

成員引用運算子
[edit | edit source]

這兩個成員訪問運算子,operator->() 和 operator->*() 可以被過載。過載這些運算子最常見的用法是定義表示式模板類,這並不是一種常見的程式設計技巧。顯然,透過過載這些運算子,你可以建立一些非常難以維護的程式碼,因此只有在非常小心的時候才過載這些運算子。

當 -> 運算子應用於型別為 (T *) 的指標值時,語言會解除指標的引用並應用 . 成員訪問運算子(因此 x->m 等效於 (*x).m)。但是,當 -> 運算子應用於類例項時,它被呼叫為單目字尾運算子;它應該返回一個值,該值可以再次應用 -> 運算子。通常,這將是一個型別為 (T *) 的值,如上面 地址運算子、引用運算子和指標運算子 下面的示例所示,但也可能是一個定義了 operator->() 的類例項;語言將根據需要呼叫 operator->(),直到它得到一個型別為 (T *) 的值。

記憶體管理運算子
[編輯 | 編輯原始碼]
  • new (為物件分配記憶體)
  • new[ ] (為陣列分配記憶體)
  • delete (釋放物件記憶體)
  • delete[ ] (釋放陣列記憶體)

記憶體管理運算子可以被過載以自定義分配和釋放(例如,插入相關的記憶體頭)。它們應該按預期行為,new 應該返回指向堆上新分配物件的指標,delete 應該釋放記憶體,忽略空引數。要過載 new,必須遵循以下規則:

  • new 必須是成員函式
  • 返回值型別必須是 void*
  • 第一個顯式引數必須是 size_t

要過載 delete,也有一些條件:

  • delete 必須是成員函式(並且不能是虛擬函式)
  • 返回值型別必須是 void
  • 引數列表只有兩種形式可用,並且在一個類中只允許出現其中一種形式:
    • void*
    • void*, size_t
轉換運算子
[編輯 | 編輯原始碼]

轉換運算子允許類的物件被隱式(強制轉換)或顯式(型別轉換)轉換為另一種型別。轉換運算子必須是成員函式,並且不應改變被轉換的物件,因此應該被標記為常量函式。轉換運算子宣告的基本語法,以及用於 int 轉換運算子的宣告如下所示。

operator ''type''() const; // const is not necessary, but is good style
operator int() const;

請注意,函式沒有宣告返回值型別,可以從轉換型別輕鬆推斷出來。在轉換運算子的函式頭中包含返回值型別是一個語法錯誤。

double operator double() const; // error - return type included

無法過載的運算子

[編輯 | 編輯原始碼]
  • ?: (條件運算子)
  • . (成員選擇運算子)
  • .* (指向成員的指標的成員選擇運算子)
  • :: (作用域解析運算子)
  • sizeof (物件大小資訊)
  • typeid (物件型別資訊)
  • static_cast (型別轉換運算子)
  • const_cast (型別轉換運算子)
  • reinterpret_cast (型別轉換運算子)
  • dynamic_cast (型別轉換運算子)

要了解為什麼語言不允許過載這些運算子,請閱讀 Bjarne Stroustrup 的 C++ 樣式和技術常見問題解答中的 "為什麼我不能過載點,::,sizeof 等?" ( http://www.stroustrup.com/bs_faq2.html#overload-dot )。

華夏公益教科書