指针向下转型和向上转型的使用区别是什么?

7

我想知道在进行向上转型和向下转型时指针转换到底发生了什么。 我有两个问题。前两个是注释。第三个问题在结尾。

#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();

第二行将显示“B”,如果A.f()是虚函数,因为paUpcast实际上指向B对象。
然而,当我们输入
B* pbDowncast=(B*)pa;
pbDowncast->f();

它将显示"A"而不是"B",这导致了矛盾的发生。

有人能解释或给我一些提示吗?非常感谢。

2个回答

11
我会尝试解释我所理解的内容。有一个帮助我的提示是将其类比成乐高积木。
在你的情况下,我们有两个乐高积木,一个名为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= new B();

上面这行代码等价于以下代码吗?

A* paUpcast = (A*) B;

嗯,假设你真的想写成这样:A* paUpcast = (A*) new B();,那么是的,它们是等价的。你创建了B类的一个新实例并将其存储到指向A类的指针中,在赋值之前转换新实例并不会改变它将被存储到基类指针中的事实。

为什么我们不能使用以下两个代码:

B* pbDowncast=new A();

B* pbDowncast = (B*) A;

记住乐高积木。在执行B* pbDowncast=new A()时会发生什么呢?
B* pbDowncast --> +-+ new A()
                  |a|
                  +-+

创建一个新的基类实例并将其存储到指向派生类的指针中,您试图将基类视为派生类。如果仔细观察,乐高积木块不适合!A 块缺少被认为是 B 种类所必需的 额外部分;所有这些东西“都存储”在乐高积木块的额外部分中,B = 所有A的内容加上更多的东西
  B
+-+-----------------------------------+
|a|extra stuff that only B pieces have|
+-+-----------------------------------+

如果你尝试调用只有类 拥有的方法会发生什么情况?有一个 指针,可以调用所有 方法,但是你创建的实例来自 种类,不会有 方法,它没有被创建为这个额外的东西。
然而,当我们键入
B * pbDowncast =(B *)pa; pbDowncast->f();
显示“A”而不是“B”,这导致矛盾发生。
对我来说,这并不像矛盾,记住乐高积木,pa指针指向的是类型为 的积木片。
A *pa --> +-+
          |a|
          +-+

这段文字缺少所有的B内容,事实上它缺少了在标准输出上打印B的f()方法...但它有一个在输出上打印A的f()方法。希望对您有所帮助!
编辑:
“看起来你也认为使用向下转型是不合适的,不是吗?”
不,我不同意。向下转型并不完全不合适,但根据其用途可能是不合适的。像所有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因其性能不佳而广为人知,你可以学习一些替代方案,例如内部对象标识符或转换运算符,但为了回答问题,如果正确使用,向下转型并不是坏事。


哥们儿,感谢你的图形解释。看起来你也同意使用 downcast 是不合适的,是吗? - StevenR
@paperBirdMaster:在向上转型和向下转型方面,这是我找到的最好的之一。谢谢! - Amit Pal

3
不要在C++中使用C风格的类型转换!这样做没有理由,会混淆你想做什么的意思,更重要的是,如果你没有将转换的对象正确转换,它可能会产生有趣的结果。在上述情况下,你总是可以使用static_cast<T*>(),不过所有下行转换都应该使用dynamic_cast<T*>():后者只有在转换确实是有效的情况下才成功,此时它会返回指向正确对象的指针,并根据需要调整指针值(在涉及多重继承的情况下指针值可能会发生变化)。如果dynamic_cast<T*>(x)失败,即x实际上不是指向类型为T(或其派生类)的对象的指针,则它返回null。
现在,回到问题本身:你进行的指针转换只影响指针本身,不会影响对象。也就是说,所指向的对象的类型永远不会改变。在问题1的场景中,你创建了一个指向派生类型的指针,并将其赋给指向基类型的指针。由于派生类型是基类型的子类,因此这种转换是隐式的,但等价于:
A* paUpcast = static_cast<A*>(pb);

在第二种情况下,你将paUpcast转换为B*。由于paUpcast是将B*转换为A*的结果,因此使用static_cast<T*>(x)可以将其转换回B*:当使用static_cast<>()转换指针或引用时,可以反转隐式转换的效果。如果您想以任何方式导航类层次结构,而不仅仅是反转隐式转换的效果,则需要使用dynamic_cast<>()。在这种情况下,您也可以使用dynamic_cast<>(),但dynamic_cast<>()具有一些成本。
现在,对于第三种情况,您实际上拥有指向A对象的pa,该对象不是B对象。编译器允许您将pa强制转换为B*,但您正在欺骗编译器:pa不是将B*隐式转换为A*的结果,并且使用static_cast<B*>(pa)(或(B*)pa在这种情况下是等效的,但C样式的转换并不总是等效于static_cast<>()),结果是未定义的行为:编译器会执行它想要做的任何操作。由于AB对象的布局相似,因此它最终调用了A的虚函数,但这只是许多可能的结果之一,并且没有保证会发生什么。如果您使用了dyanmic_cast<B*>(pa),则结果将是空指针,表示从paB*的转换无法工作。

感谢您的解释。根据您所说的,向下转换在我的情况下似乎不合适,如果我在向下转换中使用 static_cast,则结果将取决于编译器。 - StevenR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接