[C++] - 純虛函數 & 抽象基類 & 接口類

翻譯自:https://www.learncpp.com/cpp-tutorial/126-pure-virtual-functions-abstract-base-classes-and-interface-classes/

 

1.純虛函數和抽象基類

C++允許我們創建一種特別的虛函數:純虛函數(pure virtual function)(或者叫抽象函數abstract function),這種特別的虛函數可以沒有實現。純虛函數僅僅扮演者placeholder的角色,等待着子類去重新定義。

我們通過給函數賦值0來創建純虛函數:

class Base
{
public:
    const char* sayHi() { return "Hi"; } // a normal non-virtual function    
 
    virtual const char* getName() { return "Base"; } // a normal virtual function
 
    virtual int getValue() = 0; // a pure virtual function
 
    int doSomething() = 0; // Compile error: can not set non-virtual functions to 0
};

使用純虛函數有兩個結果:

1>只有擁有一個以上的純虛函數的類就變爲抽象基類,抽象基類不能被實例化。


int main()
{
    Base base; // We can't instantiate an abstract base class, but for the sake of example, pretend this was allowed
    base.getValue(); // what would this do?
 
    return 0;
}

2>任何繼承抽象基類的類必須定義純虛函數,否則繼承抽象基類的子類也變成抽象基類。

2.純虛函數例子

一個簡單地Animal基類和連個子類Cat和Dog:

#include <string>
class Animal
{
protected:
    std::string m_name;
 
    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(std::string name)
        : m_name(name)
    {
    }
 
public:
    std::string getName() { return m_name; }
    virtual const char* speak() { return "???"; }
};
 
class Cat: public Animal
{
public:
    Cat(std::string name)
        : Animal(name)
    {
    }
 
    virtual const char* speak() { return "Meow"; }
};
 
class Dog: public Animal
{
public:
    Dog(std::string name)
        : Animal(name)
    {
    }
 
    virtual const char* speak() { return "Woof"; }
};

通過定義Animal的構造函數爲protected,我們已經禁止直接創建Animal實例。但是有兩個問題:

1>子類仍然可以訪問Animal的構造函數來實例化Animal類;

2>仍然可以創建一個沒有重定義speak()方法的子類對象。

例如:

#include <iostream>
class Cow: public Animal
{
public:
    Cow(std::string name)
        : Animal(name)
    {
    }
 
    // We forgot to redefine speak
};
 
int main()
{
    Cow cow("Betsy");
    std::cout << cow.getName() << " says " << cow.speak() << '\n';
 
    return 0;
}

輸出:

Betsy says ???

我們忘了重定義speak()方法,所以cow.speak()調用的是Animal.speak(),然而這不是我們想要的。

解決這個問題更好的方法是使用純虛函數:

#include <string>
class Animal // This Animal is an abstract base class
{
protected:
    std::string m_name;
 
public:
    Animal(std::string name)
        : m_name(name)
    {
    }
 
    std::string getName() { return m_name; }
    virtual const char* speak() = 0; // note that speak is now a pure virtual function
};

這裏要注意幾點,

1>speak()現在是純虛函數,這意味着Animal現在是抽象基類,因此不能被實例化。所以我們不必講Animal的構造函數設爲protected;

2>因爲Cow類繼承自Animal,但我們沒有定義Cow::speak(),因此Cow也是一個抽象基類。

現在我們編譯這段代碼:

#include <iostream>
class Cow: public Animal
{
public:
    Cow(std::string name)
        : Animal(name)
    {
    }
 
    // We forgot to redefine speak
};
 
int main()
{
    Cow cow("Betsy");
    std::cout << cow.getName() << " says " << cow.speak() << '\n';
 
    return 0;
}

編譯器會報錯,因爲Cow是一個抽象基類,我們無法實例化抽象基類:

C:\Test.cpp(141) : error C2259: 'Cow' : cannot instantiate abstract class due to following members:
        C:Test.cpp(128) : see declaration of 'Cow'
C:\Test.cpp(141) : warning C4259: 'const char *__thiscall Animal::speak(void)' : pure virtual function was not defined

因此,只有Cow類實現了speak()方法才能實例化Cow類。

#include <iostream>
class Cow: public Animal
{
public:
    Cow(std::string name)
        : Animal(name)
    {
    }
 
    virtual const char* speak() { return "Moo"; }
};
 
int main()
{
    Cow cow("Betsy");
    std::cout << cow.getName() << " says " << cow.speak() << '\n';
 
    return 0;
}

輸出:

Betsy says Moo

