我正在尝试使用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
在哪里分配?有什么想法吗?
答案 0 :(得分:82)
对象foo
是一个类型为Foo*
的局部变量。该变量很可能在main
函数的堆栈上分配,就像任何其他局部变量一样。但是foo
中存储的值是一个空指针。它并不指向任何地方。任何地方都没有Foo
类型的实例。
要调用虚函数,调用者需要知道调用函数的对象。那是因为对象本身就是告诉哪个函数应该被调用的原因。 (这通常是通过为对象提供指向vtable的指针,函数指针列表来实现的,并且调用者只知道它应该调用列表中的第一个函数,而不事先知道指针指向的位置。)
但是要调用非虚函数,调用者不需要知道所有这些。编译器确切地知道将调用哪个函数,因此它可以生成CALL
机器代码指令以直接转到所需的函数。它只是将指向函数的对象的指针传递给函数的隐藏参数。换句话说,编译器将您的函数调用转换为:
void Foo_say_hi(Foo* this);
Foo_say_hi(foo);
现在,由于该函数的实现永远不会引用其this
参数指向的对象的任何成员,因此您实际上避免了取消引用空指针的内容,因为您从不取消引用它。
正式地,在空指针上调用任何函数 - 甚至是非虚函数 - 是未定义的行为。未定义行为的允许结果之一是您的代码似乎完全按照您的意图运行。 您不应该依赖它,尽管您有时会从编译器供应商那里找到做依赖的库。但是编译器供应商的优势在于能够为未定义的行为添加进一步的定义。不要自己动手。
答案 1 :(得分:16)
say_hi()
成员函数通常由编译器实现为
void say_hi(Foo *this);
由于您不访问任何成员,因此您的调用成功(即使您根据标准输入了未定义的行为)。
Foo
根本没有分配。
答案 2 :(得分:7)
取消引用NULL指针会导致“未定义的行为”,这意味着任何事情都可能发生 - 您的代码甚至可能正常工作。但是,您不能依赖于此 - 如果您在不同的平台上运行相同的代码(甚至可能在同一平台上),它可能会崩溃。
在你的代码中没有Foo对象,只有一个指针,其值为NULL。
答案 3 :(得分:5)
这是未定义的行为。但是,如果您不访问成员变量和虚拟表,大多数编译器都会生成正确处理此情况的指令。
让我们看一下visual studio中的反汇编,以了解会发生什么
Foo* foo = 0;
004114BE mov dword ptr [foo],0
foo->say_hi(); // works well
004114C5 mov ecx,dword ptr [foo]
004114C8 call Foo::say_hi (411091h)
foo->say_virtual_hi(); // will crash the app
004114CD mov eax,dword ptr [foo]
004114D0 mov edx,dword ptr [eax]
004114D2 mov esi,esp
004114D4 mov ecx,dword ptr [foo]
004114D7 mov eax,dword ptr [edx]
004114D9 call eax
你可以看到Foo:say_hi被称为通常的功能,但在ecx寄存器中使用 this 。为简化起见,您可以假设 this 作为隐含参数传递,我们从未在您的示例中使用过 但在第二种情况下,我们计算虚拟表的功能地址 - 由于foo地址和获取核心。
答案 4 :(得分:2)
a)它有效,因为它不会通过隐式“this”指针取消引用任何内容。一旦你这样做,繁荣。我不是100%肯定,但我认为空指针解除引用是由RW保护第一个1K的内存空间,因此如果你只将它取消引用超过1K行(即一些实例变量),则很少有空引用的可能性。这将分配到很远,如:
class A {
char foo[2048];
int i;
}
然后a-&gt;当A为空时,我可能会被删除。
b)无处,你只声明了一个指针,它在main():s堆栈上分配。
答案 5 :(得分:2)
对say_hi的调用是静态绑定的。因此,计算机实际上只是对函数进行标准调用。该函数不使用任何字段,因此没有问题。
对virtual_say_hi的调用是动态绑定的,因此处理器进入虚拟表,由于那里没有虚拟表,它会随机跳转到某个地方并使程序崩溃。
答案 6 :(得分:2)
认识到两个调用都会产生未定义的行为,这种行为可能以意想不到的方式表现出来,这一点非常重要。即使呼叫出现,它也可能正在铺设雷区。
请考虑对您的示例进行这一小改动:
Foo* foo = 0;
foo->say_hi(); // appears to work
if (foo != 0)
foo->say_virtual_hi(); // why does it still crash?
由于foo
的第一次调用在foo
为空时启用了未定义的行为,编译器现在可以自由地假设foo
不为null。这使if (foo != 0)
变得多余,编译器可以优化它!您可能认为这是一种非常无意义的优化,但编译器编写者已经变得非常积极,在实际代码中发生了类似的事情。
答案 7 :(得分:1)
在C ++的最初阶段,C ++代码被转换为C.对象方法被转换为非对象方法(在您的情况下):
foo_say_hi(Foo* thisPtr, /* other args */)
{
}
当然,简化了名称foo_say_hi。有关更多详细信息,请查找C ++名称修改。
正如您所看到的,如果thisPtr永远不会被解除引用,那么代码就可以了并且成功了。在您的情况下,没有使用任何实例变量或依赖于thisPtr的任何内容。
然而,虚拟功能是不同的。有很多对象查找以确保将正确的对象指针作为参数传递给函数。这将取消引用thisPtr并导致异常。