基类指针vs继承类指针?

时间:2010-04-23 17:51:12

标签: c++ inheritance polymorphism

假设我有一个继承自类Dog的类Animal。这两行代码之间有什么区别?

    Animal *a = new Dog();
    Dog *d = new Dog();

在一个中,指针用于基类,而在另一个中,指针用于派生类。但这种区别何时变得重要?对于多态性,任何一个都可以完全相同,对吗?

8 个答案:

答案 0 :(得分:11)

对于类型检查的所有目的,编译器将a视为可以指向任何Animal,即使您知道它指向Dog:

  • 您无法将a传递给期待Dog*
  • 的函数
  • 您不能a->fetchStick(),其中fetchStickDog的成员函数,而不是Animal
  • Dog *d2 = dynamic_cast<Dog*>(d)可能只是编译器上的指针副本。 Dog *d3 = dynamic_cast<Dog*>(a)可能不是(我在这里猜测,我不打算检查任何编译器。重点是:编译器可能对ad做出不同的假设转换代码时。)

您可以通过其中任何一个调用Animal的虚函数(即定义的多态接口),效果相同。假设Dog没有隐藏它们(好的一点,JaredPar)。

对于在Animal中定义并在Dog中定义(重载)的非虚函数,通过a调用该函数与通过d调用该函数不同。

答案 1 :(得分:4)

这个问题的答案是巨人:这取决于

指针的类型可以通过多种方式变得重要。 C ++是一种非常复杂的语言,它显示的方式之一就是继承。

让我们举一个简短的例子,说明这可能有多重要的方法之一。

class Animal {
public:
  virtual void MakeSound(const char* pNoise) { ... }
  virtual void MakeSound() { ... }
};

class Dog : public Animal {
public:
  virtual void MakeSound() {... }
};

int main() {
  Animal* a = new Dog();
  Dog* d = new Dog();
  a->MakeSound("bark");
  d->MakeSound("bark"); // Does not compile
  return 0;
}

原因是C ++命名查找方式的怪癖。简而言之:当寻找一种调用C ++的方法时,将遍历类型层次结构,寻找具有匹配名称方法的第一种类型。然后,它将从具有在该类型上声明的名称的方法中查找正确的重载。由于Dog仅声明没有参数的MakeSound方法,因此没有重载匹配且无法编译。

答案 2 :(得分:2)

第一行允许您只在以下位置调用Animal类的成员:

Animal *a = new Dog();
a->eat(); // assuming all Animal can eat(), here we will call Dog::eat() implementation.
a->bark(); // COMPILATION ERROR : bark() is not a member of Animal! Even if it's available in Dog, here we manipulate an Animal.

虽然(正如其他人所指出的),在这个cas中,a仍然是一个Animal,你不能提供a作为函数的参数,要求更具体的子类是狗:

void toy( Dog* dog );

toy( a ); // COMPILATION ERROR : we want a Dog!

第二行允许您使用子类的特定功能:

Dog *a = new Dog();
a->bark(); // works, but only because we're manipulating a Dog

因此,使用基类作为类层次结构的“通用”接口(允许您使所有动物吃掉()而不用担心如何)。

答案 3 :(得分:2)

使用指针调用虚函数时,区别很重要。假设Animal和Dog都有do_stuff()函数。

  1. 如果将Animal :: do_stuff()声明为virtual,则在Animal指针上调用do_stuff()将调用Dog :: do_stuff()。

  2. 如果未将Animal :: do_stuff()声明为虚拟,则在Animal指针上调用do_stuff()将调用Animal :: do_stuff()。

  3. 这是一个完整的工作程序来演示:

    #include <iostream>
    
    class Animal {
    public:
            void do_stuff() { std::cout << "Animal::do_stuff\n"; }
            virtual void virt_stuff() { std::cout << "Animal::virt_stuff\n"; }
    };
    
    class Dog : public Animal {
    public:
            void do_stuff() { std::cout << "Dog::do_stuff\n"; }
            void virt_stuff() { std::cout << "Dog::virt_stuff\n"; }
    };
    
    int main(int argc, char *argv[])
    {
            Animal *a = new Dog();
            Dog *b = new Dog();
    
            a->do_stuff();
            b->do_stuff();
            a->virt_stuff();
            b->virt_stuff();
    }
    

    输出:

    Animal::do_stuff
    Dog::do_stuff
    Dog::virt_stuff
    Dog::virt_stuff
    

    这只是一个例子。其他答案列出了其他重要的差异。

答案 4 :(得分:1)

不,他们不一样。

Dog指针不像Animal那样具有多态性。所有它可以在运行时指向Dog或Dog的子类。如果没有Dog的子类,那么Dog运行时类型和编译时类型是相同的。

Animal指针可以引用Animal的任何子类:Dog,Cat,Wildebeast等。

答案 5 :(得分:0)

当您尝试调用不是Animal方法的Dog方法时,差异很重要。在第一种情况下(指向Animal的指针),您必须首先将指针强制转换为Dog。另一个区别是,如果您碰巧超载非虚方法。然后将调用Animal :: non_virtual_method()(指向Animal的指针)或Dog :: non_virtual_method(指向Dog的指针)。

答案 6 :(得分:0)

您必须始终记住,每个类,数据和界面都有两个部分。

您的代码在堆上真正创建了2个Dog对象。这意味着数据是Dog。 此对象的大小是所有数据成员Dog + Animal + vtable指针的总和。

ponters a和d(lvalues)从界面的角度来看是不同的。这决定了如何以代码方式对待它们。因此即使Animal * a真的是一只狗,即使Dog :: Bark()存在,你也无法访问a-&gt; Bark()。 d-&gt; Bark()本来可以正常工作。

将vtable添加回图片中,假设Animal的界面有动物::移动一个通用的Move(),而Dog真的用Dog :: Move(){像狗}覆盖。

即使你有动物a *并执行a-&gt; Move(),感谢vtable,你实际上会移动(){像狗一样}。发生这种情况是因为Animal :: Move()是一个(虚拟)函数指针,在构造Dog()时重新指向Dog's :: Move()。

答案 7 :(得分:-1)

它在运行时没有真正的区别,因为这两个实例是相同的。唯一的区别是在编译时,你可以调用例如d-&gt; bark()而不是a-&gt; bark(),即使实际上包含一只狗。编译器将变量视为动物,而且只考虑该变量。