假设我有一个继承自类Dog
的类Animal
。这两行代码之间有什么区别?
Animal *a = new Dog();
Dog *d = new Dog();
在一个中,指针用于基类,而在另一个中,指针用于派生类。但这种区别何时变得重要?对于多态性,任何一个都可以完全相同,对吗?
答案 0 :(得分:11)
对于类型检查的所有目的,编译器将a
视为可以指向任何Animal,即使您知道它指向Dog:
a
传递给期待Dog*
。a->fetchStick()
,其中fetchStick
是Dog
的成员函数,而不是Animal
。Dog *d2 = dynamic_cast<Dog*>(d)
可能只是编译器上的指针副本。 Dog *d3 = dynamic_cast<Dog*>(a)
可能不是(我在这里猜测,我不打算检查任何编译器。重点是:编译器可能对a
和d
做出不同的假设转换代码时。)您可以通过其中任何一个调用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()函数。
如果将Animal :: do_stuff()声明为virtual,则在Animal指针上调用do_stuff()将调用Dog :: do_stuff()。
如果未将Animal :: do_stuff()声明为虚拟,则在Animal指针上调用do_stuff()将调用Animal :: do_stuff()。
这是一个完整的工作程序来演示:
#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(),即使实际上包含一只狗。编译器将变量视为动物,而且只考虑该变量。