具有实现的纯虚函数

218

我的基本理解是,纯虚函数没有实现,但我被告知可能会有纯虚函数的实现。

class A {
public:
    virtual void f() = 0;
};

void A::f() {
    cout<<"Test"<<endl;
}

上面的代码是否正确?

将一个纯虚函数定义为带有实现的函数有什么目的?

10个回答

253

一个纯虚函数必须在将直接实例化的派生类型中实现,但是基类仍然可以定义实现。如果访问权限允许,派生类可以使用完全作用域名称(例如,在您的示例中调用 A::f() - 如果 A::f()publicprotected)来显式调用基类实现。如下所示:

class B : public A {

    virtual void f() {
        // class B doesn't have anything special to do for f()
        //  so we'll call A's

        // note that A's declaration of f() would have to be public 
        //  or protected to avoid a compile time problem

        A::f();
    }

};

我能想到的一个使用案例是,当存在一种更或多或少合理的默认行为时,但类设计者希望这种默认行为仅在显式调用时被调用。这也可能是这样一种情况:您希望派生类始终执行自己的工作,但也能够调用一组公共功能。
请注意,即使语言允许这样做,我也很少看到它被广泛使用(事实上,即使是经验丰富的C++程序员,他们似乎对它的可行性感到惊讶)。

1
你忘记了解释为什么这会让程序员惊讶:因为内联定义是被标准禁止的。纯虚方法的定义必须被“驱逐”(即放在 .inl 或 .cpp 文件中,以符合常见的文件命名规范)。 - v.oddou
这个调用方法与静态方法成员调用相同。在Java中属于某种类方法。 - Sany Liew
2
“不常用” == 不好的做法吗?我正在寻找完全相同的行为,尝试实现NVI。而且对我来说,NVI似乎是一个好的做法。 - Uylenburgh
7
值得指出的是,将A::f()声明为纯虚函数意味着B 必须 实现f()(否则B将是抽象的且无法实例化的)。正如@MichaelBurr所指出的那样,为A::f()提供实现意味着B 可以 使用它来定义f()。 - fearless_fool
2
如果我没记错的话,Scott Meyer在他的经典著作《Effective C++》中有一篇关于这个问题用例的优秀文章。 - irsis
显示剩余3条评论

96

明确一点,你误解了在虚函数后面使用 = 0 的含义。

= 0 表示派生类必须提供实现,而不是基类不能提供实现。

在实践中,当你将虚函数标记为纯虚函数(=0)时,提供定义很少有意义,因为除非有人通过 Base::Function(...) 明确调用它或者基类构造函数调用所涉及的虚函数,否则它永远不会被调用。


14
这是错误的。如果你在抽象类的构造函数中调用了该纯虚函数,将会发生纯虚函数调用。在这种情况下,你最好有一个实现。 - rmn
1
@rmn,是的,在构造函数中使用虚函数调用方面,你是正确的。我已经更新了答案。希望每个人都知道不要这样做。 :) - Terry Mahaffey
3
事实上,在构造函数中进行一次基础纯虚函数调用会导致实现定义的行为。在VC++中,这将导致“_purecall”崩溃。 - Ofek Shilon
@OfekShilon 这是正确的 - 我会倾向于将其称为未定义行为和不良实践/代码重构的候选项(即在构造函数中调用虚方法)。我想这与虚表的一致性有关,可能没有准备好路由到正确实现的主体。 - teodron
1
在构造函数和析构函数中,虚函数是虚函数。 - Jesper Juhl
显示剩余2条评论

21
如果你有一些应该由派生类执行的代码,但又不希望直接执行它——并且想强制进行重写。
你的代码是正确的,尽管总的来说这不是一个经常使用的功能,通常只在尝试定义纯虚析构函数时才会看到——在这种情况下,你必须提供一个实现。有趣的是,一旦你从那个类派生,你就不需要重写析构函数了。
因此,纯虚函数的一个明智用途是将纯虚析构函数指定为“非最终”关键字。
以下代码令人惊讶地正确:
class Base {
public:
  virtual ~Base() = 0;
};

