C++ 程式設計/運算子/運算子過載
運算子過載(不太常見的術語為特設多型)是多型性(語言的面向物件特性的組成部分)的一種特殊情況,其中一些或所有運算子,如+, =或==被視為多型函式,因此根據其引數的型別表現出不同的行為。運算子過載通常只是語法糖。它可以很容易地透過函式呼叫來模擬。
考慮此操作
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
}
};
除了必須是成員的運算子之外,運算子可以過載為成員函式或非成員函式。是否將運算子過載為成員由程式設計師決定。運算子通常在以下情況下過載為成員
- 更改左側運算元,或者
- 需要直接訪問物件的非公共部分。
當運算子被定義為成員時,顯式引數的數量減少了一個,因為呼叫物件被隱式地提供為運算元。因此,二元運算子接受一個顯式引數,而一元運算子不接受任何引數。對於二元運算子,左側運算元是呼叫物件,並且不會對其進行任何型別的強制轉換。這與非成員運算子形成對比,在非成員運算子中,左側運算元可以被強制轉換。
// 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 可能不會按預期工作。值得特別注意的是移位運算子 << 和 >>。它們已在標準庫中過載以與流互動。在將這些運算子過載為與流一起工作時,應遵循以下規則
- 將 << 和 >> 過載為友元(以便它可以訪問流的私有變數,該變數按引用傳遞)
- (輸入/輸出會修改流,並且不允許複製)
- 運算子應返回接收到的流的引用(以允許鏈式操作,例如 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;
}
賦值運算子 = 必須是成員函式,並且編譯器為使用者定義的類提供預設行為,使用其賦值運算子對每個成員進行賦值。對於僅包含變數的簡單類來說,這種行為通常是可以接受的。但是,當類包含對外部資源的引用或指標時,應過載賦值運算子(作為一般規則,只要需要解構函式和複製建構函式,就需要賦值運算子),否則,例如,兩個字串將共享相同的緩衝區,更改其中一個將更改另一個。
在這種情況下,賦值運算子應執行兩個職責
- 清理物件的舊內容
- 複製另一個物件的資源
對於包含原始指標的類,在進行賦值之前,賦值運算子應該檢查自賦值,自賦值通常不會生效(因為當物件的舊內容被擦除時,它們不能被複制回來重新填充物件)。自賦值通常是編碼錯誤的標誌,因此對於沒有原始指標的類,這種檢查通常被省略,因為雖然這個操作會浪費 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 )。