我正在试验 C++,发现下面的代码很奇怪。
class Foo{
public:
virtual void say_virtual_hi(){
std::cout << "Virtual Hi";
}
void say_hi()
{
std::cout << "Hi";
}
};
int main(int argc, char** argv)
{
Foo* foo = 0;
foo->say_hi(); // works well
foo->say_virtual_hi(); // will crash the app
return 0;
}
我知道虚拟方法调用会崩溃,因为它需要 vtable 查找并且只能使用有效对象。
我有以下问题
- 非虚方法
say_hi
如何在 NULL 指针上工作? - 对象
foo
在哪里分配?
有什么想法吗?
原文由 Navaneeth K N 发布,翻译遵循 CC BY-SA 4.0 许可协议
对象
foo
是类型为Foo*
的局部变量。就像任何其他局部变量一样,该变量可能会在main
函数的堆栈上分配。但是存储在foo
中的 值 是一个空指针。它没有指向任何地方。在任何地方都没有Foo
类型的实例。要调用虚函数,调用者需要知道该函数是在哪个对象上被调用的。那是因为对象本身告诉我们应该真正调用哪个函数。 (这通常通过给对象一个指向 vtable 的指针、一个函数指针列表来实现,而调用者只知道它应该调用列表中的第一个函数,而不事先知道该指针指向的位置。)
但是要调用一个非虚拟函数,调用者不需要知道所有这些。编译器确切地知道哪个函数将被调用,因此它可以生成一个
CALL
机器代码指令直接转到所需的函数。它只是将一个指向函数调用对象的指针作为隐藏参数传递给函数。换句话说,编译器将您的函数调用转换为:现在,由于该函数的实现从不引用其
this
参数所指向的对象的任何成员,因此您有效地避开了取消引用空指针的子弹,因为您从不取消引用空指针。正式地,在空指针上调用 任何 函数——甚至是非虚拟函数——都是未定义的行为。未定义行为的允许结果之一是您的代码似乎完全按照您的预期运行。 您 不 应该依赖它,尽管有时您会从编译器供应商那里找到依赖它的库。但是编译器供应商的优势在于能够为原本未定义的行为添加进一步的定义。不要自己做。