对于这类基本问题here和here,我看到了很少的宠物和狗类型示例,但它们对我没有意义,这就是原因。
假设我们有以下类结构
class Pet {};
class Dog : public Pet {};
然后是以下陈述
在我看来,
a (Dog) is a (Pet)
在现实生活中可能是真的,但在C ++中不是真的。只需看看Dog对象的逻辑表示,它看起来像这样:
更合适的说法
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
!
很明显,我错过了一个关键点,这会让我的整个推理颠倒过来。你能告诉我它是什么吗?
我认为与现实世界的例子相似只会使事情复杂化。我希望从技术细节方面理解这一点。谢谢!
答案 0 :(得分:29)
编辑:重新阅读您的问题,我的回答让我在顶部说出这一点:
您对C ++中的 is a
的理解(一般来说是多态)是错误的。
A is B
表示A has at least the properties of B, possibly more
,按照定义。
这与Dog
具有Pet
且[宠物的[属性]是[{1}}的[属性]的子集的语句兼容。< / p>
这是多态性和继承的定义问题。您绘制的图表与Dog
和Pet
实例的内存中表示形式对齐,但在您解释它们的方式上会产生误导。
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]的情况下,才能将值赋给变量。由于Dog
是Pet
,Dog*
是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*
继承自Pet
,Pet* pet = new Dog
就可以指出任意数量的不同类型。因此Dog
被允许也就不足为奇了。 Pet
是Pet
。我有一个指向Dog
的指针,恰好是Pet*
。
另一方面,如果我有Dog
,我不知道它实际指向的是什么。它可以指向Cat
,但它也可以指向Fish
,Pet->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
};
所以我希望你不仅可以看到真实的生活差异,而且还注意到Bike
和Car
对象的程序内存布局会有所不同,即使它们是< / strong> Vehicles
。这就是为什么儿童对象不能成为任何类型的孩子;它可能只是被定义为。
答案 8 :(得分:0)
以上所有答案都很好。我只想补充一点。我认为你的狗/宠物图有误导性。
我理解为什么你把DOG图描绘成PET的超集:你可能认为,因为一只狗比一只宠物有更多的属性,它需要用一个更大的集合来表示。
但是,每当你绘制一个图集,其中集合B是集合A的一个子集时,你就是说B类型的对象数量肯定比A类型的对象多。 另一方面,由于类型B的对象具有更多属性,因此您可以对它们执行更多操作,因为您可以执行A类型对象上允许的所有操作。
如果你碰巧知道一些关于功能分析的东西(这是一个很长的镜头,但也许你看过它),它与banach空间和它们的对偶之间存在着相同的关系:空间越小,你的操作集就越大可以对他们做。
答案 9 :(得分:0)
实际原因是 - 派生类包含有关基类的所有信息以及一些额外的信息。现在,指向派生类的指针将需要更多空间,这在基类中是不够的。所以指向派生类的指针不能指向它。另一方面,情况恰恰相反。
答案 10 :(得分:0)