C++多型和虛擬函式詳解

2020-07-16 10:04:43
如果用不同型別的資料執行程式碼會產生不同的行為,那麼該段程式碼就認為是多型的。例如,如果一個函數在傳遞不同型別的引數時執行不同的操作,那麼稱之為是多型的。

為了說明多型性,請看下面的程式:
//Inheritance4.h 的內容
#include <string>
#include <memory>
using namespace std;

enum class Discipline { ARCHEOLOGY, BIOLOGY, COMPUTER_SCIENCE };
enum class Classification { FRESHMAN, SOPHOMORE, JUNIOR, SENIOR };
class Person
{
    protected:
        string name;
    public:
        Person() { setName(""); }
        Person(const string& pName) { setName(pName); }
        void setName(const string& pName) { name = pName; }
        string getName() const { return name; }
};

class Student:public Person
{
    private:
        Discipline major;
        shared_ptr<Person> advisor;
    public:
        Student(const string& sname, Discipline d, const shared_ptr<Person>& adv) : Person(sname)
        {
            major = d;
            advisor = adv;
        }
        void setMajor(Discipline d) { major = d; }
        Discipline getMajor() const { return major; }
        void setAdvisor(shared_ptr<Person> p) { advisor = p; }
        shared_ptr<Person> getAdvisor() const { return advisor; }
};

class Faculty :public Person
{
    private:
        Discipline department;
    public:
        Faculty(const string& fname, Discipline d) : Person(fname)
        {
            department = d;
        }
        void setDepartment(Discipline d) { department = d; }
        Discipline getDepartment() const { return department; }
};

class TFaculty : public Faculty
{
    private:
        string title;
    public:
        TFaculty(const string& fname, Discipline d, string title):Faculty(fname, d)
        {
            setTitle(title);
        }
        void setTitle (const string& title) { this->title = title; }
        // Override getName()
        string getName() const
        {
            return title + " " + Person::getName();
        }

};
//main主程式
// This exhibits the default non-polymorphic behavior of C++.
#include "inheritance4.h"
#include <vector>
#include <iostream>
using namespace std;
int main()
{
    // Create a vector of pointers to Person objects
    vector<shared_ptr<Person>> people
    {
        make_shared<TFaculty>
        ("Indiana Jones", Discipline::ARCHEOLOGY, "Dr."), make_shared<Student>("Thomas Cruise", Discipline::COMPUTER_SCIENCE, nullptr), make_shared<Faculty>("James Stock", Discipline::BIOLOGY),make_shared<TFaculty>("Sharon Rock", Discipline::BIOLOGY, "Professor"), make_shared<TFaculty>("Nicole Eweman", Discipline::ARCHEOLOGY, "Dr.")
    };

    // Print the names of the Person objects
    for (int k = 0; k < people.size (); k++)
    {
        cout << people[k]->getName() << endl;
    }
    return 0;
}
程式輸出結果:

Indiana Jones
Thomas Cruise
James Stock
Sharon Rock
Nicole Eweman

該程式建立了一個向量,這個向量中的指標型別為 Student、Faculty 和 TFaculty,但實際上卻指向了 Person 物件。然後它使用了相同的程式碼列印所有物件中名稱。因為一個向量只能存放一種型別的元素,所以必須使用一個指向基礎類別的指標向量。

請注意,即使 TFaculty 物件具有自己的更特殊化的版本,程式也會為陣列中的所有物件呼叫 Person 版本的 getName 函數。這段程式碼顯然不是多型的,因為它為每個物件執行相同的成員函數,而不管它的型別如何。換句話說,對於不同型別的物件,其表現卻並無不同。

為了更好地理解所發生的事情,現在來仔細看一看這 5 次呼叫中每一次所發生的情況。

people[k]->getName();

該函數用於檢索要列印的名稱。在每次呼叫中,指向基礎類別 Person 的指標 people[k] 用於呼叫不同派生類的物件中的 getName 函數。其中的一些類,如 TFaculty,提供了更特殊化的版本來覆蓋 getName 函數。當 people[k] 指向 TFaculty 物件時,編譯器必須在 Person 中定義的 getName 與 TFaculty 中定義的 getName 之間進行選擇。Person 是指標所屬的類,TFaculty 是物件實際所屬的類。 

前面已經介紹過,當一個指向基礎類別的指標被用於存取被派生類覆蓋的成員函數時,預設的 C++ 行為是使用指標所在類中定義的函數版本,而不是物件所屬類中的版本。所以,這裡編譯器選擇的是在 Person 中定義的 getName 函數,這也是函數的5次呼叫,雖然物件型別不同,但是表現卻相同的原因。

在物件導向程式設計中,通過基礎類別指標呼叫派生類物件的成員函數的情況是很常見的。假設有一個基礎類別 B,它有一個成員函數為基礎類別指標 ptr 指向派生類 D 的一個物件。該範例語句如下:
class B {
    public:
        void mfun()
        {
            cout << "Base class version";
        }
};

