C++ 为什么多态在没有指针/引用的情况下不起作用?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15188894/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 19:09:00  来源:igfitidea点击:

Why doesn't polymorphism work without pointers/references?

c++pointerspolymorphismvirtual

提问by user997112

I did find some questions already on SO with similar title- but when I read the answers they were focussing on different parts of the question which were really specific (e.g. STL/containers)..

我确实在 SO 上发现了一些具有类似标题的问题 - 但是当我阅读答案时,他们专注于问题的不同部分,这些部分非常具体(例如 STL/容器)。

Could someone please show me why you must use pointers/references for implementing polymorphism? I can understand pointers may help- but surely references only differentiate between pass-by-value and pass-by-reference??

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

Surely so long as you allocate memory on the heap- so that you can have dynamic binding then this would have been enough- obviously not.

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

采纳答案by Stephen Lin

In C++, an object always has a fixed type and size known at compile-time and (if it can and does have its address taken) always exists at a fixed address for the duration of its lifetime. These are features inherited from C which help make both languages suitable for low-level systems programming. (All of this is subject to the as-if, rule, though: a conforming compiler is free to do whatever it pleases with code as long as it can be proven to have no detectable effect on any behavior of a conforming program that is guaranteed by the standard.)

在 C++ 中,对象始终具有在编译时已知的固定类型和大小,并且(如果它可以并且确实获取了其地址)在其生命周期内始终存在于固定地址。这些是从 C 继承的特性,有助于使这两种语言都适用于低级系统编程。(不过,所有这一切都受制于 as-if 规则:一个符合标准的编译器可以自由地对代码做任何它喜欢的事情,只要它可以被证明对一个符合标准的程序的任何行为都没有可检测的影响。按照标准。)

A virtualfunction in C++ is defined (more or less, no need for extreme language lawyering) as executing based on the run-time type of an object; when called directly on an object this will always be the compile-time type of the object, so there is no polymorphism when a virtualfunction is called this way.

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

Note that this didn't necessarily have to be the case: object types with virtualfunctions are usually implemented in C++ with a per-object pointer to a table of virtualfunctions which is unique to each type. If so inclined, a compiler for some hypothetical variant of C++ could implement assignment on objects (such as Base b; b = Derived()) as copying both the contents of the object and the virtualtable pointer along with it, which would easily work if both Baseand Derivedwere the same size. In the case that the two were not the same size, the compiler could even insert code that pauses the program for an arbitrary amount of time in order to rearrange memory in the program and update all possible references to that memory in a way that could be proven to have no detectable effect on the semantics of the program, terminating the program if no such rearrangement could be found: this would be very inefficient, though, and could not be guaranteed to ever halt, obviously not desirable features for an assignment operator to have.

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

So in lieu of the above, polymorphism in C++ is accomplished by allowing references and pointers to objects to reference and point to objects of their declared compile-time types and any subtypes thereof. When a virtualfunction is called through a reference or pointer, and the compiler cannot prove that the object referenced or pointed to is of a run-time type with a specific known implementation of that virtualfunction, the compiler inserts code which looks up the correct virtualfunction to call a run-time. It did not have to be this way, either: references and pointers could have been defined as being non-polymorphic (disallowing them to reference or point to subtypes of their declared types) and forcing the programmer to come up with alternative ways of implementing polymorphism. The latter is clearly possible since it's done all the time in C, but at that point there's not much reason to have a new language at all.

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

In sum, the semantics of C++ are designed in such a way to allow the high-level abstraction and encapsulation of object-oriented polymorphism while still retaining features (like low-level access and explicit management of memory) which allow it to be suitable for low-level development. You could easily design a language that had some other semantics, but it would not be C++ and would have different benefits and drawbacks.

总之,C++ 的语义被设计成允许面向对象的多态性的高级抽象和封装,同时仍然保留允许它适合于低水平发展。您可以轻松设计一种具有其他语义的语言,但它不会是 C++,并且会有不同的优点和缺点。

回答by Luchian Grigore

"Surely so long as you allocate memory on the heap" - where the memory is allocated has nothing to do with it. It's all about the semantics. Take, for instance:

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

Derived d;
Base* b = &d;

dis on the stack (automatic memory), but polymorphism will still work on b.

d位于堆栈上(自动内存),但多态性仍将在b.

If you don't have a base class pointer or reference to a derived class, polymorphism doesn't work because you no longer have a derived class. Take

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

Base c = Derived();

The cobject isn't a Derived, but a Base, because of slicing. So, technically, polymorphism still works, it's just that you no longer have a Derivedobject to talk about.

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

Now take

现在拿

Base* c = new Derived();

