C++精進篇(八)之―函式重定義、過載、重寫、重入
C++函式重定義、過載、重寫、重入
1. 重寫 (override):
父類與子類之間的多型性。子類重新定義父類中有相同名稱和引數的虛擬函式。
函式重寫的特點:
(1)不同的範圍,分別位於基類和派生類中
(2)重寫函式必須有相同的型別,名稱和引數列表 (即相同的函式原型)
(3)重寫函式的訪問修飾符可以不同。儘管 virtual 是 private 的,派生類中重寫改寫為 public,protected 也是可以的
(4)被重寫的函式不能是 static 的。基類函式必須有virtual關鍵字 ( 即函式在最原始的基類中被宣告為 virtual ) 。
特殊情況:若派生類重寫的虛擬函式屬於一個過載版本,則該重寫的函式會隱藏基類中與虛擬函式同名的其他函式。
作用效果:父類的指標或引用根據傳遞給它的子類地址或引用,動態地呼叫屬於子類的該函式。這個晚繫結過程只對virtual函式起作用。具體原理是由虛擬函式表(VTABLE)決定的。
2. 過載 (overload):
指函式名相同,但是它的引數表列個數或順序,型別不同。但是不能靠返回型別來判斷。
函式過載的特點:
(1)相同的範圍(在同一個類中)
(2)函式名字相同
(3)形參列表不同(可能是引數個數 or 型別 or 順序不同),返回值無要求
特殊情況:若某一個過載版本的函式前面有virtual修飾,則表示它是虛擬函式。但它也是屬於過載的一個版本
不同的建構函式(無參構造、有參構造、拷貝構造)是過載的應用
作用效果和原理:編譯器根據函式不同的引數表,將函式體與函式呼叫進行早繫結。過載與多型無關,只是一種語言特性,與面向物件無關。
(4)virtual關鍵字可有可無
3. 重定義 (redefining):
子類重新定義父類中有相同名稱的非虛擬函式 ,重定義(隱藏)指派生類的函式遮蔽了與其同名的基類函式( 引數列表可以不同 ) 。
(1)如果派生類的函式和基類的函式同名,但是引數不同,此時,不管有無virtual,基類的函式被隱藏。
(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有vitual關鍵字,此時,基類的函式被隱藏。
函式重定義的特點:
(1)不在同一個作用域(分別位於基類、派生類)
(2)函式的名字必須相同
(3)對函式的返回值、形參列表無要求
特殊情況:若派生類定義的該函式與基類的成員函式完全一樣(返回值、形參列表均相同),且基類的該函式為virtual,則屬於派生類重寫基類的虛擬函式。
作用效果:若重新定義了基類中的一個過載函式,則在派生類中,基類中該名字的函式(即其他所有過載版本)都被自動隱藏,包括同名的虛擬函式。
4. 重寫與過載的區別 (override) PK (overload)
(1) 方法的重寫是子類和父類之間的關係,是垂直關係;方法的過載是同一個類中方法之間的關係,是水平關係。
(2) 重寫要求引數列表相同;過載要求引數列表不同。
(3) 重寫關係中,呼叫那個方法體,是根據物件的型別(物件對應儲存空間型別)來決定;過載關係,是根據呼叫時的實參表與形參表來選擇方法體的。
示例1:
class Base
{
private:
virtual voiddisplay() { cout<<"Base display()"<<endl; }
voidsay(){ cout<<"Base say()"<<endl; }
public:
void exec(){display(); say(); }
voidf1(string a) { cout<<"Base f1(string)"<<endl; }
void f1(inta) { cout<<"Base f1(int)"<<endl; } //overload
};
class DeriveA:public Base
{
public:
voiddisplay() { cout<<"DeriveA display()"<<endl;} //override
void f1(inta,int b) { cout<<"DeriveA f1(int,int)"<<endl;} //redefining
void say() {cout<<"DeriveA say()"<<endl; } //redefining
};
class DeriveB:public Base
{
public:
void f1(inta) { cout<<"DeriveB f1(int)"<<endl; } //redefining
};
int main()
{
DeriveA a;
Base *b=&a;
b->exec(); //display():version of DeriveAcall(polymorphism)
//say():version of Base called(allways )
a.exec(); //same result as laststatement
a.say();
//a.f1(1); //error:no matchingfunction, hidden !!
DeriveB c;
c.f1(1); //version of DeriveBcalled
}
示例2:
#include <iostream>
#include <windows.h>
using namespace std;
class Base
{
public:
int a;
Base()
{
cout << "Base() 無參建構函式!" << endl;
}
Base(int data)
{
a = data;
cout << "Base(int data) 有參建構函式!" << endl;
}
Base(const Base &tmp)
{
a = tmp.a;
cout << "Base 拷貝建構函式!!" << endl;
}
//基類中對函式名fuc1,進行了過載。其中最後一個過載函式為虛擬函式
void fuc1()const
{
cout << "呼叫 void Base::fuc1()" << endl;
}
//overload
int fuc1(int data) const
{
cout << "呼叫 Base fuc1(int data)" << endl;
return 1;
}
//overload 虛擬函式
virtual double fuc1(int dataA, int dataB)
{
cout << "呼叫 Base fuc1(int a,int b)" << endl;
return dataA*dataB / 2.0;
}
};
class Derived : public Base
{
public:
//先呼叫基類的建構函式,然後物件成員的建構函式,最後才是派生類的建構函式
Base base;
Derived()
{
cout << "Derived() 無參建構函式被呼叫!" << endl;
}
//對基類的函式名fuc1,進行了重定義。則會隱藏基類中的其他fuc1函式
int fuc1() const
{
cout << " 呼叫 Derived fuc1()函式" << endl;
return 1;
}
//重寫基類的虛擬函式
//redefine override
double fuc1(int dataA, int dataB)
{
cout << "呼叫 Derived fuc1(int dataA,int dataB)函式" << endl;
return (dataA + dataB) / 2.0;
}
};
int main()
{
//-----test 1------------------------
cout << "-------test 1------------" << endl;
//Base class
Base base;
base.fuc1();
base.fuc1(12);
base.fuc1(10, 20);
//derived class
Derived derived;
derived.fuc1();
//derived.fuc1(2); //error C2661: 'fuc1' : no overloaded function takes 1 parameters
derived.fuc1(10, 20);
//--------test 2----------------------------------
cout << "-------test 2------------" << endl;
Base b = derived;
b.fuc1();
b.fuc1(10, 20);//呼叫的是基類的函式,不發生多型
//--------test 3----------------------------------------
cout << "-------test 3------------" << endl;
Base &bR = derived;//引用
b.fuc1();//fuc1()不是虛擬函式,呼叫基類的函式
bR.fuc1(10, 20);//呼叫的是派生類的函式,發生多型
//--------test 4--------------------------------------
cout << "-------test 4------------" << endl;
Base* pB = &derived;
b.fuc1();
pB->fuc1(10, 20);//呼叫的是派生類的函式,發生多型
Sleep(50000);
return 1;
}
分析:test 1中進行了過載測試,根據傳遞引數的不一樣,呼叫不同的函式 (早繫結,與多型無關)。test 2中Test b = d;定義了一個基類物件,用派生類物件來進行初始化。這會呼叫基類的拷貝建構函式,生成基類的物件b,基類的拷貝建構函式初始化b的VPTR,指向b的VTABLE。因此所有的函式呼叫都只發生在基類,不會產生多型。
這是一個物件切片過程(參見《C++程式設計思想.第二版》),物件切片是當它拷貝到一個新的物件時,會去掉原來物件的一部分,而不是像使用指標或引用那樣簡單地改變地址的內容。test 3和test 4中,定義的基類指標和引用,故會發生多型。注意:在 C++ 中若基類中有一個函式名被過載,在子類中重定義該函式,則基類的所有版本將被隱藏——即子類只能用子類定義的,基類的不再可用。基類class Base中定義了名為fuc1的3個過載函式,其中最後一個是虛擬函式; 派生類class Derived中對fuc1進行了重定義,所以會隱藏基類中名為fuc1的版本。其中派生類的double fuc1(int dataA,int dataB)屬於對虛擬函式的重寫。
5. 函式重入 一個函式在執行的過程中,還可以被另外一個地方呼叫執行。比如遞迴函式呼叫,多執行緒的多個執行緒同時執行一個函式。重入也就是同時執行一個函式的意思。而可重入表示的是一個函式是否可以同時執行而不出錯。如果一個函式執行過程會記錄狀態,比如使用全域性變數記錄,或者使用類成員變數記錄,然後根據狀態執行不同的程式碼,這樣就會導致第一個執行的和第二個同時執行的效果不一樣,因為第一個執行的已經改變了狀態,導致第二個執行的就是另外一個效果。這樣的表示不可重入。 所以,要讓函式重入,請保證不在函式中不可記錄狀態、將函式獨立成一個完整功能體,不與外部交涉。傳進去一個引數一定得到一個固定的結果,不管是多少個同時執行都如此。