关于c ++:为什么没有指针/引用时多态性不起作用?

Why doesn't polymorphism work without pointers/references?

我确实找到了一些类似标题的问题——但当我读到答案时,他们把注意力集中在问题的不同部分,这些部分确实很具体(例如STL/容器)。

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

当然,只要您在堆上分配内存—这样您就可以进行动态绑定—那么这就足够了—显然不是。


"当然,只要您在堆上分配内存"——在那里分配内存与之无关。这都是关于语义的。例如:

1
2
Derived d;
Base* b = &d;

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

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

1
Base c = Derived();

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

现在采取

1
Base* c = new Derived();

EDOCX1·2=指向内存中的某个地方,并且您并不真正关心它实际上是EDCOX1 4还是EDCOX1×3,但是对EDCOX1×9方法的调用将被动态地解决。


在C++中,对象总是具有固定类型和大小,在编译时已知(如果它能够并且确实有地址)总是存在于固定地址中,在其生命周期的持续时间内。这些是从C继承来的特性,有助于使这两种语言适合于低级系统编程。(所有这一切都要遵守似乎的规则,尽管:只要能证明符合标准的编译器对标准保证的符合标准的程序的任何行为都没有可检测的影响,那么符合标准的编译器就可以自由地用代码做它想做的任何事情。)

C++中的EDOCX1 0函数定义了(或多或少,不需要极端的语言法律化),这是基于对象的运行时类型执行的;当直接调用对象时,它总是对象的编译时类型,因此当EDCOX1×0函数被这样调用时,没有多态性。

注意,这不一定是这样的:带有EDCOX1和0个函数的对象类型通常在C++中实现,每个对象指针指向EDCOX1×0个函数的表,这些函数对于每种类型都是唯一的。如果是这样的话,对于某些假设的C++变体,编译器可以实现对对象的分配(例如EDCOX1(4)),既复制对象的内容,又复制EDCOX1、0×表指针,如果EDCX1 6和EDCX1 7都是相同大小,则很容易工作。如果两个程序的大小不相同,编译器甚至可以插入代码,使程序暂停任意时间,以便重新排列程序中的内存,并更新对该内存的所有可能引用,以证明对程序的语义没有可检测的影响,从而终止程序。m如果找不到这样的重新排列:这将是非常低效的,而且不能保证永远停止,显然对于一个分配操作员来说是不可取的特性。

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

总而言之,C++的语义是这样设计的,以允许面向对象的多态性的高级抽象和封装,同时仍然保留特征(如低级别访问和明确的内存管理),这使得它适合于低级开发。您可以很容易地设计一种具有其他语义的语言,但它不是C++,将有不同的优点和缺点。


我发现在这样分配时调用一个复制构造函数是非常有帮助的:

1
2
3
4
5
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是类基的实际对象,而不是原始对象,因此在此基础上调用的函数是基的函数。


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

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

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

但是,uint8uint16operator=将移动不同数量的数据。

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

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

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

那么,如果我们让我们的多态函数接受uint128,并传递给它一个uint8,会怎么样呢?如果我们复制的uint8被不幸地定位,我们的函数将尝试复制128个字节,其中127个字节在我们可访问的内存->崩溃之外。

考虑以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

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

这是"切片"。

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

1
std::vector<A*> vec;

这个向量的元素可以是指向A的指针,也可以是源自A的东西。语言通常通过使用"vtable"来解决这一问题,vtable是对对象实例的一小部分添加,用于标识类型并为虚拟函数提供函数指针。你可以把它想象成:

1
2
3
4
5
template<class T>
struct PolymorphicObject {
    T::vtable* __vtptr;
    T __instance;
};

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

现在的问题不是切片而是类型正确性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() << '
'
;
}

网址:http://ideone.com/g62cn0

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

我们应该做的是使用EDCX1〔26〕。

HTTP://IDENE.COM/VYM3LP

但同样,这是复制A到A,所以切片将发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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() << '
'
;
}

http://ideone.com/dhgwun

(复制了i,但丢失了b的j)

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

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


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

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


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

现在,不是所有的事情都是一成不变的,还有其他可以减轻痛苦的情况。对于虚拟方法,指向对象的指针也是指向对象的vtable的指针,vtable指示方法的位置。这允许编译器查找和调用函数,而不管它使用的是什么对象。

另一个原因是,对象通常在调用库之外实现,并使用完全不同(可能不兼容)的内存管理器进行分配。它还可能包含无法复制的成员,或者如果使用其他管理器复制这些成员会导致问题。复制可能会有副作用和其他各种并发症。

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