NULL指针与static_cast的兼容性

时间:2009-12-09 09:16:14

标签: c++ casting

Q1。为什么使用带有static_cast的NULL指针会导致崩溃,而dynamic_cast和reinterpret_cast会返回一个NULL指针?

问题出现在类似于下面给出的方法中:

void A::SetEntity(B* pEntity, int iMyEntityType)
{   
    switch (iMyEntityType)
    {   
    case ENTITY1:
        {
            Set1(static_cast<C*>(pEntity));
            return;
        }
    case ENTITY2:
        {
            Set2(static_cast<D*>(pEntity));
            return;
        }
    case ENTITY3:
        {
            Set3(static_cast<E*>(pEntity));
            return;
        }   
    }
}

Inheritance:
  class X: public B
  class Y: public B
  class Z: public B

  class C: public X, public M
  class D: public Y, public M
  class E: public Z, public M

Q2。从B到C / D / E的static_casting是否有效? (这个工作正常,直到输入变为NULL)

我正在使用gcc版本3.4.3

5 个答案:

答案 0 :(得分:8)

你可以static_cast一个空指针 - 它会给你一个空指针。

在您的代码段中,问题很可能是您将pEntityiMyEntityType的不一致值传递到函数中。因此,当static_cast完成时,它会盲目地转换为错误的类型(与实际对象的类型不同),并且会得到一个无效的指针,该指针稍后会传递给调用堆栈并导致未定义的行为(崩溃程序) 。在同一个案例中dynamic_cast看到该对象实际上不是预期的类型,并返回一个空指针。

答案 1 :(得分:4)

您使用的是什么编译器?从基类型到派生类型的静态强制转换可能会导致对指针的调整 - 特别是如果涉及多个继承(在您的描述中情况似乎不是这种情况)。但是,没有MI,它仍然是可能的。

标准表示如果正在转换空指针值,则结果将为空指针值(5.2.9 / 8静态转换)。但是,我认为在许多编译器中,大多数向下转换(特别是涉及单继承时)不会导致指针调整,所以我可以想象编译器可能有一个错误,以至于它不会对null进行特殊检查这将需要避免将零值空指针“转换”为某些非零值无意义指针。我认为,对于存在这样的错误,你必须做一些不寻常的事情,让编译器必须调整向下转换中的指针。

看看为您的示例生成了哪种汇编代码可能会很有趣。

有关编译器如何布置可能需要使用静态强制转换进行指针调整的对象的详细信息,Stan Lippman's "Inside the C++ Object Model"是一个很好的资源。

Stroustrup's paper on Multiple Inheritance for C++(自1989年起)也是一本很好的读物。如果C ++编译器有像我在这里推测的那样的错误,那就太糟糕了 - Stroustrup在那篇论文中明确讨论了空指针问题(4.5 Zero Valued Pointers)。

关于你的第二个问题:

  

Q2。从B到C / D / E的static_casting是否有效?

只要执行B指针指向C / D / E指针的转换时,B指针实际上指向C / D / E对象的B子对象(分别),这是完全有效的而B不是虚拟基础。这在标准的同一段中提到(5.2.9 / 8静态铸造)。我已经突出显示了与您的问题最相关的段落的句子:

  

“指向cv1 B的指针”类型的右值,其中B是类类型,可以转换为“指向cv2 D的指针”类型的右值,其中D是从B派生的类(第10节),如果存在从“指向D的指针”到“指向B的指针”的有效标准转换(4.10),cv2与cv1具有相同的cv资格,或者更高的cv资格,并且B不是D的虚拟基类。 空指针值(4.10)将转换为目标类型的空指针值。如果“指向cv1 B的指针”类型的rvalue指向实际上是D类型对象的子对象的B,则生成的指针指向D类型的封闭对象。否则,结果为演员阵容未定义。

最后,您可以使用以下内容解决问题:

Set1(pEntity ? static_cast<C*>(pEntity) : 0);

这是编译器应该为你做的。

答案 2 :(得分:3)

static_cast本身不会导致崩溃 - 它在运行时的行为与reinterpret_cast相同。你的代码中的其他地方有问题。

答案 3 :(得分:1)

MyClass* p = static_cast<MyClass*>(0)效果很好。

如果使用多重继承,则static_cast可能会移动指针。 请考虑以下代码:

struct B1 {};
struct B2 {};

struct A : B2, B1 {
 virtual ~A() {}
};

什么是struct AA包含虚拟函数表,B1B2B1相对于A进行了转移。 要将B1转换为A,编译器需要后退。

如果指向B1的指针为NULL,则shift给出无效结果。

答案 4 :(得分:1)

static_cast适用于知道可以完成演员表的情况(要么转换为父类,要么您有其他方法来评估类的类型)。没有运行时检查类型(因此static)。另一方面,dynamic_cast将在运行时检查对象是否真的是您要将其强制转换为的类型。至于reinterpret_cast,除了为不同的目的使用相同的内存之外,它什么都不做。请注意,reinterpret_cast永远不应该用于从一个类更改为另一个类。

最后,NULL指针崩溃的原因static_cast是因为带有继承的static_cast可能需要编译器的一些指针算术。这取决于编译器如何实际实现继承。但是在多重继承的情况下,它没有选择。

查看此内容的一种方法是,子类“包含”父类。它的虚拟表包含父表之一,但具有附加功能。如果在开头添加了这些功能,那么对父类的任何强制转换将指向一个不同的地方......从那里看不到子类的功能。我希望这是有道理的。

关于指针算法的注意事项

首先,多次继承总是如此,但编译器也可能选择单继承。

基本上,如果您使用虚拟方法查看对象内容的内存布局,您可以执行以下操作:

+---------------+----------------+
| ptr to vtable | members   .... |
+---------------+----------------+

在单继承的情况下,这已经足够了。特别是,您可以确保任何派生类的vtable以母类的vtable开头,并且第一个成员是母类的成员。

现在,如果你有多重继承,事情会更复杂。特别是,您可能无法以一致的方式合并vtable和成员(至少在一般情况下不是这样)。所以,假设你继承了A,B和C类,你可能会有类似的东西:

                       A                       B                      C
+----------------------+-----------+-----------+----------+-----------+-----+
| local vtable/members | vtable A  | members A | vtable B | members B | ... |
+----------------------+-----------+-----------+----------+-----------+-----+

这样,如果指向A,您将看到对象为A类型的对象,以及其余对象。但是如果你想看到对象是B类型的,你需要指向地址B等。注意,这可能不是系统所做的,但这就是它的git。