我想知道当我们使用向下转换和向上转换时指针转换真正发生了什么。
我有2个问题。其中的前2个是评论。第三季度结束。
#include<iostream>
using namespace std;
class A{
public:
virtual void f()
{
cout<<"A"<<endl;
}
};
class B: public A{
public:
virtual void f()
{
cout<<"B"<<endl;
}
};
int main()
{
A* pa =new A();
B* pb =new B();
A* paUpcast= new B();
/*
Q1:
Is the line above equivalent to the following?
A* paUpcast = (A*) B;
*/
B* pbDowncast=(B*)paUpcast;
/*
Q2:Why we cannot use the following 2 code;
B* pbDowncast=new A();
B* pbDowncast = (B*) pa;
*/
pa->f();
pb->f();
paUpcast->f();
pbDowncast->f();
return 1;
}
Q3:我正在尝试总结一个规则,以推断如果我们将虚函数和指针一起使用时会发生什么,但我只是无法弄清楚。
最初,我认为虚函数会将我们引向指针真正指出的地方。
因此,当我们键入
A* paUpcast= new B();
paUpcast->f();
如果A.f()是虚函数,则第二行将显示“B”,因为paUpcast实际上指向B对象
但是,当我们键入
B* pbDowncast=(B*)pa;
pbDowncast->f();
并且将显示“A”而不是“B”,这使矛盾发生了。
谁能解释或给我一些提示?非常感谢
最佳答案
我会尝试解释我的理解方式。帮助我的秘诀是考虑乐高积木。
在您的情况下,我们有两个乐高积木,一个名为A
,另一个名为B
...,但您可以想象B
片断是由两个片断组成的片断,其中一个片断是A
的相同类型:
A B
+-+ +-+-+
|a| |a|b|
+-+ +-+-+
然后,您可以使用指针来表示每个乐高积木,但每个积木都有其自己的形状,因此,请想象一下:
A* pa =new A();
B* pb =new B();
A* paUpcast= new B();
A *pa --> +-+ new A()
|a|
+-+
B* pb --> +-+-+ new B()
|a|b|
+-+-+
A* paUpcast --> +-+-+ new B()
|a|b|
+-+-+
请注意,
paUpcast
指针是A
类型的指针,但持有一块B
类型的指针,B
片断与A
指针不同,因为您可以看到它是一个比其基数稍大的片断。这是您在谈论的上流,基本指针就像一个通配符,可以将继承树中所有向下相关的内容保留下来。
好吧,假设您真的要写这个:
A* paUpcast = (A*) new B();
是的,是的。您创建了一个B
类的新实例,并将其存储到A
类的指针中,在将新实例分配给该指针之前进行转换并不会改变将其存储到基类指针中的事实。记住乐高积木。进行
B* pbDowncast=new A()
会发生什么?:B* pbDowncast --> +-+ new A()
|a|
+-+
创建一个新的基本clas实例并将其存储到指向派生类的指针中,如果您仔细查看该乐高积木不适合,您将尝试将其视为派生类!
A
部分缺少需要考虑的B
类型的额外内容;所有这些东西都“存储”在乐高玩具的额外部分B = all the A stuff plus something more
中: B
+-+-----------------------------------+
|a|extra stuff that only B pieces have|
+-+-----------------------------------+
如果您尝试调用仅
B
类具有的方法,将会发生什么?有了B
指针,您就可以调用所有B
方法,但是您创建的实例是从A
类型开始的,而该实例没有B
方法,它并不是由所有这些额外的东西创建的。它对我来说并没有矛盾,记得乐高积木,
pa
指针指向一块A
类型:A *pa --> +-+
|a|
+-+
这部分缺少所有的
B
东西,事实是缺少在标准输出上打印f()
的B
方法...但是它有一种在输出上打印f()
的A
方法。希望对您有所帮助!
编辑:
不,我不同意。向下转换不是完全不适当的,但是根据其用途,它可能是不适当的。像所有C++工具一样,向下转换具有实用性和使用范围。尊重良好使用的所有欺骗手段都是适当的。
那么,使用向下转换工具有什么好处?恕我直言,任何不会破坏代码或程序流程的东西,都可以使代码尽可能地可读,并且(对于我来说最重要)是程序员知道他在做什么。
毕竟,向下转换可能的继承分支是一种常见的做法:
A* paUpcast = new B();
static_cast<B*>(paUpcast)->f();
但是使用更复杂的继承树会很麻烦:
#include<iostream>
using namespace std;
class A{
public:
virtual void f()
{
cout<<"A"<<endl;
}
};
class B: public A{
public:
virtual void f()
{
cout<<"B"<<endl;
}
};
class C: public A{
public:
virtual void f()
{
cout<<"C"<<endl;
}
};
A* paUpcast = new C();
static_cast<B*>(paUpcast)->f(); // <--- OMG! C isn't B!
为了解决这个问题,您可以使用
dynamic_cast
A* paUpcast = new C();
if (B* b = dynamic_cast<B*>(paUpcast))
{
b->f();
}
if (C* c = dynamic_cast<C*>(paUpcast))
{
c->f();
}
但是
dynamic_cast
以lack of performance闻名,您可以研究dynamic_cast
的一些替代方法,例如内部对象标识符或转换运算符,但是为了坚持这个问题,如果正确使用,向下转换一点也不坏。