我正在尝试使用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
分配?
有什么想法吗?
该对象foo
是一个带有类型的局部变量Foo*
.该变量很可能在main
函数的堆栈上分配,就像任何其他局部变量一样.但存储的值foo
是空指针.它并不指向任何地方.在Foo
任何地方都没有任何类型的实例.
要调用虚函数,调用者需要知道调用函数的对象.那是因为对象本身就是告诉哪个函数应该被调用的原因.(这通常是通过给对象一个指向vtable的指针,一个函数指针列表来实现的,而调用者只知道它应该调用列表中的第一个函数,而不事先知道指针指向的位置.)
但是要调用非虚函数,调用者不需要知道所有这些.编译器确切地知道将调用哪个函数,因此它可以生成CALL
机器代码指令以直接转到所需的函数.它只是将指向函数的对象的指针传递给函数的隐藏参数.换句话说,编译器将您的函数调用转换为:
void Foo_say_hi(Foo* this); Foo_say_hi(foo);
现在,由于该函数的实现永远不会引用其this
参数指向的对象的任何成员,因此您实际上避免了取消引用空指针的子弹,因为您从不取消引用它.
形式上,在空指针上调用任何函数(甚至是非虚函数)都是未定义的行为.未定义行为的允许结果之一是您的代码似乎完全按照您的意图运行.你不应该依赖,虽然有时你会发现从你的编译器供应商库,不依赖于这一点.但是编译器供应商的优势在于能够为未定义的行为添加进一步的定义.不要自己动手.
所述say_hi()
成员函数通常是由编译器所实施
void say_hi(Foo *this);
由于您不访问任何成员,因此您的调用成功(即使您根据标准输入了未定义的行为).
Foo
根本没有分配.
取消引用NULL指针会导致"未定义的行为",这意味着任何事情都可能发生 - 您的代码甚至可能正常工作.但是,您不能依赖于此 - 如果您在不同的平台上运行相同的代码(甚至可能在同一平台上),它可能会崩溃.
在您的代码中没有Foo对象,只有一个指针,其值为NULL.
这是未定义的行为.但是,如果您不访问成员变量和虚拟表,大多数编译器都会生成正确处理此情况的指令.
让我们看看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寄存器中有这个.为简化起见,您可以假设这是作为隐式参数传递的,我们从未在您的示例中使用过.
但在第二种情况下,我们计算虚拟表的功能地址 - 由于foo地址和获取核心.