为什么派生类指针不能在没有强制转换的情况下指向基类对象?

时间:2012-03-17 17:42:49

标签: c++ inheritance pointers

对于这类基本问题herehere,我看到了很少的宠物和狗类型示例,但它们对我没有意义,这就是原因。

假设我们有以下类结构

class Pet {};
class Dog : public Pet {};

然后是以下陈述

  

a (Dog) is a (Pet)

在我看来,

在现实生活中可能是真的,但在C ++中不是真的。只需看看Dog对象的逻辑表示,它看起来像这样:

enter image description here

更合适的说法

  

a (Dog) has a (Pet)

  

a (Pet) is a subset of (Dog)

如果你注意到与“狗是宠物”的逻辑相反


现在的问题是,#2不允许下面的#1:

Pet* p = new Dog;  // [1] - allowed!
Dog* d = new Pet;  // [2] - not allowed without explicit casting!

我的理解是,[1]不应该在没有警告的情况下被允许,因为指针不能指向其超集类型的对象(Dog对象是Pet的超集),因为Pet没有不知道Dog可能宣称的新成员(上图中的狗 - 宠物子集)。

[1]相当于试图指向int*对象的double

很明显,我错过了一个关键点,这会让我的整个推理颠倒过来。你能告诉我它是什么吗?

我认为与现实世界的例子相似只会使事情复杂化。我希望从技术细节方面理解这一点。谢谢!

11 个答案:

答案 0 :(得分:29)

编辑:重新阅读您的问题,我的回答让我在顶部说出这一点:

您对C ++中的 is a 的理解(一般来说是多态)是错误的。

A is B 表示A has at least the properties of B, possibly more按照定义

