您正在对这些答案进行假设,因此为了清楚起见,我将尝试做一个更简单、更实际的解释。
面向对象设计的基本关系有两个:
IS-A 和 HAS-A。这些不是我编的。他们就是这么称呼的。
IS-A 表示特定对象标识为在类层次结构中高于它的类。如果香蕉对象是水果类的子类,那么它就是水果对象。这意味着在任何可以使用水果类的地方,都可以使用香蕉。不过,它不是反身的。如果需要特定类,则不能用基类替换特定类。
Has-a 表示一个对象是复合类的一部分并且存在所有权关系。这意味着在 C++ 中它是一个成员对象,因此拥有类有责任在销毁它之前处理它或移交所有权。
这两个概念在单继承语言中比在像 c++ 这样的多继承模型中更容易实现,但规则本质上是相同的。当类标识不明确时会出现复杂情况,例如将 Banana 类指针传递给采用 Fruit 类指针的函数。
首先,虚拟函数是运行时的东西。它是多态性的一部分,因为它用于决定在运行程序中调用它时要运行哪个函数。
virtual 关键字是一个编译器指令,用于在类标识不明确时按特定顺序绑定函数。虚函数总是在父类中(据我所知),并向编译器指示成员函数与其名称的绑定应该首先使用子类函数,然后是父类函数。
Fruit 类可以有一个默认返回“NONE”的虚函数 color()。
Banana 类 color() 函数返回“YELLOW”或“BROWN”。
但是如果接收 Fruit 指针的函数在发送给它的 Banana 类上调用 color() —— 调用哪个 color() 函数?
该函数通常会为 Fruit 对象调用 Fruit::color()。
这在 99% 的情况下都不是预期的。
但是如果 Fruit::color() 被声明为虚拟,那么会为该对象调用 Banana:color(),因为正确的 color() 函数将在调用时绑定到 Fruit 指针。
运行时将检查指针指向的对象,因为它在 Fruit 类定义中被标记为虚拟。
这与覆盖子类中的函数不同。在这种情况下
Fruit 指针将调用 Fruit::color() 如果它只知道它是一个指向 Fruit 的指针。
所以现在出现了“纯虚函数”的想法。
这是一个相当不幸的短语,因为纯度与它无关。这意味着永远不会调用基类方法。
确实不能调用纯虚函数。然而,它仍然必须被定义。必须存在函数签名。为了完整性,许多编码人员制作了一个空实现 {},但如果没有,编译器将在内部生成一个。在这种情况下,即使指针指向 Fruit 也调用函数时,会调用 Banana::color(),因为它是 color() 的唯一实现。
现在是拼图的最后一块:构造函数和析构函数。
纯虚构造函数完全是非法的。刚刚出来。
但纯虚析构函数在您想要禁止创建基类实例的情况下确实有效。如果基类的析构函数是纯虚函数,则只能实例化子类。
惯例是将其分配给 0。
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
在这种情况下,您必须创建一个实现。编译器知道这是你在做什么,并确保你做对了,或者它强烈抱怨它不能链接到它需要编译的所有函数。如果您在如何建模类层次结构方面没有走在正确的轨道上,这些错误可能会令人困惑。
因此,在这种情况下,您被禁止创建 Fruit 实例,但允许创建 Banana 实例。
删除指向 Banana 实例的 Fruit 指针的调用
总是会先调用 Banana::~Banana() 然后再调用 Fuit::~Fruit()。
因为不管怎样,调用子类析构函数时,基类析构函数必须跟在后面。
这是一个坏模型吗?是的,它在设计阶段更复杂,但它可以确保在运行时执行正确的链接,并且在确切访问哪个子类存在歧义的情况下执行子类函数。
如果您编写 C++ 时只传递准确的类指针,而没有泛型或模糊指针,则实际上不需要虚函数。
但是,如果您需要类型的运行时灵活性(如在 Apple Banana Orange ==> Fruit 中),则函数会变得更容易、更通用,并且冗余代码更少。
您不再需要为每种水果编写一个函数,而且您知道每种水果都会以自己正确的函数响应 color()。
我希望这种冗长的解释能够巩固概念,而不是混淆事物。有很多很好的例子可以看看,
看够了,实际运行它们并弄乱它们,你就会得到它。