當我們想要在基類中放一個方法,但只有子類知道這個方法該怎麼實現時,純虛函數是非常有用的。純虛函數使得基類不能被實例化,並強制子類去定義這些純虛函數,如果子類想要能夠實例化的話。這有助於確保子類不會忘記定義這些基類期望它們實現純虛函數。

3.帶實現的純虛函數

我們可以定義帶有實現的純虛函數:

#include <string>
class Animal // This Animal is an abstract base class
{
protected:
    std::string m_name;
 
public:
    Animal(std::string name)
        : m_name(name)
    {
    }
 
    std::string getName() { return m_name; }
    virtual const char* speak() = 0; // The = 0 means this function is pure virtual
};
 
const char* Animal::speak()  // even though it has a body
{
    return "buzz";
}

在這種情況下,speak()仍然被視爲一個純虛函數(因爲"=0",及時它有實現),並且Animal仍然被視爲一個抽象基類(因此無法實例化)。任何繼承Animal的類需要去提供自己定義的speak()方法,否則它也會被視爲抽象基類。

當爲純虛函數提供實現時,必須分開獨立去提供實現(不是在基類聲明的.h中,inline提供)。

當你想要你的基類提供一個純虛函數的默認實現,但仍然想強制子類去提供它們自己定義的實現時,這種方法是有用的。但是,如果子類就想調用基類的默認實現,它可以簡單地直接調用基類實現:

#include <string>
#include <iostream>
 
class Animal // This Animal is an abstract base class
{
protected:
    std::string m_name;
 
public:
    Animal(std::string name)
        : m_name(name)
    {
    }
 
    std::string getName() { return m_name; }
    virtual const char* speak() = 0; // note that speak is a pure virtual function
};
 
const char* Animal::speak()
{
    return "buzz"; // some default implementation
}
 
class Dragonfly: public Animal
{
 
public:
    Dragonfly(std::string name)
        : Animal(name)
    {
    }
 
    virtual const char* speak() // this class is no longer abstract because we defined this function
    {
        return Animal::speak(); // use Animal's default implementation
    }
};
 
int main()
{
    Dragonfly dfly("Sally");
    std::cout << dfly.getName() << " says " << dfly.speak() << '\n';
 
    return 0;
}

輸出:

Sally says buzz

這種方法使用較少,不常見。

4.接口類(Interface classes)

接口類就是沒有成員變量,所有的成員函數都是純虛函數的類。換句話說,接口類單純就是聲明,沒有實際的實現。當你想要定義一些子類必須要實現的功能而且完全由子類來提供功能的實現時,接口非常有用。

接口類通常以"I"字母開頭命名,例如:

class IErrorLog
{
public:
    virtual bool openLog(const char *filename) = 0;
    virtual bool closeLog() = 0;
 
    virtual bool writeError(const char *errorMessage) = 0;
 
    virtual ~IErrorLog() {} // make a virtual destructor in case we delete an IErrorLog pointer, so the proper derived destructor is called
};

任何繼承自IErrorLog並想要能夠實例化的子類必須提供三個純虛函數的實現。你可以繼承一個類,叫FileErrorLog,實現openLog()方法在磁盤上打開文件,closeLog()方法關閉文件,writeError()方法向文件中寫內容。你可以繼承一個類,叫ScreenErrorLog,實現openLog()方法和closeLog()方法不做事,writeError()方法在屏幕上彈出對話框打印內容。

現在假如你想要使用一種error log,如果你直接使用FileErrorLog或ScreenErrorLog類,那你就限制於只能使用這種類型的error log。例如,下面的方法強制調用方法mySqrt使用FileErrorLog,這或許不是用戶所想要的:

#include <cmath> // for sqrt()
double mySqrt(double value, FileErrorLog &log)
{
    if (value < 0.0)
    {
        log.writeError("Tried to take square root of value less than 0");
        return 0.0;
    }
    else
        return sqrt(value);
}

一個更好的方法是使用IErrorLog來代替:

#include <cmath> // for sqrt()
double mySqrt(double value, IErrorLog &log)
{
    if (value < 0.0)
    {
        log.writeError("Tried to take square root of value less than 0");
        return 0.0;
    }
    else
        return sqrt(value);
}

現在調用方法可以傳入任何實現了IErrorLog接口的類。通過使用IErrorLog,你的方法變得更獨立和靈活。

不要忘了爲你的接口類提供一個虛析構函數,這樣如果指向接口類的指針被delete時,子類的析構函數也會被調用。

5.純虛函數和虛表

抽象基類仍然有虛表,因爲如果你有一個指向抽象基類的指針或引用,這些虛表仍然可以被使用。純虛函數的虛表入口通常是包含一個null指針,或指向一個普通函數(有時這個函數叫__purecall,如果沒有override純虛函數,那麼這個函數會打印error)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章