Base::~Base() {}

class Derived : public Base {};

int main() { 
  // Base b; -- compile error
  Derived d; 
}

1
无论虚拟的还是非虚拟的、纯的还是不纯的,基类析构函数总是被调用的;而对于其他函数,你不能保证覆盖虚拟函数是否会调用基类实现,无论基类版本是否是纯的。 - CB Bailey
1
那段代码是错误的。由于语言的语法怪异性,你必须在类定义之外定义dtor。 - Roger Pate
@Roger:谢谢,实际上帮了我很多——这是我一直在使用的代码,在MSVC下编译良好,但我想它可能不可移植。 - Kornel Kisielewicz

21

它的优势在于强制子类型仍需重写该方法,但也提供了默认或附加实现。


1
为什么要强制实现,如果已经有默认实现呢?这听起来像是普通的虚函数。如果只是普通的虚函数,我可以选择覆盖它,如果我没有覆盖,那么就会提供一个默认实现(基类的实现)。 - StackExchange123

8

5

带或不带函数体的纯虚函数意味着派生类型必须提供自己的实现。

在基类中使用带有函数体的纯虚函数是有用的,如果您的派生类想要调用基类的实现。


4

是的,这是正确的。在你的例子中,从A派生的类继承了接口f()和默认实现。但你强制要求派生类实现方法f()(即使只是调用A提供的默认实现)。

斯科特·迈耶斯在他的书《Effective C++(第二版)》中讨论了这个问题,第36条是区分接口继承和实现继承。最新版可能会有所改变。


3
“virtual void foo() =0;”语法并不意味着您不能在当前类中实现foo(),您可以这样做。它也不意味着您必须在派生类中实现它。在我们讨论钻石问题之前,请注意这是隐式代码。
class A
{
public: 
    virtual void foo()=0;
    virtual void bar();
}

class B : public virtual A
{
public:
    void foo() { bar(); }
}

class C : public virtual A
{
public:
    void bar();
}

class D : public B, public C
{}

int main(int argc, const char* argv[])
{
    A* obj = new D();
    **obj->foo();**
    return 0;
}

现在,obj->foo()的调用将导致B::foo()被执行,然后是C::bar()。
你看...纯虚方法不必在派生类中实现(class C中的foo()没有实现——编译器会编译) 在C++中有很多漏洞。
希望我能帮到您 :-)

8
不需要在所有的派生类中都实现,但是你必须在所有你打算实例化的派生类中实现。在你的例子中,你不能实例化一个类型为 C 的对象。你可以实例化一个类型为 D 的对象,因为它从 B 中获得了 foo 的实现。 - YoungJohn

1
如果我问你一个动物的声音是什么,正确的回答是询问哪种动物,这正是纯虚函数(或抽象函数)的目的。在基类(动物)中无法为函数提供实现,但每个动物都有自己的声音。
class Animal
{
   public:
       virtual void sound() = 0;
}

class Dog : public Animal
{
   public:
       void sound()
       {
           std::cout << "Meo Meo";
       }
}

问题是在这里定义Animal::sound()的意义是什么。 - Burak

0

在编程中,一个重要的应用场景是拥有带有实现体的纯虚方法,当你想要创建一个抽象类,但是在类中没有适合做成纯虚方法的方法时,你可以将类的析构函数设置为纯虚方法,并为其提供所需的实现(即使是空实现)。例如:

class Foo
{
   virtual ~Foo() = 0;
   void bar1() {}
   void bar2(int x) {}
   // other methods
};

Foo::~Foo()
{
}

这种技术使得Foo类成为抽象类,因此无法直接实例化该类。同时,您没有添加额外的纯虚方法来使Foo类成为抽象类。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接