Why doesn't polymorphism work without pointers/references?
我确实找到了一些类似标题的问题——但当我读到答案时,他们把注意力集中在问题的不同部分,这些部分确实很具体(例如STL/容器)。
有人能告诉我为什么必须使用指针/引用来实现多态性吗?我可以理解指针可能有帮助——但引用肯定只区分传递值和传递引用??
当然,只要您在堆上分配内存—这样您就可以进行动态绑定—那么这就足够了—显然不是。
"当然,只要您在堆上分配内存"——在那里分配内存与之无关。这都是关于语义的。例如:
1 2 | Derived d; Base* b = &d; |
如果没有基类指针或对派生类的引用,则多态性将不起作用,因为不再有派生类。采取
1 | Base c = 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++中的多态性是通过允许引用和指针指向对象来引用和指向它们声明的编译时类型的对象和它们的任何子类型来实现的。当通过引用或指针调用
总而言之,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位只需要一个指向其地址的指针。
所以我们可以将
当然,如果我们实现了
但是,
为了创建多态函数,我们必须能够:
a/通过将数据复制到正确大小和布局的新位置,按值接收参数,b/用一个指针指向物体的位置,c/引用对象实例,
我们可以使用模板来实现,因此多态性可以在没有指针和引用的情况下工作,但是如果我们不计算模板,那么让我们考虑一下,如果我们实现
那么,如果我们让我们的多态函数接受
考虑以下内容:
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? |
在编辑
如果我们将EDOCX1的一个实例(16)传递给这个函数,我们得到的应该只是一个没有a、b、c的
这是"切片"。
即使有指针和引用,我们也不能免费避免这种情况。考虑:
1 | std::vector<A*> vec; |
这个向量的元素可以是指向
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
(复制了
这里的结论是,指针/引用是必需的,因为原始实例带有复制可能与之交互的成员信息。
而且,多态性在C++中没有完全解决,人们必须认识到它们提供/阻止可能产生切片的动作的义务。
您需要指针或引用,因为对于您感兴趣的多态类型(*),您需要动态类型可以不同于静态类型,换句话说,对象的实际类型与声明的类型不同。在C++中,只有指针或引用才发生这种情况。
(*)genericity,模板提供的多态类型,不需要指针或引用。
当一个对象通过值传递时,它通常被放在堆栈上。把一些东西放在堆栈上需要知道它有多大。当使用多态性时,您知道传入的对象实现了一组特定的特性,但是您通常不知道对象的大小(您也不应该,这必然是好处的一部分)。因此,您不能将它放在堆栈上。但是,您总是知道指针的大小。
现在,不是所有的事情都是一成不变的,还有其他可以减轻痛苦的情况。对于虚拟方法,指向对象的指针也是指向对象的vtable的指针,vtable指示方法的位置。这允许编译器查找和调用函数,而不管它使用的是什么对象。
另一个原因是,对象通常在调用库之外实现,并使用完全不同(可能不兼容)的内存管理器进行分配。它还可能包含无法复制的成员,或者如果使用其他管理器复制这些成员会导致问题。复制可能会有副作用和其他各种并发症。
结果是,指针是对象上您真正正确理解的唯一信息位,它提供了足够的信息来确定您需要的其他位在哪里。