为什么多态不工作没有指针/引用?

我在SO上已经find了一些相似的题目,但是当我读到答案的时候,他们把注意力集中在了真正具体的问题的不同部分(如STL /容器)上。

有人能告诉我为什么你必须使用指针/引用来实现多态吗? 我可以理解的指针可能会有所帮助 – 但确实引用只区分传递值和传递引用?

当然,只要你在堆上分配内存 – 这样你就可以拥有dynamic绑定,那么这就足够了 – 显然不是。

在C ++中,一个对象在编译时总是有一个固定的types和大小,并且(如果它可以并且确实拥有它的地址)始终存在于一个固定的地址上。 这些是从Cinheritance的function,它们使这两种语言都适合于低级系统编程。 (所有这些都受制于规则,但是:符合的编译器可以自由地执行代码所要求的任何操作,只要它能够被certificate对被保证的一致性程序的任何行为没有可察觉的影响由标准。)

C ++中的virtual函数被定义(或多或less,不需要极端的语言律师),基于对象的运行时types执行; 当直接调用一个对象时,这将始终是该对象的编译时types,所以当以这种方式调用一个virtual函数时,没有多态性。

请注意,这不一定是这种情况:具有virtual函数的对象types通常是用C ++实现的,每个对象指针指向一个virtual函数表,每个types都是唯一的。 如果这样倾向,C ++的一些假设变体的编译器可以实现对对象(例如Base b; b = Derived() )的赋值,同时复制对象和virtual表指针的内容,如果BaseDerived都是相同的大小。 如果两者的大小不一样,编译器甚至可以插入代码来暂停程序一段时间,以便重新排列程序中的内存,并以可能的方式更新所有可能的内存引用被certificate对程序的语义没有任何可察觉的影响,如果不能find这样的重新排列,则终止程序:尽pipe这样做效率很低,但不能保证永久停止,对于赋值操作符来说显然是不希望的特征有。

因此,代替上述,C ++中的多态性是通过允许对象的引用和指针引用并指向其声明的编译时types及其任何子types的对象来实现的。 当通过引用或指针调用virtual函数,并且编译器无法certificate引用或指向的对象是具有该virtual函数的特定已知实现的运行时types时,编译器会插入查找正确的代码virtual函数来调用运行时。 它不一定是这样的:引用和指针可以被定义为非多态(不允许它们引用或指向它们声明的types的子types),并迫使程序员提出实现多态的替代方法。 后者显然是可能的,因为它始终在C中完成,但是在那个时候根本没有什么理由要有一种新的语言。

总而言之,C ++的语义被devise为允许高级抽象和封装面向对象的多态,同时仍然保留特性(如低级访问和显式的内存pipe理),使其适用于低层次的发展。 你可以很容易地devise一个有其他语义的语言,但它不是C ++,会有不同的优点和缺点。

“只要你在堆上分配内存” – 内存分配的地方与它无关。 这完全是关于语义的。 拿,例如:

 Derived d; Base* b = &d; 

d在堆栈上(自动存储器),但多态性仍然可以在b工作。

如果您没有基类指针或对派生类的引用,则多态性不起作用,因为您不再具有派生类。 采取

 Base c = Derived(); 

c对象不是一个Derived ,而是一个Base ,因为切片 。 所以,在技术上,多态仍然有效,只是你不再有一个Derived对象来谈论。

现在拿走

 Base* c = new Derived(); 

c只是指向内存中的某个地方,而不关心这个实际上是一个Base还是一个Derived ,但是对一个virtual方法的调用将被dynamicparsing。

我发现真正有帮助的理解,复制构造函数调用时分配像这样:

 class Base { }; class Derived : public Base { }; Derived x; /* Derived type object created */ Base y = x; /* Copy is made (using Base's copy constructor), so y really is of type Base. Copy can cause "slicing" btw. */ 

由于y是Base类的实际对象,而不是原来的对象,因此调用的函数是Base的函数。

考虑一些小端的体系结构:值先存储低位字节。 因此,对于任何给定的无符号整数,值0-255存储在值的第一个字节中。 访问任何值的低8位只需要一个指向它的地址的指针。

所以我们可以实现uint8作为一个类。 我们知道uint8一个实例是…一个字节。 如果我们从它中派生出uint16uint32等,那么为了抽象的目的, 接口保持不变,但是最重要的变化是对象具体实例的大小。

当然,如果我们实现了uint8char ,大小可能是相同的,同样sint8

但是, uint8uint16 operator=将要移动不同数量的数据。

为了创build一个Polymorphic函数,我们必须能够:

