《隨筆九》—— C++中的 “ 虛擬函式 ”
目錄
看不懂可以先看這個:《隨筆八》—— C++中的“ 多型中的靜態聯編 、動態聯編”https://blog.csdn.net/qq_34536551/article/details/84195882
虛擬函式的作用:
在同一個類中不能定義兩個函式原型相同的函式, 否則就是重複定義。 但是在類的繼承層次結構中,在不同的層次可以出現函式原型相同而功能不同的函式, 編譯器按照同名覆蓋的原則決定呼叫的哪個函式。 然後我們就可以用同一個呼叫形式, 既能呼叫派生類的虛擬函式又能呼叫基類的同名虛擬函式。
最後我們使用基類物件的指標 或者 基類物件的引用,指向派生類物件, 這樣我們就可以用該指標或者引用 訪問 基類和派生類中的同名函式(它們都是虛擬函式)。
定義虛擬函式
● 虛擬函式的定義是在基類中進行的, 即把基類中需要定義為虛擬函式的成員函式宣告為 virtual、 當基類中的某個成員函式被宣告為虛擬函式後, 我們就可以在派生類中重新定義該函式, 但是注意的是該函式的原型必須跟基類中的那個函式原型一致。 虛擬函式的定義語法為:
virtual 返回型別 識別符號 (引數表)
{
// Satement
}
那麼需要注意的是: 虛擬函式是成員函式, 不能是 static 成員函式。
下面看一個程式程式碼:
class Animal { public: void sleep() { cout << "呼叫的是基類中的sleep" << endl; } virtual void breathe() { cout << "呼叫的是基類中的breathe" << endl; } }; class Fish :public Animal { public: void breathe() { cout << "呼叫的是派生類中的breathe" << endl; } }; int main() { Fish Myfish; Animal *an = &Myfish; an->breathe(); // 呼叫的是派生類中的該函式, 因為該函式是virtual 函式 an->Animal::breathe(); //呼叫的是基類中的該函式 system("pause"); return 0; }
在上述程式碼中, 將基類中的 breathe() 定義為虛擬函式,我們在主調函式中 定義 Animal 物件指標指向類 Fish的物件 Myfish, 呼叫 breathe ()函式,呼叫的就是派生類中的該函式, 如果該函式在基類中不是虛擬函式, 如果用這樣的方法呼叫breathe(), 那麼呼叫的就是基類中的該函式。
那麼,當將基類中的成員函式 breathe() 宣告為 virtual 時, 編譯器在編譯的時候 發現 Animal 類中有虛擬函式, 此時編譯器會為每個包含虛擬函式的類建立一個虛擬函式表, 該表是一個一維陣列, 在這個陣列中存放每個虛擬函式的地址。
在上述程式碼中 Animal 類 和 Fish 類都包含一個虛擬函式 breathe () , 因此編譯器會為這兩個類分別建立一個虛擬函式表,如圖所示:
在上述程式碼中, 當 Fish 類的 Myfish 的物件 構造完畢後, 其內部的虛表指標也被初始化為指向Fish 類的虛表。 在型別轉換後, 呼叫an->breathe(), 由於 an 實際指向的是Fish 類的物件, 該物件內部的虛表指標指向的Fish 類的虛表, 因此最終呼叫的是Fish 類的 breathe() 函式。
下面簡要說明宣告虛擬函式需要注意的問題:虛擬函式屬於它所在類層次結構, 而不是屬於某一個類, 在派生類中對基類的虛擬函式進行覆蓋時, 要求派生類中的宣告的虛擬函式與基類中的被覆蓋的虛擬函式之間滿足如下條件:
與基類的虛擬函式的函式原型完全相同。
它們的返回值型別都要一致。
虛擬函式的宣告只能出現在類定義中,不能出現在函式體定義的時候, 而且, 基類中只有保護成員和公有成員才能被宣告為虛擬函式。
在派生類中重新定義虛擬函式時, 關鍵字virtual 可以寫或不寫。
若在派生類中沒有重新定義虛擬函式時, 則該類的物件將使用其基類中的虛擬函式程式碼。
虛擬函式必須是類的成員函式, 不能是友元, 但它可以是另一個類的友元。
解構函式可以是virtual 函式, 但建構函式則不能是虛擬函式。
一個類的虛擬函式僅對派生類中重定義的虛擬函式其作用, 對其他函式沒有影響, 意識就是說, 只有被說明為虛擬函式的那個成員函式才具有多型性。
簡要說明使用基類指標或者基類引用指向派生類物件應注意的問題:
宣告為基類物件的指標或者引用可以指向它的公有派生類物件, 但不允許指向它的私有派生類物件。
允許宣告為基類物件的指標或者引用可以指向它的公有派生類物件, 但不允許宣告一個派生類物件的指標或引用指向基類的物件。
宣告為基類物件的指標或者引用時, 當其指向它的公有派生類的物件時, 只能直接訪問派生類中從基類繼承下來的成員, 不能直接訪問公有派生類中定義的成員。 要想訪問其公有派生類中的成員, 可將基類指標或者引用用顯式型別轉換方式轉換為派生類指標。
在派生類中重新定義基類中的虛擬函式, 是函式過載的另一種形式,虛擬函式跟函式過載的區別是:
一般的函式過載, 要求其函式名一樣, 但是引數的個數、順序、引數型別必須有所不同。
過載函式可以是成員函式或友元函式, 虛擬函式不能是友元函式。
過載函式的呼叫是以所傳遞的引數序列的差別作為呼叫不同函式的依據; 虛擬函式是根據物件的不同去呼叫不同類的虛擬函式。
虛擬函式在執行時表現出多型功能, 而過載函式則在編譯時表現出多型性。
過載一個虛擬函式時, 要求派生類中的該函式與基類中的該函式的函式原型完全相同。
如果僅僅返回型別不同,其餘相同, 則系統會給出錯誤資訊。
如果函式名相同,而引數個數、引數的型別或引數的順序不同, 虛擬函式的特性將被丟失。
注意: 有時我們在基類中定義的非虛擬函式會在派生類中被重新定義, 如果基類指標呼叫該函式, 那麼呼叫的是基類中的該函式;
如果說用派生類指標呼叫該同名函式 , 那麼呼叫的是派生類中的同名函式, 這並不是多型性(使用的是不同型別的指標), 並沒有用到虛擬函式的功能。
三種呼叫虛擬函式的方式比較
class father
{
public:
virtual void run() const
{
cout << "呼叫的是基類中的run函式!" << endl;
}
};
class son : public father
{
public:
virtual void run() const
{
cout << "呼叫的是派生類son中的run函式!" << endl;
}
};
class daughter :public father
{
public:
virtual void run() const
{
cout << "呼叫的是派生類中daughter的run函式!" << endl;
}
};
void one(father one)
{
one.run();
}
void two(father *two)
{
two->run();
}
void three(father &three)
{
three.run();
}
int main()
{
father *p = new son;
one(*p);
father *pp = new daughter;
two(pp);
father *ppp = new father;
three(*ppp);
delete p;
delete pp;
delete ppp;
system("pause");
return 0;
}
輸出結果為:
呼叫的是基類中的run函式!
呼叫的是派生類中daughter的run函式!
呼叫的是基類中的run函式!
在該程式碼中: one(*p); 該呼叫函式 one 傳遞了一個實參 “ *p” , 由於 p 是 son 類物件的記憶體地址, 因此 *p代表的是 son 物件, 我們可以看見我們是按值傳遞的, 輸出的結果是 “ 呼叫的是基類中的run函式! ” 我們呼叫的是 father 類的 成員函式 one,而不是 son類的成員函式 one。 所以說, 如果說 按物件名的方式呼叫虛擬函式, 虛擬函式不起作用,並沒有起到執行時的多型作用,採用的是靜態聯編的方式。
只有在使用基類物件的指標 或者引用時來呼叫虛擬函式時, 虛擬函式才能起到執行時的多型作用,才能實現在執行時的動態聯編, 所以說 後面的兩個指標 *pp 和 *ppp, 都起到了多型的作用, 輸出結果就可以看出。
虛擬函式的訪問方法
對於虛擬函式的訪問方法有:
基類物件的指標或者引用
物件名
類的成員函式訪問該類層次中的虛擬函式
用建構函式和解構函式
(1 ) 下面用第一種方式和第二種方式呼叫虛擬函式,看示例程式碼:
class Point
{
private:
float x, y;
public:
virtual double area()
{
return 1.1;
}
};
class Circle :public Point
{
private:
float radius;
public:
virtual double area()
{
return 2.2;
}
};
int main()
{
Circle cl;
Point *pp = &cl;
// 基類物件的指標指向公有派生類物件, 實現動態聯編
cout << "呼叫的是派生類中的area:" << pp->area() << endl;
cout << "呼叫的是基類中的area:" << pp->Point::area() << endl;
//下面這兩個使用物件名呼叫虛擬函式, 實現的是靜態聯編
cout << "呼叫的是派生類中的area:" << cl.area() << endl;
cout << "呼叫的是基類中的area:" << cl.Point::area() << endl << endl;
Point *p = new Circle;
cout << "呼叫的是派生類中的area:" << p->area() << endl;
cout << "呼叫的是基類中的area:" << p->Point::area() << endl << endl;
delete p;
// 基類物件的引用指向公有派生類物件, 實現動態聯編
Point &ppp = cl;
cout << "呼叫的是派生類中的area:" << ppp.area() << endl;
cout << "呼叫的是基類中的area:" << ppp.Point::area() << endl << endl; // 實現的是靜態聯編
system("pause");
return 0;
}
輸出結果為:
呼叫的是派生類中的area: 2.2
呼叫的是基類中的area: 1.1
呼叫的是派生類中的area: 2.2
呼叫的是基類中的area: 1.1
呼叫的是派生類中的area: 2.2
呼叫的是基類中的area: 1.1
呼叫的是派生類中的area: 2.2
呼叫的是基類中的area: 1.1
(2)下面用類的成員函式訪問該類層次中的虛擬函式
class A
{
public:
virtual void func1()
{
cout << "呼叫的是基類中的func1 虛擬函式!" << endl;
a1();
}
virtual void func2()
{
cout << "呼叫的是基類中的func2 虛擬函式!" << endl;
}
void a1()
{
cout << "呼叫的是基類中的a1 普通成員函式!" << endl;
func2();
}
};
class B : public A
{
public:
virtual void func2()
{
cout << "呼叫的是派生類中的func2 虛擬函式!" << endl;
}
};
int main()
{
A a;
a.func1();
cout << endl;
B b;
b.func1();
system("pause");
return 0;
}
輸出結果為:
呼叫的是基類中的func1 虛擬函式!
呼叫的是基類中的a1 普通成員函式!
呼叫的是基類中的func2 虛擬函式!
呼叫的是基類中的func1 虛擬函式!
呼叫的是基類中的a1 普通成員函式!
呼叫的是派生類中的func2 虛擬函式!
可以看出, 當用b.func1 呼叫虛擬函式時, 因為派生類中沒有該函式,所以呼叫基類中的該函式, 然後基類中的 func1函式中呼叫 a1函式, a1函式實際呼叫的是派生類中的func2 函式,從輸出結果可以看出。
(3) 使用建構函式和解構函式呼叫虛擬函式, 使用建構函式和解構函式將採用靜態聯編
#include<iostream>
#include<string>
#include<cstdlib>
using namespace std;
class A
{
public:
A()
{
cout << "呼叫的是基類中的建構函式!\n" << endl;
}
virtual void func1()
{
cout << "呼叫基類中的func1 函式!\n" << endl;
}
};
class B : public A
{
public:
B()
{
cout << "呼叫的是派生類中B的建構函式!\n" << endl;
func1(); // 建構函式訪問虛擬函式
A::func1();
}
virtual void func1()
{
cout << "呼叫派生類中B的func1 函式!\n" << endl;
}
~B() //解構函式訪問虛擬函式
{
func1();
A::func1();
}
};
int main()
{
{
B b;
}
system("pause");
return 0;
}
輸出結果為:
呼叫的是基類中的建構函式!
呼叫的是派生類中B的建構函式!
呼叫派生類中B的func1 函式!
呼叫基類中的func1 函式!
呼叫派生類中B的func1 函式!
呼叫基類中的func1 函式!
該文章後期還會更新修改。