为什么 C++ 支持带有实现的纯虚函数?

Why does C++ support for a pure virtual function with an implementation?

今天做了一个简单的测试:

struct C{virtual void f()=0;};
void C::f(){printf("weird\n");}

程序没问题,但我觉得很奇怪,当我们使用=0时,意味着函数体应该定义在继承的类中,但似乎我仍然可以给它实现功能。

I tried both GCC and VC, both OK. So it seems to me this should be part of C++ standard.

But why this is not a syntax error?

我能想到的一个原因是像 C# 同时具有 'interface' 和 'abstract' 关键字,接口不能有实现,而抽象可以有一些实现。

我的困惑是这样吗,C++应该支持这种奇怪的语法?

必须定义析构函数,即使它是纯虚拟的。如果您不定义析构函数,编译器将生成一个。

编辑:你不能在没有定义的情况下声明析构函数,这将导致 link 错误。

您无论如何都可以从派生的 classes 调用函数体。 您可以实现纯虚函数的主体以提供默认行为,同时您希望派生 class 的设计者明确使用该函数。

基本上,两全其美(或最差...)。

派生的 class 需要实现纯虚方法,基 class 的设计者出于某种原因需要这样做。并且基础 class 还提供了此方法的默认实现,如果派生 class 需要或需要它,则可以使用它。

所以一些示例代码可能看起来像;

class Base {
public:
  virtual int f() = 0;
};
int Base::f() {
  return 42;
}

class Derived : public Base {
public:
  int f() override {
    return Base::f() * 2;
  }
};

那么常见的用例是什么...

此技术的一个常见用例与析构函数有关 - 基本上,基 class 的设计者希望它是一个抽象 class,但是方法 none作为纯虚函数很有意义。析构函数是一个可行的候选者。

class Base {
public:
  ~Base() = 0;
};
Base::~Base() { /* destruction... */ }

其他人提到了与析构函数的语言一致性,所以我会从软件工程的角度出发:

这是因为您定义的 class 可能有一个有效的默认实现,但调用它是 risky/expansive/whatever。如果您不将其定义为纯虚拟,派生的 classes 将隐式继承该实现。并且可能永远不会知道直到 运行 时间。

如果将其定义为纯虚拟,则派生的 class 必须实现该功能。如果 risk/cost/whatever 没问题,它可以静态调用默认实现 Base::f();
重要的是,这是一个有意识的决定,而且是明确的。

必须在子classes 中覆盖纯虚函数。但是,您可以提供一个默认实现,它适用于子 classes,但可能不是最佳的。

构造的用例用于抽象形状,例如

class Shape {
public:
    virtual Shape() {}

    virtual bool contains(int x, int y) const = 0;
    virtual int width() const = 0;
    virtual int height() const = 0;
    virtual int area() const = 0;
}

int Shape::area() const {
    int a = 0;
    for (int x = 0; x < width(); ++x) {
        for (int y = 0; y < height(); ++y) {
            if (contains(x,y)) a++;
        }
    }
    return a;
}

面积法适用于任何形状,但效率极低。鼓励 Subclass 提供合适的实现,但如果有 none 可用,他们仍然可以显式调用基础 class 的方法

C++ 支持带有实现的纯虚函数,因此 class 设计人员可以强制派生 classes 覆盖函数以添加特定细节,但仍提供有用的默认实现,他们可以将其用作一个共同的基础。

经典示例:

class PersonBase
{
private:
    string name;
public:
    PersonBase(string nameIn) : name(nameIn) {} 

    virtual void printDetails() = 0
    {
        std::cout << "Person name " << name << endl;
    }
};

class Student : public PersonBase
{
private:
    int studentId;
public: 
    Student(string nameIn, int idIn) : PersonBase(nameIn), studentId(idIn) {  }
    virtual void printDetails()
    {
        PersonBase::printDetails(); // call base class function to prevent duplication
        std::cout << "StudentID " << studentId << endl;
    }
};

请注意,您不能实例化具有纯虚方法的对象。

尝试实例化:

C c;

用VC2015,如期出现错误:

1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): error C2259: 'C': cannot instantiate abstract class 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: due to following members: 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: 'void C::f(void)': is abstract 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(6): note: see declaration of 'C::f'

回答你的问题: 该机制仅将函数声明为纯虚函数,但仍然存在虚函数 table 和基类。它会避免你实例化基类(C),但不会避免使用它:

struct D : public C { virtual void f(); }; 
void D::f() { printf("Baseclass C::f(): "); C::f(); }
...
D d; 
d.f();

纯虚的意思"child must override"。

所以:

struct A{ virtual void foo(){}; };
struct B:A{ virtual void foo()=0; };
struct C:B{ virtual void foo(){}; };
struct D:C{ virtual void foo()=0; };
void D::foo(){};
struct E:D{ virtual void foo(){D::foo();}; };

A 有一个虚拟 foo。

B使它抽象。在创建实例之前,派生类型现在必须实现它。

C 实现它。

D 将其抽象化,并添加了一个实现。

E通过调用D的实现来实现。

A、C 和 E 可以创建实例。 B和D不能。

抽象与实现技术可用于提供部分或低效的实现,派生类型可以在想要使用它时显式调用它,但不要获得 "by default",因为这样做是不明智的。

另一个有趣的用例是 parent 接口在不断变化,而且代码库很大。它有一个功能齐全的实现。 Children 使用默认值的人必须重复签名并明确转发给它。想要覆盖的直接覆盖即可。

当基本 class 签名发生变化时,代码将无法编译,除非每个 child 显式调用默认值或正确覆盖。在 override 关键字之前,这是确保您不会意外创建新虚函数而不是覆盖 parent 的唯一方法,并且它仍然是在 [=] 中执行策略的唯一方法39=]类型。