注意:答案是按特定順序給出的,但由于許多用戶根據投票而不是給出答案的時間對答案進行排序,因此以下是按最有意義的順序排列的答案索引:
- C 運算子多載的一般語法
- C 運算子多載的三個基本規則
- 會員與非會員之間的決定
- 多載常用運算子
- 賦值運算子
- 輸入和輸出運算子
- 函式呼叫運算子
- 比較運算子
- 算術運算子
- 陣列下標
- 類指標型別的運算子
- 轉換運算子
- 多載新建和洗掉
(注意:這是Stack Overflow 的 C FAQ 的一個條目。如果您想批評以這種形式提供 FAQ 的想法,那么開始這一切的 meta 上的帖子就是這樣做的地方。答案這個問題在C 聊天室中得到監控,FAQ 的想法最初就是從那里開始的,所以你的答案很可能會被提出這個想法的人閱讀。)
uj5u.com熱心網友回復:
多載常用運算子
多載運算子的大部分作業是樣板代碼。這并不奇怪,因為運算子只是語法糖,它們的實際作業可以由(并且通常被轉發到)普通函式來完成。但重要的是您要正確使用此樣板代碼。如果你失敗了,要么你的操作員代碼無法編譯,要么你的用戶代碼無法編譯,要么你的用戶代碼會表現出意外。
賦值運算子
關于賦值有很多要說的。不過大部分在GMan著名的Copy-And-Swap FAQ中已經講過了,所以我這里就略過了,只列出最完美的賦值運算子供參考:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
位移運算子(用于流 I/O)
位移運算子<<和>>,雖然仍然用于硬體介面中,用于它們從 C 繼承的位操作函式,但在大多數應用程式中作為多載流輸入和輸出運算子變得更加普遍。有關多載為位操作運算子的指南,請參閱下面有關二元算術運算子的部分。要在您的物件與 iostream 一起使用時實作您自己的自定義格式和決議邏輯,請繼續。
流運算子是最常見的多載運算子之一,是二元中綴運算子,其語法沒有指定對它們是成員還是非成員的限制。因為它們改變了它們的左引數(它們改變了流的狀態),所以根據經驗法則,它們應該被實作為它們的左運算元型別的成員。但是,它們的左運算元是來自標準庫的流,雖然標準庫定義的大多數流輸出和輸入運算子確實被定義為流類的成員,但是當您為自己的型別實作輸出和輸入操作時,您不能改變標準庫的流型別。這就是為什么您需要為您自己的型別實作這些運算子作為非成員函式。兩者的規范形式是:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
實施operator>>時,只有在讀取本身成功時才需要手動設定流的狀態,但結果不是預期的。
函式呼叫運算子
函式呼叫運算子,用于創建函式物件,也稱為仿函式,必須定義為成員函式,因此它總是具有成員函式的隱式this引數。除此之外,它可以被多載以接受任意數量的附加引數,包括零。
下面是一個語法示例:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
用法:
foo f;
int a = f("hello");
在整個 C 標準庫中,函式物件總是被復制。因此,您自己的函式物件的復制成本應該很低。如果一個函式物件絕對需要使用復制昂貴的資料,最好將該資料存盤在別處并讓函式物件參考它。
比較運算子
根據經驗,二進制中綴比較運算子應該作為非成員函式1實作。一元前綴否定!應該(根據相同的規則)作為成員函式實作。(但超載通常不是一個好主意。)
標準庫的演算法(例如std::sort())和型別(例如std::map)將始終只期望operator<存在。但是,您的型別的用戶也希望所有其他運算子都存在,因此如果您定義operator<,請務必遵循運算子多載的第三條基本規則,并定義所有其他布爾比較運算子。實作它們的規范方法是這樣的:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
這里要注意的重要一點是,這些運算子中只有兩個實際上在做任何事情,其他運算子只是將他們的引數轉發給這兩個運算子中的任何一個來完成實際作業。
多載其余二元布爾運算子 ( ||, &&) 的語法遵循比較運算子的規則。但是,您不太可能找到這2個合理的用例。
1 與所有經驗法則一樣,有時也可能有理由打破這一規則。如果是這樣,請不要忘記二元比較運算子的左側運算元(對于成員函式而言是 )也*this需要是const。因此,作為成員函式實作的比較運算子必須具有以下簽名:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(注意const最后。)
2 需要注意的是內置版本||和&&使用快捷語意。而用戶定義的(因為它們是方法呼叫的語法糖)不使用快捷語意。用戶會期望這些運算子具有快捷語意,并且他們的代碼可能依賴于它,因此強烈建議永遠不要定義它們。
算術運算子
一元算術運算子
一元遞增和遞減運算子有前綴和后綴兩種形式。為了區分彼此,后綴變體采用額外的虛擬 int 引數。如果多載遞增或遞減,請確保始終同時實作前綴和后綴版本。這是遞增的規范實作,遞減遵循相同的規則:
class X {
X& operator ()
{
// do actual increment
return *this;
}
X operator (int)
{
X tmp(*this);
operator ();
return tmp;
}
};
請注意,后綴變體是根據前綴實作的。另請注意,postfix 會進行額外的復制。2個
多載一元減號和加號不是很常見,最好避免。如果需要,它們應該作為成員函式多載。
2 另請注意,后綴變體做的作業更多,因此使用效率低于前綴變體。這是一個很好的理由,通常更喜歡前綴增量而不是后綴增量。雖然編譯器通常可以優化內置型別的后綴增量的額外作業,但它們可能無法為用戶定義的型別做同樣的事情(這可能看起來像串列迭代器一樣無辜)。一旦你習慣了 do ,當它不是內置型別時i 就很難記住 to do iinstead (而且你必須在更改型別時更改代碼),所以最好養成總是i使用前綴增量,除非明確需要后綴。
二元算術運算子
對于二元算術運算子,不要忘記遵守運算子多載的第三個基本規則:如果提供 ,則同時提供 =,如果提供-,則不要省略-=,等等。據說Andrew Koenig 是第一個觀察到復合賦值的人運算子可以用作非復合運算子的基礎。也就是說,運算子 是根據 實作的 =,-是根據-=等實作的。
根據我們的經驗法則, 它的同伴應該是非成員,而他們的復合賦值對應者( =等),改變他們的左引數,應該是成員。這是和的示例 =代碼 ;其他二進制算術運算子應以相同的方式實作:
class X {
X& operator =(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator (X lhs, const X& rhs)
{
lhs = rhs;
return lhs;
}
operator =每個參考回傳其結果,同時operator 回傳其結果的副本。當然,回傳一個參考通常比回傳一個副本更有效,但在 的情況下operator ,沒有辦法繞過復制。當您撰寫 時a b,您希望結果是一個新值,這就是operator 必須回傳一個新值的原因。3
另請注意,它通過復制而不是 const 參考operator 獲取其左運算元。這樣做的原因與每個副本都采用其論點的原因相同。operator=
位操作運算子~ & | ^ << >>應以與算術運算子相同的方式實作。但是,(除了多載<<以及>>輸出和輸入)很少有合理的多載這些用例。
3 同樣,從中吸取的教訓a = b是,一般來說,效率更高a b,如果可能,應該首選。
陣列下標
陣列下標運算子是一個二元運算子,必??須作為類成員實作。它用于允許通過鍵訪問其資料元素的類似容器的型別。提供這些的規范形式是這樣的:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
除非您不希望您的類的用戶能夠更改回傳的資料元素operator[](在這種情況下您可以省略非常量變體),否則您應該始終提供運算子的兩種變體。
如果已知 value_type 參考內置型別,則運算子的 const 變體應該更好地回傳副本而不是 const 參考:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
類指標型別的運算子
要定義您自己的迭代器或智能指標,您必須多載一元前綴解參考運算子*和二元中綴指標成員訪問運算子->:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
請注意,這些也幾乎總是需要 const 和非 const 版本。對于->運算子,如果value_type是class(or structor union)型別,operator->()則遞回呼叫another,直到anoperator->()回傳非型別別的值。
一元尋址運算子永遠不應該被多載。
operator->*()看到這個問題。它很少使用,因此很少超載。事實上,即使是迭代器也不會多載它。
繼續轉換運算子
uj5u.com熱心網友回復:
C 運算子多載的三個基本規則
當談到 C 中的運算子多載時,您應該遵循三個基本規則。與所有此類規則一樣,確實存在例外情況。有時人們偏離了它們,結果并不是糟糕的代碼,但這種積極的偏離很少見。至少,我看到的 100 個這樣的偏差中有 99 個是不合理的。但是,1000 分中有 999 分也是可能的。所以您最好遵守以下規則。
每當運算子的含義不明顯且無可爭議時,不應多載。 相反,提供一個具有精心選擇的名稱的函式。
基本上,多載運算子的首要規則的核心是:不要這樣做。這可能看起來很奇怪,因為有很多關于運算子多載的知識,所以很多文章、書籍章節和其他文本都涉及所有這些。但是,盡管有這些看似顯而易見的證據,但只有少數情況適合運算子多載. 原因是實際上很難理解運算子應用程式背后的語意,除非運算子在應用程式領域中的使用是眾所周知且無可爭議的。與普遍的看法相反,這幾乎不是事實。始終堅持運營商眾所周知的語意。
C 對多載運算子的語意沒有限制。您的編譯器將愉快地接受實作二元運算子的代碼,以從其右運算元中減去。但是,此類運算子的用戶絕不會懷疑運算式a bto subtractafromb。當然,這假設應用程式域中運算子的語意是無可爭議的。始終提供所有相關操作的集合。
運算子彼此相關并與其他操作相關。如果您的型別支持a b,用戶也希望能夠呼叫a = b。如果它支持 prefix incrementa,他們也希望a能正常作業。如果他們可以檢查是否a < b,他們一定會期望也可以檢查是否a > b。如果他們可以復制構造您的型別,他們希望賦值也能正常作業。
繼續會員與非會員之間的決定。
uj5u.com熱心網友回復:
會員與非會員之間的決定
二元運算子=(賦值)、[](陣列訂閱)、->(成員訪問)以及 n 元()(函式呼叫)運算子必須始終作為成員函式實作,因為語言的語法要求它們這樣做。
其他運營商可以作為成員或非成員來實作。然而,其中一些通常必須作為非成員函式來實作,因為您不能修改它們的左運算元。其中最突出的是輸入和輸出運算子<<和>>,其左運算元是標準庫中的流類,您無法更改。
對于必須選擇將它們實作為成員函式或非成員函式的所有運算子,請使用以下經驗法則來決定:
- 如果它是一元運算子,則將其實作為成員函式。
- 如果二元運算子平等對待兩個運算元(保持它們不變),則將此運算子實作為非成員函式。
- 如果一個二元運算子不平等對待它的兩個運算元(通常它會改變它的左運算元) ,如果它必須訪問運算元的私有部分,那么讓它成為它的左運算元型別的成員函式可能是有用的。
當然,與所有經驗法則一樣,也有例外。如果你有一個型別
enum Month {Jan, Feb, ..., Nov, Dec}
并且你想為它多載遞增和遞減運算子,你不能將其作為成員函式來執行,因為在 C 中,列舉型別不能有成員函式。所以你必須將它多載為一個自由函式。operator<()對于嵌套在類模板中的類模板,當在類定義中作為行內成員函式完成時,更容易撰寫和閱讀。但這些確實是罕見的例外。
(但是,如果你做了一個例外,不要忘記運算元的const-ness 問題,對于成員函式,它成為隱式this引數。如果作為非成員函式的運算子將其最左邊的引數作為const參考, 與成員函式相同的運算子需要const在末尾有*this一個作為const參考。)
繼續普通運算子多載。
uj5u.com熱心網友回復:
C 運算子多載的一般語法
您不能更改 C 中內置型別的運算子的含義,只能為用戶定義的型別1多載運算子。也就是說,至少一個運算元必須是用戶定義的型別。與其他多載函式一樣,運算子只能為一組特定引數多載一次。
并非所有運算子都可以在 C 中多載。不能多載的運算子包括:. :: sizeof typeid .*C 中唯一的三元運算子,?:
在 C 中可以多載的運算子包括:
- 算術運算子:
-*/%和=-=*=/=%=(所有二進制中綴);-(一元前綴);--(一元前綴和后綴) - 位操作:
&|^<<>>和&=|=^=<<=>>=(所有二進制中綴);~(一元前綴) - 布爾代數:(
==!=<><=>=||&&所有二進制中綴);!(一元前綴) - 記憶體管理:
newnew[]deletedelete[] - 隱式轉換運算子
- 雜項:(
=[]->->*,所有二進制中綴);*&(所有一元前綴)()(函式呼叫,n 元中綴)
然而,您可以多載所有這些并不意味著您應該這樣做。請參閱運算子多載的基本規則。
在 C 中,運算子以具有特殊名稱的函式的形式多載。與其他函式一樣,多載運算子通常可以作為其左運算元型別的成員函式或非成員函式來實作。您是可以自由選擇還是必須使用其中任何一個取決于幾個標準。2應用于物件 x的一元運算子@3operator@(x)被呼叫為 as或 as x.operator@()。@應用于物件xand的二元中綴運算子y稱為 asoperator@(x,y)或 as x.operator@(y)。4個
作為非成員函式實作的運算子有時是其運算元型別的友元。
1 “用戶定義”一詞可能有點誤導。C 區分內置型別和用戶定義型別。前者屬于例如 int、char 和 double;后者屬于所有 struct、class、union 和 enum 型別,包括來自標準庫的型別,即使它們本身并未由用戶定義。
2 這將在本常見問題解答的后面部分進行介紹。
3 The@不是 C 中的有效運算子,這就是我將其用作占位符的原因。
4 C 中唯一的三元運算子不能多載,唯一的 n 元運算子必須始終作為成員函式實作。
繼續C 運算子多載的三個基本規則。
uj5u.com熱心網友回復:
轉換運算子(也稱為用戶定義的轉換)
在 C 中,您可以創建轉換運算子,這些運算子允許編譯器在您的型別和其他定義的型別之間進行轉換。有兩種型別的轉換運算子,隱式的和顯式的。
隱式轉換運算子(C 98/C 03 和 C 11)
隱式轉換運算子允許編譯器將用戶定義型別的值隱式轉換(如 和 之間的轉換int)long為其他型別。
下面是一個帶有隱式轉換運算子的簡單類:
class my_string {
public:
operator const char*() const {return data_;} // This is the conversion operator
private:
const char* data_;
};
隱式轉換運算子,如單引數建構式,是用戶定義的轉換。當嘗試匹配對多載函式的呼叫時,編譯器將授予一個用戶定義的轉換。
void f(const char*);
my_string str;
f(str); // same as f( str.operator const char*() )
起初這似乎很有幫助,但問題是隱式轉換甚至在不期望的時候啟動。在下面的代碼中,void f(const char*)將被呼叫,因為my_string()不是左值,所以第一個不匹配:
void f(my_string&);
void f(const char*);
f(my_string());
初學者很容易犯這個錯誤,甚至有經驗的 C 程式員有時也會感到驚訝,因為編譯器選擇了他們沒有懷疑的多載。這些問題可以通過顯式轉換運算子來緩解。
顯式轉換運算子 (C 11)
與隱式轉換運算子不同,顯式轉換運算子永遠不會在您不希望它們出現時出現。下面是一個帶有顯式轉換運算子的簡單類:
class my_string {
public:
explicit operator const char*() const {return data_;}
private:
const char* data_;
};
注意explicit. 現在,當您嘗試從隱式轉換運算子執行意外代碼時,您會收到編譯器錯誤:
prog.cpp: 在函式“int main()”中: prog.cpp:15:18: 錯誤:沒有用于呼叫“f(my_string)”的匹配函式 prog.cpp:15:18:注意:候選人是: prog.cpp:11:10: 注意:void f(my_string&) prog.cpp:11:10: 注意:引數 1 沒有從“my_string”到“my_string&”的已知轉換 prog.cpp:12:10: 注意:void f(const char*) prog.cpp:12:10: 注意:引數 1 沒有從“my_string”到“const char*”的已知轉換
要呼叫顯式強制轉換運算子,您必須使用static_cast、C 風格強制轉換或建構式風格強制轉換(即T(value))。
但是,有一個例外:允許編譯器隱式轉換為bool. 此外,不允許編譯器在轉換為之后再進行一次隱式轉換bool(編譯器一次允許進行 2 次隱式轉換,但最多只能進行 1 次用戶定義的轉換)。
因為編譯器不會強制轉換 "past" bool,顯式轉換運算子現在不再需要Safe Bool 習慣用法。例如,C 11 之前的智能指標使用 Safe Bool 習慣用法來防止轉換為整數型別。在 C 11 中,智能指標使用顯式運算子代替,因為不允許編譯器在將型別顯式轉換為 bool 后隱式轉換為整數型別。
繼續多載new和delete。
uj5u.com熱心網友回復:
多載new和delete運算子
注意:這只涉及多載and的語法,不涉及此類多載運算子的實作。我認為多載的語意應該有自己的常見問題解答,在運算子多載的主題中我永遠無法做到公正。newdeletenewdelete
基本
在 C 中,當您撰寫一個new 運算式時,在計算此運算式時會new T(arg)發生兩件事:首先operator new呼叫以獲取原始記憶體,然后T呼叫適當的建構式將此原始記憶體轉換為有效物件。同樣,當你洗掉一個物件時,首先呼叫它的解構式,然后將記憶體回傳給operator delete.
C 允許您調整這兩個操作:記憶體管理和在分配的記憶體中構造/銷毀物件。后者是通過為類撰寫建構式和解構式來完成的。微調記憶體管理是通過撰寫你自己的operator newand來完成的operator delete。
運算子多載的第一條基本規則——不要這樣做——尤其適用于多載newand delete。幾乎使這些運算子過載的唯一原因是性能問題和記憶體限制,并且在許多情況下,其他操作(例如更改所使用的演算法)將提供比嘗試調整記憶體管理更高的成本/收益比。
C 標準庫帶有一組預定義的newanddelete運算子。最重要的是這些:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
void* operator new[](std::size_t) throw(std::bad_alloc);
void operator delete[](void*) throw();
前兩個為一個物件分配/解除分配記憶體,后兩個為物件陣列。如果您提供自己的版本,它們不會超載,而是替換標準庫中的版本。
如果你多載operator new,你也應該總是多載匹配operator delete,即使你從未打算呼叫它。原因是,如果建構式在計算 new 運算式時拋出例外,運行時系統會將記憶體回傳給匹配物件,該operator delete匹配operator new物件被呼叫以分配記憶體以在其中創建物件。如果您不提供匹配物件operator delete,呼叫默認的,這幾乎總是錯誤的。
如果多載newand delete,您也應該考慮多載陣列變體。
放置new
C 允許 new 和 delete 運算子使用額外的引數。
所謂的 placement new 允許您在某個地址創建一個物件,該地址將傳遞給:
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
為此,標準庫帶有 new 和 delete 運算子的適當多載:
void* operator new(std::size_t,void* p) throw(std::bad_alloc);
void operator delete(void* p,void*) throw();
void* operator new[](std::size_t,void* p) throw(std::bad_alloc);
void operator delete[](void* p,void*) throw();
請注意,在上面給出的放置 new 的示例代碼中,operator delete永遠不會呼叫,除非 X 的建構式拋出例外。
您還可以使用其他引數多載newand 。delete與 placement new 的附加引數一樣,這些引數也列在關鍵字后的括號內new。僅出于歷史原因,此類變體通常也稱為 placement new,即使它們的引數不是將物件放置在特定地址。
類特定的新建和洗掉
最常見的情況是您希望微調記憶體管理,因為測量表明特定類或一組相關類的實體經常被創建和銷毀,并且運行時系統的默認記憶體管理已針對一般性能,在這種特定情況下處理效率低下。為了改善這一點,您可以為特定類多載 new 和 delete:
class my_class {
public:
// ...
void* operator new(std::size_t);
void operator delete(void*);
void* operator new[](std::size_t);
void operator delete[](void*);
// ...
};
因此,多載后,new 和 delete 的行為類似于靜態成員函式。對于 的物件my_class,std::size_t引數始終為sizeof(my_class)。然而,這些運算子也被呼叫用于派生類的動態分配物件,在這種情況下它可能比那個更大。
全域新建和洗掉
要多載全域 new 和 delete,只需將標準庫的預定義運算子替換為我們自己的即可。但是,很少需要這樣做。
uj5u.com熱心網友回復:
為什么將operator<<物件流式傳輸到std::cout檔案或流式傳輸到檔案的函式不能成為成員函式?
假設您有:
struct Foo
{
int a;
double b;
std::ostream& operator<<(std::ostream& out) const
{
return out << a << " " << b;
}
};
鑒于此,您不能使用:
Foo f = {10, 20.0};
std::cout << f;
由于operator<<是 作為 的成員函式多載的Foo,因此運算子的 LHS 必須是一個Foo物件。這意味著,您將需要使用:
Foo f = {10, 20.0};
f << std::cout
這是非常不直觀的。
如果將其定義為非成員函式,
struct Foo
{
int a;
double b;
};
std::ostream& operator<<(std::ostream& out, Foo const& f)
{
return out << f.a << " " << f.b;
}
您將能夠使用:
Foo f = {10, 20.0};
std::cout << f;
這是非常直觀的。
uj5u.com熱心網友回復:
為了簡短起見,我將參考過去一周我在學習 Python 和 C 時遇到的一些要點,oops 和其他東西,所以它如下所示:
運營商的 Arity 不能比它是什么更進一步修改!
多載運算子只能有一個默認引數,函式呼叫運算子不能保留它。
只有內置運算子可以多載,其余不能!
有關詳細資訊,您可以參考以下鏈接,該鏈接會將您重定向到 GeekforGeeks 提供的檔案。
https://www.geeksforgeeks.org/g-fact-39/
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/534990.html
上一篇:為什么在單獨的回圈中按元素添加比在組合回圈中快得多?
下一篇:什么是三法則?