这与Dog具有Pet且[宠物的[属性]是[{1}}的[属性]的子集的语句兼容。< / p>


这是多态性和继承的定义问题。您绘制的图表与DogPet实例的内存中表示形式对齐,但在您解释它们的方式上会产生误导。

Dog

指针Pet* p = new Dog; 被定义为指向任何Pet兼容对象,在C ++中,是p的任何子类型(注意:Pet是根据定义,它本身的子类型)。运行时可以确保,当访问Pet后面的对象时,它将包含预期包含p的任何内容,以及可能更多。 “可能更多”部分是图表中的Pet。绘制图表的方式会产生误导性的解释。

考虑内存中特定于类的成员的布局:

Dog

现在,只要Pet: [pet data] Dog: [pet data][dog data] Cat: [pet data][cat data] 指向,就需要Pet *p部分,以及其他任何内容。从上面的列表中,[pet data]可能指向三者中的任何一个。 只要您使用Pet *p来访问对象,您就只能访问Pet *p,因为您不知道之后是什么,。这是一份合同,上面写着这至少是一个Pet,也许更多

无论[pet data]指向什么,都必须包含Dog *d[pet data]。所以记忆中唯一可能指向的对象就是狗。相反,通过[dog data],您可以同时访问Dog *d[pet data]。类似于[dog data]


让我们解释你感到困惑的声明:

Cat
     

我的理解是,在没有警告的情况下不应该允许1   因为指针无法指向对象   其超集的类型(Dog对象是Pet的超集)简单   因为Pet对Dog的新成员一无所知   可能已经声明了(上图中的狗 - 宠物子集)。

指针Pet* p = new Dog; // [1] - allowed! Dog* d = new Pet; // [2] - not allowed without explicit casting! 希望在其指向的位置找到p。由于右侧是[pet data],并且每个Dog对象在其Dog前面都有[pet data],因此指向[dog data]类型的对象完全没问题。

编译器不知道指针后面的 else 是什么,这就是您无法通过Dog访问[dog data]的原因。

声明是允许的,因为编译器在编译时可以保证p的存在。 (这个陈述显然是从现实中简化出来的,以适合你的问题描述)

  

1相当于试图指向双重对象的int *!

int和double之间没有这种子类型关系,C ++中的[pet data]Dog 之间没有这种关系。尽量不要将这些混合到讨论中,因为它们是不同的:你在int的之间强制转换而double(Pet是显式的,(int) double是隐式的),你不能强制转换在指向它们的指针之间。忘了这个比较。

关于[2]:声明声明“(double) int指向具有d [pet data] 的对象,可能更多。”但是你只分配[dog data],所以编译器会告诉你不能这样做。

实际上,编译器无法保证这是否正常并且拒绝编译。有合理的情况,编译器拒绝编译,但程序员,你知道更好。这就是[pet data]static_cast的用途。我们上下文中最简单的例子是:

dynamic_cast
如果d = p; // won't compile d = static_cast<Dog *>(p); // [3] d = dynamic_cast<Dog *>(p); // [4] 不是p,那么

[3]会一直成功并导致可能难以追踪的错误。
如果Dog实际上不是NULL,[4]将会返回p

我热烈建议尝试这些演员,看看你得到了什么。假设RTTI已启用,您应该从Dog获取[dog data]的垃圾和static_cast的{​​{1}}指针。

答案 1 :(得分:5)

在技术细节方面:

Dog对象的附加信息附加到Pet对象的末尾,因此Dog的前缀[以位为单位]实际上是Pet,所以将Dog*对象分配给Pet*变量没有问题。访问Pet对象的Dog个字段/方法非常安全。

然而 - 对立不是真的。如果您将Pet*的地址分配给Dog*变量,然后访问Dog [不在Pet]中的某个字段,您将退出分配空间。

打字推理:
另请注意,只有在没有强制转换[c ++ is static typing langauge]的情况下,才能将值赋给变量。由于DogPetDog*Pet* - 所以这不是冲突的,但反过来却不是真的。

答案 2 :(得分:4)

是宠物,因为它来自Pet类。在C ++中,它几乎满足了OOP的要求。 What is the Liskov substitution principle

Dog* d = new Pet;  // [2] - not allowed without explicit casting!

当然不允许,宠物也可以是猫或鹦鹉。

答案 3 :(得分:4)

这是分层分类的问题。如果我告诉我的孩子他们可以养宠物,那么肯定会允许一只狗。但如果我告诉他们他们只能养猫,那么他们就不会要求养鱼了。

答案 4 :(得分:2)

我认为你很困惑 is-a 在OO上下文中意味着什么。您可以说Dog有一个Pet子对象,如果您查看对象的位表示,则为真。但重要的是,编程是关于将现实建模为计算机可以处理的程序。根据您的示例,继承是您建立关系 is-a 的方式:

  

A Dog是一个Pet

一般说来,它表示宠物的所有行为,可能有一些不同的特征行为(吠声),但它是动物,它提供公司,你可以喂它......所有这些行为都将由({1}}类中的(虚拟)成员函数,可能会在Pet类型中被覆盖,因为其他操作已定义。但重要的是,通过使用继承,Dog的所有实例都可以在需要强烈Dog的情况下使用。

答案 5 :(得分:1)

您在基类和父类之间感到困惑。

Pet是基类。只要Pet*继承自PetPet* pet = new Dog就可以指出任意数量的不同类型。因此Dog被允许也就不足为奇了。 PetPet。我有一个指向Dog的指针,恰好是Pet*

另一方面,如果我有Dog,我不知道它实际指向的是什么。它可以指向Cat,但它也可以指向FishPet->bark()或完全不同的其他内容。因此,该语言不会让我拨打Pet,因为并非所有bark()都可以Cat(例如meow() s Pet*

但是,如果我有Dog知道,实际上是Dog,则转换为bark()是完全安全的,然后拨打Pet* p = new Dog; // sure this is allowed, we know that all Dogs are Pets Dog* d = new Pet; // this is not allowed, because Pet doesn't support everything Dog does

所以:

{{1}}

答案 6 :(得分:0)

在英语中,你试图解决的陈述可能是“导致它成为宠物的狗的方面是狗的所有方面的一部分”和“所有实体的集合”狗是宠物实体集的一部分。

D,P使得D(x)=&gt; x∈Dog,P(x)=&gt; x∈Pet

D(x)=&gt; P(x)

(如果x具有狗的所有方面,则D(x)为真,所以这就是说狗的东西的各个方面是宠物的事物方面的超级集合 - P(x)如果D(x)为真,则为真,但不一定相反)

狗⊇宠物=&gt;

∀xx∈狗=&gt; x∈宠物(每只狗都是宠物)

但如果D(x)≡x∈Dog,则这些是相同的陈述。

所以说'让它成为宠物的狗的各个方面是整个狗的一部分'相当于说'狗的一组是宠物的一部分'

答案 7 :(得分:0)

考虑这种情况(抱歉这样一个俗气的例子):

车辆可以是任何车辆

class Vehicle
{
   int numberOfWheels;
   void printWheels();
};

汽车是一种载体

class Car: public Vehicle
{
    // the wheels are inherited
    int numberOfDoors;
    bool doorsOpen;
    bool isHatchBack;
};

自行车是一种车辆,但这辆车也不是一辆车也是一辆车

class Bike: public Vehicle
{
    int numberOfWings; // sorry, don't know exact name
    // the wheels are inherited
};

所以我希望你不仅可以看到真实的生活差异,而且还注意到BikeCar对象的程序内存布局会有所不同,即使它们是< / strong> Vehicles。这就是为什么儿童对象不能成为任何类型的孩子;它可能只是被定义为。

答案 8 :(得分:0)

以上所有答案都很好。我只想补充一点。我认为你的狗/宠物图有误导性。

我理解为什么你把DOG图描绘成PET的超集:你可能认为,因为一只狗比一只宠物有更多的属性,它需要用一个更大的集合来表示。

但是,每当你绘制一个图集,其中集合B是集合A的一个子集时,你就是说B类型的对象数量肯定比A类型的对象多。 另一方面,由于类型B的对象具有更多属性,因此您可以对它们执行更多操作,因为您可以执行A类型对象上允许的所有操作。

如果你碰巧知道一些关于功能分析的东西(这是一个很长的镜头,但也许你看过它),它与banach空间和它们的对偶之间存在着相同的关系:空间越小,你的操作集就越大可以对他们做。

答案 9 :(得分:0)

实际原因是 - 派生类包含有关基类的所有信息以及一些额外的信息。现在,指向派生类的指针将需要更多空间,这在基类中是不够的。所以指向派生类的指针不能指向它。另一方面,情况恰恰相反。

答案 10 :(得分:0)

基于之前的一些答案,我得到了一些理解,我将它们放在我的话语中。 Based on some of previous answers I developed some understanding and I am putting them in my words