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

kmbjn2e3  于 2023-08-09  发布在  其他
关注(0)|答案(8)|浏览(91)

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

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

字符串
程序还可以,但对我来说很奇怪,当我们使用=0时,这意味着函数体应该在继承的类中定义,但似乎我仍然可以给予它实现函数。
我尝试了GCC和VC,都很好。所以在我看来,这应该是C标准的一部分。
但为什么这不是语法错误呢?
我能想到的一个原因是,就像C#同时有'interface'和'abstract'关键字一样,interface不能有实现,而abstract可以有一些实现。
这是我的困惑,C
应该支持这样一种奇怪的语法?

1l5u6lss

1l5u6lss1#

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上编译。

C03的第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;
}

krugob8w

krugob8w2#

其他人提到了与析构函数的语言一致性,所以我将从软件工程的Angular 出发:
这是因为你定义的类可能有一个有效的默认实现,但是调用它是有风险的/扩展的/不管怎样。如果你不把它定义为纯虚的,派生类将隐式地继承这个实现。并且可能直到运行时才知道。
如果将其定义为纯虚函数,则派生类必须实现该函数。如果风险/成本/其他都没问题,它可以静态调用默认实现Base::f();
重要的是,这是一个有意识的决定,而且这个决定是明确的。

wwodge7n

wwodge7n3#

基本上,两个世界中最好的(或最坏的...)。
派生类是实现纯虚方法所必需的,而基类的设计器出于某种原因需要这样做。基类还提供了此方法的默认实现,如果派生类希望或需要它,则可以使用该实现。
所以一些示例代码看起来像这样;

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... */ }

uxhixvfz

uxhixvfz4#

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

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

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

pkbketx9

pkbketx95#

纯虚拟意味着“子必须覆盖”。
于是:

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不能。
抽象与实现的技术可用于提供部分或低效的实现,当派生类型想要使用它时,它们可以显式地调用,但不要“默认”,因为这是不明智的。
另一个有趣的用例是父接口处于变化中,并且代码库很大。它有一个完整的功能实现。使用默认值的子级必须重复签名并显式转发到它。那些想要覆盖的只是覆盖。
当基类sigrnature更改时,除非每个子类显式调用默认值或正确重写,否则代码将无法编译。在override关键字之前,这是确保您不会意外创建新虚函数而不是重写父类的唯一方法,并且它仍然是在父类中强制执行策略的唯一方法。

ujv3wf0j

ujv3wf0j6#

请注意,你不能用纯虚方法示例化一个对象。
尝试示例化:

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'


回答您的问题:这些机制只声明函数是纯虚的,但仍然有虚函数表和基类。它将避免示例化Baseclass(C),但不会避免使用它:

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

ldxq2e6h

ldxq2e6h7#

必须定义析构函数,即使它是纯虚的。如果你没有定义析构函数,编译器将生成一个析构函数。
编辑:你不能在没有define的情况下声明析构函数,这会导致链接错误。

of1yzvn4

of1yzvn48#

你可以从派生类调用函数体。您可以实作纯虚函式的函式体以提供预设行为,同时希望衍生类别的设计工具明确使用该函式。

相关问题