13

假设我们有一个具体的class Apple. (Apple 对象可以被实例化。)现在,有人来了,并class Peach从 Apple 派生了一个抽象。它是抽象的,因为它引入了一个新的纯虚函数。Peach 的用户现在被迫从中派生并定义这个新功能。这是一种常见的模式吗?这是正确的做法吗?

样本:


class Apple
{
public:
    virtual void MakePie();
    // more stuff here
};

class Peach : public Apple { public: virtual void MakeDeliciousDesserts() = 0; // more stuff here };

现在假设我们有一个具体的class Berry. class Tomato有人从贝瑞那里得到了一个摘要。它是抽象的,因为它覆盖了 Berry 的一个虚函数,并使其成为纯虚函数。Tomato 的用户必须重新实现之前在 Berry 中定义的功能。这是一种常见的模式吗?这是正确的做法吗?

样本:


class Berry
{
public:
    virtual void EatYummyPie();
    // more stuff here
};

class Tomato : public Berry { public: virtual void EatYummyPie() = 0; // more stuff here };

注意:名称是人为的,不反映任何实际代码(希望如此)。写这个问题没有损害任何成果。

4

7 回答 7

8

来自 Apple 的 Re Peach:

  • 如果 Apple 是一个值类(即具有复制 ctor、不相同的实例可以相等等),请不要这样做。请参阅 Meyers 更有效的 C++ 第 33 条了解原因。
  • 如果 Apple 有一个公共的非虚拟析构函数,请不要这样做,否则当您的用户通过指向 Peach 的指针删除 Apple 时,您会引发未定义的行为。
  • 否则,您可能是安全的,因为您没有违反Liskov substitutability。桃子就是苹果。
  • 如果您拥有 Apple 代码,则更愿意分解出一个通用的抽象基类(也许是 Fruit)并从中派生 Apple 和 Peach。

来自浆果的番茄:

  • 同上,加上:
  • 避免,因为它不寻常
  • 如果必须,请记录 Tomato 的派生类必须做什么,以免违反 Liskov 可替换性。您在 Berry 中覆盖的功能 - 我们称之为Juice()- 强加某些要求并做出某些承诺。派生类的实现Juice()必须不要求更多,承诺也不少。那么 DerivedTomato IS-A Berry 和只知道 Berry 的代码是安全的。

通过记录 DerivedTomatoes 必须调用 ,您可能会满足最后一个要求Berry::Juice()。如果是这样,请考虑改用模板方法:

class Tomato : public Berry
{
public:
    void Juice() 
    {
        PrepareJuice();
        Berry::Juice();
    }
    virtual void PrepareJuice() = 0;
};

现在有一个极好的机会是番茄是浆果,这与植物学的预期相反。(例外情况是,如果派生类的实现PrepareJuice强加了超出 所强加的额外先决条件Berry::Juice)。

于 2008-11-21T23:01:50.297 回答
5

在我看来,这似乎是一个糟糕设计的迹象。如果你想从一个封闭的库中获取一个具体的定义并扩展它并从中分支出一堆东西,可能会被迫,但那时我会认真考虑关于封装而不是继承的指导方针。如果你可以封装,你可能应该。

是的,我想得越多,这是一个非常糟糕的主意。

于 2008-11-21T22:23:54.180 回答
3

如果您使用具有继承模型“is-a”的推荐做法,那么这种模式几乎不会出现。

一旦你有了一个具体的类,你就是在说它是你可以实际创建一个实例的东西。如果你然后从它派生一个抽象类,那么作为基类属性的东西对于派生类来说是不正确的,它应该设置一些不正确的喇叭。

看你的例子,桃子不是苹果,所以不应该从中派生出来。源自浆果的番茄也是如此。

这是我通常建议收容的地方,但这似乎不是一个好的模型,因为苹果不包含桃子。

在这种情况下,我将排除通用接口—— PieFilling 或 DessertItem。

于 2008-11-21T22:53:38.240 回答
2

不一定是的,但肯定是臭的。特别是如果你把水果放在阳光下太久。(而且我认为我的牙医不希望我吃混凝土苹果。)

不过,我在这里看到的主要臭味并不是从具体类派生的抽象类,而是真正深的继承层次结构。

编辑:重新阅读我发现这是两个层次结构。所有水果的东西都让我搞混了。

于 2008-11-21T22:26:39.977 回答
1

有点不寻常,但是如果您有基类的其他子类,并且抽象类的子类有足够的共同点来证明抽象类的存在是合理的,例如:

class Concrete
{
public:
    virtual void eat() {}
};
class Sub::public Concrete { // some concrete subclass
    virtual void eat() {}
};
class Abstract:public Concrete // abstract subclass
{
public:
    virtual void eat()=0;
    // and some stuff common to Sub1 and Sub2
};
class Sub1:public Abstract {
    void eat() {}
};
class Sub2:public Abstract {
    void eat() {}
};
int main() {
    Concrete *sub1=new Sub1(),*sub2=new Sub2();
    sub1->eat();
    sub2->eat();
    return 0;
}
于 2008-11-21T23:18:54.803 回答
0

嗯……通过思考“什么……”几秒钟,我得出一个结论,这并不常见……而且,我不会从苹果中提取桃子,从浆果中提取番茄……做你有更好的例子吗?:)

你可以在 C++ 中做很多奇怪的事情......我什至想不出其中的 1%......

关于用纯虚拟覆盖虚拟,你可能只是隐藏它,它会很奇怪......

如果你能找到一个愚蠢的 C++ 编译器,将这个函数链接为一个虚函数,那么你将获得运行时纯虚函数调用......

我认为这只能用于黑客攻击,我不知道真正的黑客攻击是什么......

于 2008-11-21T22:31:30.653 回答
0

要回答您的第一个问题,您可以这样做,因为 Apple 的用户,如果给定一个从 Peach 派生的具体实例不会知道任何不同。并且该实例不会知道它不是 Apple(除非 Apple 提供了一些您没有告诉我们的被覆盖的虚拟功能)。

我还无法想象用纯虚函数覆盖虚函数会有多大用处——这甚至合法吗?

一般来说,您希望符合 Scott Meyers 书中的“使所有非叶类抽象”项。

无论如何,除此之外,您所描述的似乎是合法的-只是我看不到您经常需要它。

于 2008-11-21T22:33:26.237 回答