a /通过将数据拷贝到正确大小和布局的新位置来接收参数值,b /取一个指向对象位置的指针,c /引用对象实例,

我们可以使用模板来实现一个,所以多态性可以工作没有指针和引用,但是如果我们不计算模板,那么让我们考虑如果我们实现uint128并将其传递给期望uint8的函数会发生什么? 答:8位被复制而不是128位。

那么,如果我们使得我们的多态函数接受uint128 ,并且通过了uint8 。 如果我们uint8我们正在复制不幸的位置,我们的function将试图复制128个字节,其中127个是在我们的可访问的内存之外 – >崩溃。

考虑以下几点:

 class A { int x; }; A fn(A a) { return a; } class B : public A { uint64_t a, b, c; B(int x_, uint64_t a_, uint64_t b_, uint64_t c_) : A(x_), a(a_), b(b_), c(c_) {} }; B b1 { 10, 1, 2, 3 }; B b2 = fn(b1); // b2.x == 10, but a, b and c? 

fn编译的时候,没有B知识。 然而, B是从A派生的,所以多态性应该允许我们可以用B调用fn 。 但是,它返回的对象应该是一个包含单个int的A

如果我们将B一个实例传递给这个函数,我们得到的应该只是一个{ int x; } { int x; }没有a,b,c。

这是“切片”。

即使有指针和引用,我们也不会免费的避免这种情况。 考虑:

 std::vector<A*> vec; 

这个向量的元素可以是指向A或从A派生的东西的指针。 这种语言通常通过使用“vtable”来解决这个问题,vtable是对象实例的一小部分,用来标识types并为虚函数提供函数指针。 你可以把它看成是这样的:

 template<class T> struct PolymorphicObject { T::vtable* __vtptr; T __instance; }; 

而不是每个对象都有自己独特的vtable,类有它们,而对象实例只是指向相关的vtable。

现在的问题不是切分,而是types正确:

 struct A { virtual const char* fn() { return "A"; } }; struct B : public A { virtual const char* fn() { return "B"; } }; #include <iostream> #include <cstring> int main() { A* a = new A(); B* b = new B(); memcpy(a, b, sizeof(A)); std::cout << "sizeof A = " << sizeof(A) << " a->fn(): " << a->fn() << '\n'; } 

http://ideone.com/G62Cn0

 sizeof A = 4 a->fn(): B 

我们应该做的是使用a->operator=(b)

http://ideone.com/Vym3Lp

但是,再次,这是复制一个A到一个切片会发生:

 struct A { int i; A(int i_) : i(i_) {} virtual const char* fn() { return "A"; } }; struct B : public A { int j; B(int i_) : A(i_), j(i_ + 10) {} virtual const char* fn() { return "B"; } }; #include <iostream> #include <cstring> int main() { A* a = new A(1); B* b = new B(2); *a = *b; // aka a->operator=(static_cast<A*>(*b)); std::cout << "sizeof A = " << sizeof(A) << ", a->i = " << a->i << ", a->fn(): " << a->fn() << '\n'; } 

http://ideone.com/DHGwun

i被复制,但B的j丢失)

这里的结论是,指针/引用是必需的,因为原始实例携带会员信息与复制可能交互。

而且,这种多态性在C ++中还没有完全解决,人们必须认识到他们有义务提供/阻止可能产生切片的动作。

您需要指针或引用,因为对于您感兴趣的多态性types(*),您需要dynamictypes可能与静态types不同,换句话说,对象的真实types与声明的types不同。 在只发生指针或引用的C ++中。


(*)generics,由模板提供的多态的types,不需要指针或引用。

当一个对象被传值时,它通常被放在堆栈上。 把东西放在堆栈上需要知道它有多大。 当使用多态时,你知道传入的对象实现了一组特定的function,但是你通常不知道对象的大小(你也不一定是这个好处的一部分)。 因此,你不能把它放在堆栈上。 但是,您确实知道指针的大小。

现在,并不是所有东西都在堆叠中,还有其他一些可以减轻负担的情况。 在虚拟方法的情况下,指向对象的指针也是指向对象的vtable(s)的指针,指示方法在哪里。 这允许编译器查找和调用函数,而不pipe它正在处理什么对象。

另一个原因是对象常常在调用库之外实现,并且被分配了一个完全不同的(也可能不兼容的)内存pipe理器。 它也可以让成员不能被复制,或者如果被不同的经理复制,会造成问题。 可能会有副作用,复制和各种其他并发症。

结果是指针是你真正理解的对象上唯一的信息位,并且提供了足够的信息来确定你需要的其他位。