9

假设我们有一堂课

class A
{
    int x;
public:
    void sayHi()
    {
        cout<<"Hi";
    }
};

int main()
{
    A *a=NULL;
    a->sayHi();
}

上面的代码将在 Turbo C(我测试过的地方)上编译并Hi作为输出打印。

我期待崩溃,因为ais NULL。更重要的是,如果我将sayHi()功能设为虚拟,它会说

Abnormal temination(Segmentation fault in gcc) 

我知道其中很多是依赖于实现的,但是如果有人可以对任何实现有所了解或者只是给出一个概述,那就太好了。

4

4 回答 4

7

显然,代码具有未定义的行为,即,无论您得到什么都是偶然的。也就是说,系统在调用非虚拟成员函数时不需要知道对象:它可以根据签名来调用。此外,如果成员函数不需要访问成员,则它根本不需要对象并且可以运行。这是您在代码打印一些输出时观察到的。然而,没有定义这是否是系统实施的方式,即,没有什么说它有效。

当调用虚函数类型系统开始查看与对象关联的类型信息记录。在调用虚拟函数时NULL指针上调用虚函数时,不存在此类信息,尝试访问它可能会导致某种崩溃。不过,它不是必须的,但它适用于大多数系统。

顺便说一句,main() 总是返回int

于 2012-09-30T11:26:22.510 回答
6

在 C++ 中,类的方法不存储在该类的实例中。它们只是一些“特殊”函数,this除了程序员指定的参数之外,还透明地接受指针。

在您的情况下,该sayHi()方法不引用任何类字段,因此,永远不会遵循this指针(即)。NULL

毫无疑问,这仍然是未定义的行为。当您调用它时,您的程序可能会选择向您的联系人列表发送讨厌的电子邮件。在这种特殊情况下,它做了最糟糕的事情,并且似乎有效。

自从virtual我回答了这个问题后,已经添加了方法案例,但我不会细化我的答案,因为它包含在其他人的答案中。

于 2012-09-30T11:25:54.450 回答
4

作为概括,从没有超类和虚函数的类实例化的对象的布局如下:

* - v_ptr  ---> *  pTypeInfo
|               |- pVirtualFuncA
|               |- pVirtualFuncB
|- MemberVariableA
|- MemberVariableB

v_ptr是指向 v-table 的指针 - 其中包含对象的虚函数地址和 RTTI 数据。没有虚函数的类没有 v-tables。

在上面的示例中,class A没有虚拟方法,因此没有 v-table。这意味着sayHi()to call 的实现可以在编译时确定并且是不变的。

编译器生成将隐式this指针设置为的代码,a然后跳转到sayHi(). 由于实现不需要对象的内容,因此它在指针为时工作的事实NULL是一个幸运的巧合。

如果要进行sayHi()虚拟化,编译器无法确定要在编译时调用的实现,因此会生成在 v-table 中查找函数地址并调用它的代码。在您的示例 where aisNULL中,编译器读取 address 的内容0,导致中止。

于 2012-09-30T11:42:16.383 回答
1

如果您调用类的非虚拟方法,对于编译器来说,知道该函数属于哪个类就足够了,并且通过取消引用(尽管是 NULL)指向调用该方法的类的指针,编译器会获取该信息。sayHi()方法几乎只是一个将指向类实例的指针作为隐藏参数的函数。此指针为 NULL,但如果您不引用该方法中的任何属性,则无关紧要。

当你把这个方法变成虚拟的时候,情况就发生了变化。编译器在编译时不再知道与该方法关联的代码是什么,必须在运行时弄清楚。它的作用是查看一个表,该表基本上包含所有虚拟方法的函数指针;该表与类实例相关联,因此它查看相对于 NULL 指针的内存片段,因此在这种情况下崩溃。

于 2012-09-30T11:26:48.900 回答