[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)。

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