C++基礎::語法特性::函式重寫(override)與協變返回型別(covariant return type)
函式重寫
在進行本文的協變返回型別(covariant return type)的討論之前,不妨先重新溫故C++關於函式重寫的語法規則。協變返回型別與函式重寫有著千絲萬縷的聯絡。
首先明確函式過載(overload)與函式重寫(override )之間的差異:
函式過載與函式重寫
函式過載
同名不同參(不同的引數型別、不同的引數個數、不同的引數順序)
函式重寫
也稱為覆蓋,主要在繼承關係中體現(也即是讓子類去重寫父類中的虛擬函式),子類重新定義父類中同名同參的虛擬函式。
函式重寫的條件與注意
基本條件:
派生類重寫的函式與基類中被重寫的函式必須都是virtual函式,當然子類中的函式重寫不必顯式宣告virtual虛擬函式。
引數必須保持一致,否則就是子類中自己的成員函式,未構成函式重寫。
重寫的函式與被重寫的函式返回值型別相同,或者當返回指標或者引用時(不包括vaue語義),子類中重寫的函式返回的指標或者引用是父類中被重寫函式所返回指標或引用的子型別(這就是所謂的covariant return type:協同返回型別)
子類中的函式與父類中的同名函式具有相同的引數和返回值型別時,但如果一個是const函式(non-mutable)、一個是非const函式(mutable),不構成函式重寫。
注意:
重寫的函式所丟擲的異常必須和被重寫的函式丟擲的異常一致,或者是其子類
重寫的函式的訪問修飾符可以不同於被重寫的函式,如基類中被重寫的函式訪問修飾符為
private
,派生類中重寫的函式的訪問修飾符可以為public
或protected
。靜態方法不能被重寫,因為
static
與virtual
不能同時被使用。子類中重寫的函式
virtual
關鍵字可以加也可以不加, 本質上都是虛擬函式的屬性,只要與基類中的相關函式構成重寫關係。
我們以一個具體例項分析C++的函式重寫機制吧:
class A{};
class B:public A{};
class C
{
public:
virtual void func1()
{ cout << "C::func1()" << endl;}
virtual A* func2()
{
cout << "C::func2()" << endl;
return new A;
}
virtual A& func3()
{
cout << "C::func3()" << endl;
return *(new A);
// 這裡返回的必須是一個堆物件(heap object),
// 而不能是棧物件(stack object)或者叫區域性物件
// 因為棧物件會在函式退出時被銷燬,皮之不存毛將焉附
// 物件都不存在了,引用自然無所適從
}
void showFunc4()
{
func4();
}
virtual void func5() const
{
cout << "C::func5()" << endl;
}
virtual A func6()
{
cout << "C::func6()" << endl;
}
private:
virtual void func4()
{
cout << "C::func4()" << endl;
}
};
class D:public C
{
public:
// 構成重寫
void func1()
{
cout << "D::func1()" << endl;
}
// 返回值的型別是父類返回值型別的子類的指標,構成重寫
B* func2()
{
cout << "D::func2()" << endl;
return new B;
}
// 返回值的型別是父類返回值型別的子類的引用,構成重寫
B& func3()
{
cout << "D::func3()" << endl;
return *(new B);
}
// 可以對訪問修飾符進行重寫
void func4()
{
cout << "D::func4()" << endl;
}
// 不構成重寫
void func5()
{
cout << "D::func5()" << endl;
}
// 無法通過編譯
//B func6()
//{
// cout << "D::func6()" << endl;
//}
};
int main(int, char**)
{
C* c = new D; // 父類指標指向子類物件
c->func1();
c->func2();
c->func3();
c->showFunc4();
c->func5();
return 0;
}
分析這種問題的套路:先判斷是否構成重寫關係,再看是否是指標或者引用指向的是父類物件還是子類物件,如果是父類指標指向子類物件,且構成重寫關係,將會呼叫子類中的重寫函式。
D::func1()
D::func2()
D::func3()
D::func4()
C::func5()
協變返回型別
有了上述的準備,我們便比較容易理解作為C++函式重寫機制中的一個小的環節的協變返回型別
了,上述的D::func2(), D::func3()
正是對這一語法機制的運用。
我們來看一個更加具體的例子,一般來說,子類對父類某一函式進行重寫,必須具備相同的返回值型別。
class Shape
{
public:
virtual double area() const = 0;
}
class Circle :public Shape
{
public:
float area() const; // 錯誤,返回值型別不同
}
這一規則對covariant return type
卻有所放鬆,也即,子類對父類的某一函式進行重寫時,子類函式的返回值型別可以父類函式返回值型別的子類的指標或者引用型別,語義上要求子類與父類能夠構成一種is-a
關係。
class Shape
{
public:
virtual Shape* clone() const = 0;
}
class Circle:public Shape
{
public:
Circle* clone() const;
}
重寫的派生類函式被宣告為返回一個Circle*
而不是繼續Shape*
,因為語義上,Circle是一個Shape。這在什麼情況下會產生便捷性呢?直接操縱派生類型別而不是通過基類介面來操縱它們時,協變返回型別的優勢便會體現出來:
Circle* c = getACircle();
Circle* c2 = c->clone();
c2->someFunc();
// 如此不經過型別轉換我們便可實現對子類函式的呼叫
如果不使用協變返回型別機制,或者不存在協變返回型別機制,Circle::clone將不得不精確地匹配Shape::clone的返回型別,從而返回一個Shape*,我們就必須被迫將結果轉換為Circle*。
Circle* c = getACircle();
Circle* c2 = dynamic_cast<Circle*>(c->clone());
再看另外一個例子,考慮Shape中的Factory Method成員,它返回一個引用,指向與具體的形狀對應的形狀編輯器:
class ShapeEditor{};
class Shape
{
public:
const ShapeEditor& getEditor() const = 0;
// Factory method
};
class CircleEditor:public ShapeEditor{};
class Circle: public Shape
{
public:
const CircleEditor& getEditor() const {}
}
協變返回型別的最大或者說根本優勢在於,讓程式設計師在適度程度的抽象層面工作。如果我們是處理Shape,將獲得一個抽象的ShapeEditor
;若正在處理某種具體的形狀型別,比如Circle,我們便可直接獲得CircleEditor
。協變返回型別將使我們從這樣的一種處境中解脫出來:不得不使用易於出錯的動態型別轉換操作(dynamic_cast
Shape* s = getAShape();
const ShapeEditor& se = s->getEditor();
Circle* c = getACirle();
const CircleEditor& ce = c->getEditor();
一個協變返回型別的例項
enum AttrType { Unknown, Continuous, Discrete};
class AttrInfo
{
public:
AttrInfo(const std::string& name, AttrType type)
:_type(type), _name(name)
{}
virtual AttrInfo* clone() const = 0;
private:
AttrType _type;
std::string _name;
};
class CAttrInfo :public AttrInfo
{
public:
// ...
CAttrInfo* clone() const
{
return (new CAttrInfo(*this));
}
// ...
}
class DAttrInfo :public AttrInfo
{
public:
// ...
DAttrInfo* clone() const
{
return (new DAttrInfo(*this));
}
// ...
}
如上述程式碼所示,父類(抽象基類)中定義的被重寫的函式返回型別是AttrInfo*
(父類型別的指標),而兩個派生類各自對clone
函式的重寫版本返回型別為CAttrInfo*
和DAttrInfo*
,因為有了協變返回型別(covariant return type)的存在,這樣的語法特性才得以支援。
重定義(redefining)
concept
也叫隱藏,子類重新定義父類中的非虛擬函式,遮蔽了父類的同名函式。
基本條件
函式與被其隱藏的函式作用域不同,也就是函式在子類中宣告,被其隱藏的函式在父類中宣告。
note
- 同名,不同參,無論父類的函式是否為virtual,都將被隱藏
- 同名,同參,如果父類的函式不是virtual,將被隱藏
References
[1] <C++過載、重寫、重定義區別>
[2] <協變返回型別>
相關文章
- Go 之基礎速學 (二) 語法套路:型別、定義函式、返回值Go型別函式
- GO語言基礎(結構+語法+型別+變數)Go型別變數
- Python基礎入門_2基礎語法和變數型別Python變數型別
- 第一課 php基礎語法 變數 函式PHP變數函式
- golang基礎語法,定義函式型別 為已存在的資料型別起別名Golang函式資料型別
- 【重溫基礎】1.語法和資料型別資料型別
- python 基礎語法 - 函式(一)Python函式
- JavaScript新增型別語法Type SyntaxJavaScript型別
- C 語言中,如果函式宣告瞭返回型別,但執行路徑中沒有 return 語句,則返回什麼資料值呢?函式型別
- Java基礎-基礎語法-變數與常量Java變數
- 物件導向重寫(override)與過載(overload)區別物件IDE
- C++基礎語法C++
- 方法重寫(Override)IDE
- 過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分IDE型別
- iOS-關鍵字-泛型ObjectType 協變__covariant 逆變__contravariantiOS泛型Object
- Java 重寫(Override)與過載(Overload)JavaIDE
- Java基礎語法之資料型別Java資料型別
- C++八股之函式過載與重寫-靜態多型與動態多型C++函式多型
- Python語法--可變型別和不可變型別Python型別
- Java-override重寫與overload過載JavaIDE
- 19、Overload和Override的區別。Overloaded的方法是否可以改變返回值的型別?IDE型別
- Go變數與基礎資料型別Go變數資料型別
- Flutter (一) Dart 語言基礎詳解(變數、內建型別、函式、操作符、流程控制語句)FlutterDart變數型別函式
- 函式的提升與重寫函式
- Golang 基礎值速學之十九(函式作為值與型別)Golang函式型別
- override(重寫) and overload(過載)IDE
- Python基礎(一)可變與不可變資料型別Python資料型別
- JavaScript基礎複習(一) 語言特性及資料型別JavaScript資料型別
- Go 基礎教程--3-型別與變數Go型別變數
- 重學C語言_資料結構與基礎語法C語言資料結構
- C語言基礎函式C語言函式
- SQL語言基礎(函式)SQL函式
- 【重溫基礎】4.函式函式
- 『Java 語法基礎』a = a + b 與 a += b 的區別Java
- C語言語法基礎--S2函式和指標C語言函式指標
- fill函式與memset函式的區別(c++)函式C++
- scala語法 - 方法與函式函式
- Vuejs 基礎與語法VueJS
- 【Python基礎】Python 函式返回多個值和函式註解Python函式