class D : public B
{
    public:
        void mfun()
        {
            cout << "Derived class version";
        }
};

shared_ptr<Base> ptr = make_shared<D>();
根據前面的介紹可知,此時如果寫入以下語句,那麼將被呼叫的應該是基礎類別 B 的成員函數而不是派生類 D 的成員函數:

ptr->mfun()

如果想讓編譯器選擇使用派生類 D 中更特殊化的 mfun() 版本,那該怎麼辦呢?在 C++ 中,可以通過將 mfun() 宣告為基礎類別中的虛擬函式來執行此操作。C++ 中使用虛擬函式來支援多型行為。因此,為了實現基礎類別 B 及其所有派生類中 mfun() 函數的多型行為,必須修改基礎類別 B 中的定義如下:
class B
{
    public:
    virtual void mfun()
    {
        cout << "Base class version";
    }
};
虛擬特性是可繼承的。即如果派生類的成員函數覆蓋了基礎類別中的虛擬函式,那麼該成員函數也會自動虛擬它本身。因此,在基礎類別 B 中將 mfun 宣告為虛擬函式,也將使 D 以及所有從 D 中派生的類中的 mfun 函數變成虛擬函式。

雖然沒有必要,但是很多程式設計師都會使用 virtual 關鍵字來標記所有的虛擬函式,以便更容易地識別它們。這是很好的做法,因此,D 的定義應該如下:
class D : public B
{
    public: virtual void mfun()
    {
        cout << "Derived class version";
    }
};
在本範例中,虛擬函式己經定義在類宣告中。如果虛擬函式定義在類宣告之外,則 virtual 關鍵字將繼續對其類中宣告有效,而對定義無效。C++ 不允許在類外面定義虛擬函式時加 virtual 關鍵字。

以下程式是文章開頭程式的修改版。其中,Person 類的 getName 函數己經宣告為虛擬函式。它還包含了 inheritance5.h 檔案,這是 inheritance4.h 檔案的修改版,並且已經將 Person 類的 getName 函數變成了虛擬函式。
//Inheritance5.h 的內容
#include <string>
#include <memory>
using namespace std;

enum class Discipline { ARCHEOLOGY, BIOLOGY, COMPUTER_SCIENCE };
enum class Classification { FRESHMAN, SOPHOMORE, JUNIOR, SENIOR };
class Person
{
    protected:
        string name;
    public:
        Person() { setName(""); }
        Person(const string& pName) { setName(pName); }
        void setName(const string& pName) { name = pName; }
        //virtual function
        virtual string getName() const { return name; }
};

class Student:public Person
{
    private:
        Discipline major;
        shared_ptr<Person> advisor;
    public:
        Student(const string& sname, Discipline d, const shared_ptr<Person>& adv) : Person(sname)
        {
            major = d;
            advisor = adv;
        }
        void setMajor(Discipline d) { major = d; }
        Discipline getMajor() const { return major; }
        void setAdvisor(shared_ptr<Person>& p) { advisor = p; }
        shared_ptr<Person> getAdvisor() const { return advisor; }
};

class Faculty :public Person
{
    private:
        Discipline department;
    public:
        Faculty(const string& fname, Discipline d) : Person(fname)
        {
            department = d;
        }
        void setDepartment(Discipline d) { department = d; }
        Discipline getDepartment() const { return department; }
};

class TFaculty : public Faculty
{
    private:
        string title;
    public:
        TFaculty(const string& fname, Discipline d,const string& title):Faculty(fname, d)
        {
            setTitle(title);
        }
        void setTitle (const string& title) { this->title = title; }
        // Override getName()
        string getName() const
        {
            return title + " " + Person::getName();
        }

};
//main主程式
// This exhibits the default non-polymorphic behavior of C++.
#include "inheritance5.h"
#include <vector>
#include <iostream>
using namespace std;
int main()
{
    // Create a vector of pointers to Person objects
    vector<shared_ptr<Person>> people
    {
        make_shared<TFaculty>
        ("Indiana Jones", Discipline::ARCHEOLOGY, "Dr."), make_shared<Student>("Thomas Cruise", Discipline::COMPUTER_SCIENCE, nullptr), make_shared<Faculty>("James Stock", Discipline::BIOLOGY),make_shared<TFaculty>("Sharon Rock", Discipline::BIOLOGY, "Professor"), make_shared<TFaculty>("Nicole Eweman", Discipline::ARCHEOLOGY, "Dr.")
    };

    // Print the names of the Person objects
    for (int k = 0; k < people.size (); k++)
    {
        cout << people[k]->getName() << endl;
    }
    return 0;
}
程式輸出結果:

Dr. Indiana Jones
Thomas Cruise
James Stock
Professor Sharon Rock
Dr. Nicole Eweman