为什么没有指针/引用时多态性不起作用?

时间:2013-03-03 18:06:19

标签: c++ pointers polymorphism virtual

我确实在SO上发现了一些类似标题的问题 - 但是当我读到答案时,他们只关注问题的不同部分(例如STL /容器)。

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

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

6 个答案:

答案 0 :(得分:47)

“肯定只要你在堆上分配内存” - 分配内存与它无关。这都是关于语义的。举个例子:

Derived d;
Base* b = &d;

d在堆栈上(自动内存),但多态仍然可以在b上运行。

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

Base c = Derived();

由于切片c对象不是Derived,而是Base。因此,从技术上讲,多态性仍然有效,只是你不再需要Derived个对象来讨论。

现在拿

Base* c = new Derived();

c只是指向内存中的某个位置,而您实际上并不关心这是Base还是Derived,而是对virtual的调用方法将动态解决。

答案 1 :(得分:44)

在C ++中,对象始终具有在编译时已知的固定类型和大小(如果它可以且确实具有其地址)在其生存期内始终存在于固定地址。这些是从C继承的功能,有助于使这两种语言适合低级系统编程。 (所有这些都受到as-if规则的约束:符合标准的编译器可以随意使用代码做任何事情,只要它可以被证明对符合程序的任何行为都没有可检测的影响。按标准。)

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

请注意,这不一定是这种情况:具有virtual函数的对象类型通常在C ++中实现,每个对象指向一个virtual函数表,这对于每种类型。如果这么倾向,C ++的一些假设变体的编译器可以在对象(例如Base b; b = Derived())上实现赋值,同时复制对象的内容和virtual表指针,这很容易如果BaseDerived的大小相同,则可以正常工作。在两者大小不同的情况下,编译器甚至可以插入暂停程序任意时间的代码,以便重新安排程序中的内存并以可能的方式更新对该内存的所有可能引用。被证明对程序的语义没有可检测的影响,如果没有找到这样的重新排列就终止程序:但这样效率非常低,并且不能保证永远停止,显然不是指定操作符的理想特征。有

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

总而言之,C ++的语义设计允许对面向对象的多态性进行高级抽象和封装,同时仍保留允许它的特性(如低级访问和显式内存管理)。适合低水平的发展。您可以轻松地设计一种具有其他语义的语言,但它不会是C ++,并且会有不同的优点和缺点。

答案 2 :(得分:12)

我发现在分配这样的代码时调用复制构造函数非常有用:

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函数。

答案 3 :(得分:4)

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

因此我们可以将uint8作为一个类来实现。我们知道uint8的实例是......一个字节。如果我们从它派生并生成uint16uint32等,接口对于抽象的目的保持不变,但最重要的一个变化是具体实例的大小对象。

当然,如果我们实施uint8char,则尺寸可能相同,同样sint8

但是,operator=uint8的{​​{1}}将移动不同数量的数据。

为了创建一个多态函数,我们必须能够:

a /通过将数据复制到正确大小和布局的新位置来按值接收参数, b /指向对象的位置, c /参考对象实例,

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

那么,如果我们使多态函数接受uint8并且我们传递了uint128,那该怎么办呢?如果遗憾地找到了我们复制的uint8,我们的函数会尝试复制128个字节,其中127个字节位于我们的可访问内存之外 - >崩溃。

请考虑以下事项:

uint8

在编译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调用fn。但是,它返回的对象应该是一个包含单个int的B

如果我们将A的实例传递给此函数,我们得到的内容应该只是B,而不是a,b,c。

这是“切片”。

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

{ int x; }

此向量的元素可以指向std::vector<A*> vec; 或从A派生的内容。该语言通常通过使用“vtable”来解决这个问题,“vtable”是对象实例的一个小补充,它标识了类型并为虚函数提供了函数指针。您可以将其视为:

A

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

问题现在不是切片而是输入正确性:

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

http://ideone.com/G62Cn0

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';
}          

我们应该做的是使用sizeof A = 4 a->fn(): B

http://ideone.com/Vym3Lp

但同样,这是将A复制到A,因此会发生切片:

a->operator=(b)

http://ideone.com/DHGwun

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'; } 已复制,但B i已丢失)

这里的结论是指针/引用是必需的,因为原始实例带有成员资格信息,复制可以与之交互。

但是,这种多态性在C ++中并没有完全解决,必须认识到他们有义务提供/阻止可能产生切片的行为。

答案 4 :(得分:1)

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


(*)Genericity,模板提供的多态性类型,不需要指针也不需要引用。

答案 5 :(得分:0)

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

现在,并非一切都在堆叠上,还有其他情有可原的情况。在虚方法的情况下,指向对象的指针也是指向对象的vtable的指针,指示方法的位置。这允许编译器查找和调用函数,无论它使用什么对象。

另一个原因是对象通常在调用库之外实现,并且分配了完全不同(可能不兼容)的内存管理器。它也可能包含无法复制的成员,或者如果使用其他管理器复制它们会导致问题。复制可能会产生副作用以及各种其他复杂情况。

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