让我们说我们有一个班级
class A
{
int x;
public:
void sayHi()
{
cout<<"Hi";
}
};
int main()
{
A *a=NULL;
a->sayHi();
}
以上代码将在Turbo C(我测试过)上编译并打印Hi
作为输出。
由于a
为NULL
,我原本期待崩溃。更重要的是,如果我将sayHi()
函数设为虚拟,它会说
Abnormal temination(Segmentation fault in gcc)
我知道很多都是依赖于实现的,但是如果有人可以对任何实现有所了解,或者只是给出一个概述,那将非常好。
答案 0 :(得分:7)
显然,代码具有未定义的行为,即,无论你获得什么是偶然的。也就是说,系统在调用非虚拟成员函数时不需要知道对象:可以根据签名调用它。此外,如果成员函数不需要访问成员,则根本不需要对象,只需运行即可。这是您在代码打印一些输出时观察到的。然而,这是否是系统的实现方式尚未定义,即没有任何说它有效。
当调用虚函数类型系统时,系统会开始查看与该对象关联的类型信息记录。在NULL
指针上调用虚函数时,不存在此类信息,并且尝试访问它可能会导致某种崩溃。尽管如此,它并不是必须的,但它适用于大多数系统。
BTW,main()
始终返回int
。
答案 1 :(得分:6)
在C ++中,类的方法不存储在该类的实例中。除了程序员指定的参数之外,它们只是一些透明地接受this
指针的“特殊”函数。
在您的情况下,sayHi()
方法不引用任何类字段,因此,this
指针(NULL
)永远不会被跟踪。
毫无疑问,这仍然是未定义的行为。当您调用此程序时,您的程序可能会选择将讨厌的电子邮件发送到您的联系人列表。在这个特定的例子中,它做了最糟糕的事情,似乎有效。
自我回答问题以来,virtual
方法案例已被添加,但我不会改进我的答案,因为它已包含在其他人的答案中。
答案 2 :(得分:4)
作为概括,从没有超类和虚函数的类实例化的对象的布局如下:
* - v_ptr ---> * pTypeInfo
| |- pVirtualFuncA
| |- pVirtualFuncB
|- MemberVariableA
|- MemberVariableB
v_ptr
是指向v表的指针 - 包含虚拟函数的地址和对象的RTTI数据。没有虚函数的类没有v表。
在上面的示例中,class A
没有虚拟方法,因此没有v-table。这意味着调用sayHi()
的实现可以在编译时确定并且是不变的。
编译器生成的代码将隐式this
指针设置为a
,然后跳转到sayHi()
的开头。由于实现不需要对象的内容,因此当指针为NULL
时它工作的事实是一个愉快的巧合。
如果要使sayHi()
为虚拟,则编译器无法确定在编译器时调用的实现,因此生成代码以查找v表中函数的地址并调用它。在a
为NULL
的示例中,编译器会读取地址0
的内容,从而导致中止。
答案 3 :(得分:1)
如果你调用一个类的非虚方法,对于编译器就足以知道函数属于哪个类,并通过解除引用 - 尽管是一个NULL - 指向类的指针来调用该方法,编译器得到了信息。 sayHi()方法几乎只是一个将指向类实例的指针作为隐藏参数的函数。此指针为NULL,但如果您不引用方法中的任何属性,则无关紧要。
当您将此方法设为虚拟时,情况会发生变化。编译器不知道在编译时哪个代码与该方法相关联,并且必须在运行时解决该问题。它的作用是它查看一个基本上包含所有虚方法的函数指针的表;这个表与类实例相关联,因此它查看相对于NULL指针的内存片段,因此在这种情况下崩溃。