cjust points to some place in memory, and you don't really care whether that's actually a Baseor a Derived, but the call to a virtualmethod will be resolved dynamically.

c只是指向内存中的某个位置,您并不真正关心它实际上是 aBase还是 a Derived,但是对virtual方法的调用将被动态解析。

回答by Elliot

I found it really helpful to understand that a copy constructor is invoked when assigning like this:

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

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. */ 

Since y is an actual object of class Base, rather than the original one, functions called on this are Base's functions.

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

回答by kfsone

Consider little endian architectures: values are stored low-order-bytes first. So, for any given unsigned integer, the values 0-255 are stored in the first byte of the value. Accessing the low 8-bits of any value simply requires a pointer to it's address.

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

So we could implement uint8as a class. We know that an instance of uint8is ... one byte. If we derive from it and produce uint16, uint32, etc, the interfaceremains the same for purposes of abstraction, but the one most important change is size of the concrete instances of the object.

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

Of course, if we implemented uint8and char, the sizes may be the same, likewise sint8.

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

However, operator=of uint8and uint16are going to move different quantities of data.

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

In order to create a Polymorphic function we must either be able to:

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

a/ receive the argument by value by copying the data into a new location of the correct size and layout, b/ take a pointer to the object's location, c/ take a reference to the object instance,

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

We can use templates to achieve a, so polymorphism canwork without pointers and references, but if we are not counting templates, then lets consider what happens if we implement uint128and pass it to a function expecting uint8? Answer: 8 bits get copied instead of 128.

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

So what if we made our polymorphic function accept uint128and we passed it a uint8. If our uint8we were copying was unfortunately located, our function would attempt to copy 128 bytes of which 127 were outside of our accessible memory -> crash.

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

Consider the following:

考虑以下:

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?

At the time fnwas compiled, there was no knowledge of B. However, Bis derived from Aso polymorphism should allow that we can call fnwith a B. However, the objectit returns should be an Acomprising a single int.

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

If we pass an instance of Bto this function, what we get back should be just a { int x; }with no a, b, c.

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

This is "slicing".

这就是“切片”。

Even with pointers and references we don't avoid this for free. Consider:

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

std::vector<A*> vec;

Elements of this vector could be pointers to Aor something derived from A. The language generally solves this through the use of the "vtable", a small addition to the object's instance which identifies the type and provides function pointers for virtual functions. You can think of it as something like:

此向量的元素可以是指向 的指针A或派生自 的东西A。该语言通常通过使用“vtable”来解决这个问题,这是对对象实例的一个小补充,用于标识类型并为虚函数提供函数指针。你可以把它想象成这样:

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

Rather than every object having its own distinct vtable, classes have them, and object instances merely point to the relevant vtable.

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

The problem now is not slicing but type correctness:

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

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

http://ideone.com/G62Cn0

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

What we should have done is use a->operator=(b)

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

http://ideone.com/Vym3Lp

http://ideone.com/Vym3Lp

but again, this is copying an A to an A and so slicing would occur:

但同样,这是将 A 复制到 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

http://ideone.com/DHGwun

(iis copied, but B's jis lost)

i被复制,但 Bj丢失)

The conclusion here is that pointers/references are required because the original instance carries membershipinformation with it that copying may interact with.

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

But also, that polymorphism is not perfectly solved within C++ and one must be cognizant of their obligation to provide/block actions which could produce slicing.

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

回答by AProgrammer

You need pointers or reference because for the kind of polymorphism you are interested in (*), you need that the dynamic type could be different from the static type, in other words that the true type of the object is different than the declared type. In C++ that happens only with pointers or references.

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



(*) Genericity, the type of polymorphism provided by templates, doesn't need pointers nor references.

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

回答by ssube

When an object is passed by value, it's typically put on the stack. Putting something on the stack requires knowledge of just how big it is. When using polymorphism, you know that the incoming object implements a particular set of features, but you usually have no idea the size of the object (nor should you, necessarily, that's part of the benefit). Thus, you can't put it on the stack. You do, however, always know the size of a pointer.

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

Now, not everything goes on the stack, and there are other extenuating circumstances. In the case of virtual methods, the pointer to the object is also a pointer to the object's vtable(s), which indicate where the methods are. This allows the compiler to find and call the functions, regardless of what object it's working with.

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

Another cause is that very often the object is implemented outside of the calling library, and allocated with a completely different (and possibly incompatible) memory manager. It could also have members that can't be copied, or would cause problems if they were copied with a different manager. There could be side-effects to copying and all sorts of other complications.

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

The result is that the pointer is the only bit of information on the object that you really properly understand, and provides enough information to figure out where the other bits you need are.

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