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

Why does C++ support pure virtual functions with an implementation?

本文关键字:函数 实现 c++ 支持 为什么      更新时间:2023-10-16

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

struct C{virtual void f()=0;};
void C::f(){printf("weirdn");}

程序是好的,但对我来说很奇怪,当我们使用=0时,它意味着函数体应该在继承的类中定义,但似乎我仍然可以给它实现函数。

我尝试了GCC和VC,都可以。所以在我看来,这应该是c++标准的一部分。

但是为什么这不是语法错误呢?

我能想到的一个原因是c#同时有"接口"answers"抽象"关键字,接口不能有实现,而抽象可以有一些实现。

这是我困惑的情况,c++应该支持这种奇怪的语法吗?

c++支持带实现的纯虚函数,因此类设计人员可以强制派生类重写函数以添加特定的细节,但仍然提供有用的默认实现,可以用作公共基础。

经典的例子:

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++标准要求在类定义体之外定义纯虚函数体。换句话说,类成员函数不能既是纯虚函数又是内联函数。前面的代码示例可以在MSVC (Visual Studio)上编译,但不能在GCC或CLANG上编译。

c++ 03第10.4条第2段告诉我们什么是抽象类,并附带说明如下:[注:函数声明不能同时提供纯说明符和定义]

一个带有c++标准体的纯虚函数看起来是这样的:

#include <iostream>
using namespace std;
class PersonBase
{
private:
    string name;
public:
    PersonBase(string nameIn) : name(nameIn) {} 
    virtual void printDetails()=0;
};
void PersonBase::printDetails()
{
    std::cout << "Person name " << name << endl;
}

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

这是因为你定义的类可能有一个有效的默认实现,但是调用它是有风险的/扩展的/诸如此类的。如果不将其定义为纯虚,则派生类将隐式继承该实现。直到运行时才知道。

如果将其定义为纯虚函数,则派生类必须实现该函数。如果不介意风险/成本等问题,它可以静态地调用默认实现Base::f();
重要的是,这是一个有意识的决定,而且这个决定是明确的。

基本上是两个世界最好的(或最坏的…)

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

一些示例代码看起来像;

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 Base {
public:
  ~Base() = 0;
};
Base::~Base() { /* destruction... */ }

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

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

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;
}

面积法适用于任何形状,但效率非常低。鼓励子类提供合适的实现,但如果没有可用的实现,它们仍然可以显式调用基类的方法

纯虚拟意味着"子类必须重写"。

:

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不能。

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

另一个有趣的用例是父接口不断变化,而真正的代码库很大。它具有完整的功能实现。使用默认值的子节点必须重复签名并显式转发给它。那些想要重写的只是重写。

当基类签名改变时,除非每个子类都显式调用默认值或正确重写,否则代码将无法编译。在override关键字之前,这是确保您不会意外创建新的虚函数而不是重写父函数的唯一方法,并且它仍然是在父类型中强制执行策略的唯一方法。

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

尝试实例化:

C c;

与VC2015,有一个错误的预期:

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

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

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

必须定义析构函数,即使它是纯虚函数。如果你没有定义析构函数,编译器会生成一个。

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

无论如何都可以从派生类调用函数体。您可以实现纯虚函数体来提供默认行为,同时希望派生类的设计器显式地使用该函数。