C++ 運算子過載詳解
1. 運算子過載簡介
所謂過載,就是賦予新的含義。函式過載(Function Overloading)可以讓一個函式名有多種功能,在不同情況下進行不同的操作。同樣運算子過載(Operator Overloading)可以讓同一個運算子可以有不同的功能。
- 可以對 int、float、string 等不同型別資料進行操作
<< 既是位移運算子,又可以配合 cout 向控制檯輸出資料
也可以自定義運算子過載:
class Complex { public: Complex(); Complex(double real, double imag); Complex operator+(const Complex &a) const; void display() const; private: double m_real; double m_imag; }; // ... // 實現運算子過載 Complex Complex::operator+(const Complex &A) const{ Complex B; B.m_real = this->m_real + A.m_real; B.m_imag = this -> m_imag + A.m_imag; return B; // return Complex(this->m_real + A.m_real, this->m_imag + A.m_imag); } int main(){ Complex c1(4.3, 5.8); Complex c2(2.7, 3.7); Complex c3; c3 = c1 + c2; // 運算子過載 c3.display(); return 0; }
運算結果
7 + 9.5i
運算子過載其實就是定義一個函式,在函式體內實現想要的功能,當用到該運算子時,編譯器會自動呼叫這個函式,它本質上是函式過載。
c3 = c1 + c2;
實際上通過呼叫成員函式 operator+(),會轉換為下面的形式:
c3 = c1.operator+(c2);
全域性範圍內過載運算子
運算子過載函式不僅可以作為類的成員函式,還可以作為全域性函式。
複數加法運算通過全域性範圍內過載 + 實現:
class Complex{ // ... friend complex operator+(const complex &A, const complex &B); }; // 全域性函式 Complex operator+(const Complex &A, const Complex &B) { return Complex(A.m_real + B.m_real, A.m_imag + B.m_imag); // 訪問了Complex 的 private 成員變數,需要宣告為友元函式 }
2. 運算子過載時要遵循的規則
-
能夠過載的運算子
+ - * / % ^ & | ~ ! = < > += = = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , -> - > () [] new new[] delete delete[]
長度運算子 sizeof、條件運算子: ?、成員選擇符. 和域解析運算子::不能被過載。
-
過載不能改變運算子的優先順序和結合律
-
過載不會改變運算子的用法,即運算元個數、位置都不會改變
-
運算子過載函式不能有預設的引數,因為這改變了運算子運算元個數
-
運算子過載函式既可作為類成員函式,也可為全域性函式,注意全域性函式如何要訪問類物件的私有成員,需要宣告為類的友元
-
箭頭運算子->、下標運算子[]、函式呼叫運算子()、賦值運算子 =,只能以成員函式的形式過載。
3. 過載運算子實現形式的選擇
過載運算子可以通過成員函式和全域性函式(友元)來實現
轉換建構函式
Complex c1(25, 25);
Complex c2 = c1 + 15.6;
Complex c3 = 28.23 + c1; // 要以全域性函式實現過載
這幾行程式碼都可以順利執行,說明 Complex 物件可以和 double 型別物件相加。其實,編譯器在檢測到 Complex 和 double(小數預設為 double 型別)相加時,會先嚐試將 double 轉換為 Complex,或者反過來將 Complex 轉換為 double(只有型別相同的資料才能進行 + 運算),如果都轉換失
敗,或者都轉換成功(產生了二義性),才報錯。實際上,上述兩行加法程式碼會轉換為:
Complex c2 = operator+(c1,Complex(15.6));
Complex c3 = operator+(Complex(28.23),c1);
Complex(double real)
在作為普通建構函式的同時,還能將 double 型別轉換為 Complex 型別,集合了“建構函式”和“型別轉換”的功能,所以被稱為「轉換建構函式」。換句話說,轉換建構函式用來將其它型別(可以是 bool、int、double等基本型別,也可以是陣列、指標、結構體、類等構造型別)轉換為當前類型別。
以全域性函式形式過載
以全域性函式的形式過載了 +、-、*、/、==、!=,這樣做是為了保證這些運算子能夠被對稱的處理。
如果將 operator+定義為成員函式,根據“+ 運算子具有左結合性”這條原則,Complex c2 = c1 + 15.6;
會被轉換為下面的形式:
Complex c2 = c1.operator+(Complex(15.6));
但是對於 Complex c3 = 28.23 + c1
,編譯器會嘗試轉換為不同形式:
Complex c3 = (28.23).operator+(c1);
顯然這是錯誤的。
以成員函式的形式過載
以成員函式的形式過載了 +=、-=、 *=、/=。
運算子過載的初衷是給類新增新的功能,方便類的運算,它作為類的成員函式是理所應當的, 是首選的。不過類的成員函式不能對稱處理資料,運算子的第一個運算物件不會出現型別轉換(要類的物件才能呼叫類的成員函式)。
C++ 規定,箭頭運算子->、下標運算子[ ]、函式呼叫運算子( )、賦值運算子=只能以成員函式的形式過載。
4. 過載 >> 和 <<(輸入輸出運算子)詳解
C++ 標準庫已對 左移運算子 << 和 >> 右移運算子進行了承載,如果我們定義新的型別需要輸入輸出運算子去處理,需要再進行過載。
過載運算子 >>
以全域性函式的形式過載>>,使它能夠讀入兩個 double 型別的資料,並分別賦值給複數的實部和虛部:
istream & operator>>(istream &in, Complex &A) // 友元
{
in >> A.m_real >> A.m_image;
return in;
}
之所以返回 istream 類 物件的引用,是為了能夠連續讀取複數,讓程式碼書寫更加漂亮,例如:
Complex c1,c2;
cin >> c1 >> c2;
如果不返回引用,需要一個一個讀取:
cin >> c1;
cin >> c2;
實際上,上述 >> 運算子會被轉換成如下形式:
operator<<(cin, c);
過載運算子 <<
ostream & operator<<(ostream &out, complex &A){
out << A.m_real <<" + "<< A.m_imag <<" i ";
}
5. 過載 [] 下標運算子
下標運算子 [] 必須以成員函式的形式進行過載。
int& Array::operator[](int i){
return m_p[i];
}
const int & Array::operator[](int i) const{
return m_p[i];
}
當 Array 是 const 物件時,如果沒有提供 const 版本的 operator[],會報錯。
6. 過載 ++ 和 -- 自增自減運算子
class Stopwatch{
// ... 秒錶類
public:
Stopwatch operator++(); // ++i,前置形式
Stopwatch operator++(int); // i++,後置形式
Stopwatch run(); // 執行
private:
int m_min; // 分鐘
int m_sec; // 秒鐘
};
Stopwatch Stopwatch::run(){
++m_sec;
if(m_sec == 60){
m_min++;
m_sec = 0;
}
return *this;
}
Stopwatch Stopwatch::operator++(){
return run();
}
Stopwatch Stopwatch::operator++(int n){
Stopwatch s = *this; // i++, 先返回物件,再物件自增,所以需要將物件儲存
run();
return s;
}
函式中引數 n 是沒有任何意義的,它的存在只是為了區分是前置形式還是後置形式。
7. 過載 new 和 delete 運算子
記憶體管理運算子 new、new[]、delete 和 delete[] 也可以進行過載,其過載形式既可以是類的成員函式,也可以是全域性函式。一般情況下,內建的記憶體管理運算子就夠用了,只有在需要自己管理記憶體時才會過載。
// 成員函式
void * className::operator new( size_t size ){
//TODO:
}
void className::operator delete( void *ptr){
//TODO:
}
// 全域性函式
void * operator new( size_t size ){
//TODO:
}
void operator delete( void *ptr){
//TODO:
}
在過載 new 或 new[] 時,無論是作為成員函式還是作為全域性函式,它的第一個引數必須是 size_t 型別。size_t 表示的是要分配空間的大小,對於 new[] 的過載函式而言,size_t 則表示所需要分配的所有空間的總和。
size_t 在標頭檔案
當然,過載函式也可以有其他引數,但都必須有預設值,並且第一個引數的型別必須是 size_t。
如果類中沒有定義 new 和 delete 的過載函式,那麼會自動呼叫內建的 new 和 delete 運算子。
8. 過載()(強制型別轉換運算子)
型別強制轉換運算子是單目運算子,也可以被過載,但只能過載為成員函式,不能過載為全域性函式。經過適當 過載後,(型別名)物件這個對物件進行強制型別轉換的表示式就等價於物件.operator 型別名(),即變成對運算子函式的呼叫。
class Complex{
public:
// ...
operator double()
{
return real;
}
private:
double m_real;
double m_imag;
};
int main()
{
Complex c(1.2,3.4);
cout << (double)c << endl; // 1.2
double n = 2 + c; // 等價於 double n = 2 + c.operator double()
}
9. 運算子過載總結
注意事項:
- 過載後運算子的含義應該符合原有用法習慣。例如過載 + 運算子,完成的功能就應該類似於做加法,在過載的+ 運算子中做減法是不合適的。此外,過載應儘量保留運算子原有的特性。
- C++ 規定,運算子過載不改變運算子的優先順序。
- 以下運算子不能被過載:.、.*、::、? :、sizeof。
- 過載運算子()、[]、->、或者賦值運算子=時,只能將它們過載為成員函式,不能過載為全域性函式。
-
運算子的實質是將運算子過載為一個函式,使用運算子的表示式就會被解釋為對過載函式的呼叫。
-
運算子可以過載為全域性函式。此時函式的引數個數就是運算子的運算元個數,運算子的運算元就成為函式的實參。(友元)
-
運算子也可以過載為成員函式。此時函式的引數個數就是運算子的運算元個數減一,運算子的運算元有一個成為函式作用的物件,其餘的成為函式的實參。
-
必要時需要過載賦值運算子=,以避免兩個物件內部的指標指向同一片儲存空間。
-
<<和>>是在 iostream 中被過載,才成為所謂的“流插入運算子”和“流提取運算子”的。
-
型別的名字可以作為強制型別轉換運算子,也可以被過載為類的成員函式。它能使得物件被自動轉換為某種型別。
-
自增、自減運算子各有兩種過載方式,用於區別前置用法和後置用法。
-
運算子過載不改變運算子的優先順序。過載運算子時,應該儘量保留運算子原本的特性。