9.C++運算子過載
運算子過載
本文包括了對C++類的6個預設成員函式中的賦值運算子過載和取地址和const物件取地址操作符的過載。
運算子是程式中最最常見的操作,例如對於內建型別的賦值我們直接使用=賦值即可,因為這些編譯器已經幫我們做好了,但是物件的賦值呢?能直接賦值嗎?
概念
C++為了增強程式碼的可讀性引入了運算子過載,運算子過載是具有特殊函式名的函式,也具有其返回值型別,函式名字以及引數列表,其返回值型別與引數列表與普通的函式類似。
函式名字為:關鍵字operator後面接需要過載的運算子符號。
函式原型:返回值型別 operator操作符(引數列表)
需要注意的幾點:
-
不能通過連線其他符號來建立新的操作符:比如operator@,必須是已有的操作符;
-
過載操作符必須有一個類型別或者列舉型別的運算元;
-
用於內建型別的操作符,其含義不能改變,例如:內建的整型+,不 能改變其含義;
-
作為類成員的過載函式時,其形參看起來比運算元數目少1,成員函式的操作符有一個預設的形參this,限定為第一個形參;
-
引數個數與過載的運算子有關;
-
.* 、:: 、sizeof 、?: 、. 注意以上5個運算子不能過載;
-
運算子過載作用於左運算元,會把左運算元當做第一個引數;
既然是對自定義型別物件之間的操作符的過載,那麼它的引數一定有此型別的物件,並且需要對物件的成員進行操作,這就需要打破封裝的限制,那麼這個函式應該設定為全域性的還是類的成員呢?
有以下幾種思路:
- 函式設為公有,成員變數設為公有(不好);
- 函式設為公有另外寫一個成員函式區獲取成員變數的值(不好);
- 將函式設為類的友元函式(可以);
- 放入類中,作為成員函式(推薦);
// 全域性的operator== class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; }; // 這裡會發現運算子過載成全域性的就需要成員變數是共有的,那麼問題來了,封裝性如何保證? // 這裡其實可以用我們後面學習的友元解決,或者乾脆過載成成員函式。 bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } int main() { Date d1(2018, 9, 26); Date d2(2018, 9, 29); cout << (d1 == d2) << endl; return 0; }
這樣的寫法就打破了封裝,讓類的成員都暴露了出來,這樣的損失不太值得。
賦值運算子過載
賦值操作運算子過載特徵如下:
- 引數型別相同;
- 返回值;
- 檢測是否給自己賦值;
- 返回*this;
- 一個類如果沒有顯式的定義賦值操作符過載,編譯器會自動生成一個,完成物件位元組序的拷貝(淺拷貝);
- 賦值運算子在類中不顯式實現時,編譯器會生成一份預設的,此時使用者在類外再將賦值運算子過載為全域性的,就和編譯器生成的預設賦值運算子衝突了,故賦值運算子只能過載成成員函式。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2018, 10, 1);
// 這裡d1呼叫的編譯器生成operator=完成拷貝,d2和d1的值也是一樣的。
d1 = d2;
d1.Display();
d2.Display();
return 0;
}
是不是很像自動生成的拷貝構造?那麼它也存在一定的問題,對於日期類的物件他能很好的完成賦值操作,可對於指標型別呢?
下面的程式會崩潰
class String
{
public:
String(const char* str = "songxin")
{
cout << "String(const char* str = \"songxin\")" << endl;
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
_str = nullptr;
}
private:
char* _str;
};
int main()
{
String s1("tanmei");
String s2;
s2 = s1;
return 0;
}
原因也是因為淺拷貝的關係,導致同一塊記憶體被釋放了兩次,程式崩潰。
可以不顯式定義賦值操作符過載函式的情況
- 成員變數沒有指標;
- 成員變數有指標,但是指標沒有管理記憶體資源;
注意:賦值操作符過載與拷貝構造不同的地方就是拷貝構造是在物件定義時,而賦值操作符過載是作用於已經存在的物件。
const成員
const修飾類的成員函式,有點奇怪,const怎麼能修飾函式呢?
將const修飾的類成員函式稱之為const成員函式,const修飾類成員函式,實際修飾該成員函式隱含的this指標指向的物件,表明在該成員函式中不能對指標指向物件的任何成員進行修改。
class Date
{
public:
Date()//建構函式不寫的話建立const的物件會報錯。
:
_year(1900),
_month(1),
_day(1)
{}
void Display()
{
cout << "Display ()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Display() const
{
cout << "Display () const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
d1.Display();
const Date d2;
d2.Display();
return 0;
}
const的物件就會呼叫Display函式會呼叫哪一個呢?注意到上面程式碼的第18行的函式被const修飾,那麼這個const有什麼作用?
實際上這個const修飾的是*this,表明 *this不可被修改,那麼const的物件就會呼叫被const修飾的函式,否則可能會出現下面的問題。
- const物件可以呼叫非const成員函式嗎?
不可以,許可權放大。
- 非const物件可以呼叫const成員函式嗎?
可以,許可權縮小。
- const成員函式內可以呼叫其它的非const成員函式嗎?
不可以,許可權放大。
- 非const成員函式內可以呼叫其它的const成員函式嗎?
可以,許可權縮小。
還有一個值得注意的地方,上面的程式碼如果我們不顯式定義建構函式的話,例項化const的物件時會報錯:
“d2”: 必須初始化 const 物件
也就是說編譯器認為const物件(包括成員)無法被賦值,應該有初始化操作,而預設生成的構造是沒有對int有初始化操作的,因此報錯;
取地址及const取地址操作符過載
取地址操作符也要過載嗎?只有很少的情況會用到,通常直接使用編譯器預設生成的就可以。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
那麼什麼時候我們會過載呢?
- 想讓別人獲取指定的內容的地址;
- 隱藏物件真實的地址;
class Date
{
public:
Date* operator&()//隱藏物件真實地址
{
return nullptr;
}
const int* operator&()const//讓使用者指定獲取成員變數_day的地址
{
return &(_day);
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
const Date d1;
Date d2;
cout << &d1 << endl;//
cout << &d2 << endl;//
return 0;
}
輸出:
0000005597AFF770
0000000000000000
不過這樣的情況確實很少,也沒有什麼意義。