访问一个NULL指针的类成员

我正在试验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查找,只能使用有效的对象。

我有以下问题

  1. 非虚拟方法say_hi如何在NULL指针上工作?
  2. foo对象在哪里分配?

有什么想法吗?

对象foo是一个types为Foo*的局部variables。 该variables可能被分配到main函数的堆栈上,就像任何其他局部variables一样。 但是,存储在foo是一个空指针。 它没有指向任何地方。 在任何地方都没有Footypes的实例。

为了调用一个虚函数,调用者需要知道函数被调用的是哪个对象。 这是因为对象本身就是告诉哪个函数应该被调用。 (这经常通过给对象一个指向vtable的指针,一个函数指针列表来实现,而调用者只知道它应该调用列表中的第一个函数,而不事先知道指针指向哪里。)

但是要调用一个非虚函数,调用者不需要知道所有这些。 编译器确切地知道哪个函数会被调用,所以它可以生成一个CALL机器代码指令直接去所需的函数。 它只是传递一个指向该函数被调用的对象的指针,作为函数的隐藏参数。 换句话说,编译器将你的函数调用转换为:

 void Foo_say_hi(Foo* this); Foo_say_hi(foo); 

现在,由于该函数的实现永远不会引用由它的this参数指向的对象的任何成员,所以你实际上躲避了引用空指针的项目符号,因为你永远不会引用它。

forms上,对空指针调用任何函数 – 即使是非虚函数 – 都是未定义的行为。 未定义的行为允许的结果之一是,您的代码似乎运行完全按照您的意图。 不应该依赖这个,尽pipe你有时候会从你的编译器厂商那里find依赖它的库。 但编译器厂商的优势是能够添加进一步的定义,否则将是未定义的行为。 不要自己动手

say_hi()成员函数通常由编译器实现

 void say_hi(Foo *this); 

由于您不访问任何成员,因此您的呼叫成功(即使按照标准进入未定义的行为)。

Foo根本不分配。

解引用NULL指针会导致“未定义的行为”,这意味着任何事情都可能发生 – 您的代码甚至可能看起来正常工作。 然而,你不能依赖这个 – 如果你在不同的平台上运行相同的代码(甚至可能在同一个平台上),它可能会崩溃。

在你的代码中没有Foo对象,只有一个用NULL值初始化的指针。

这是未定义的行为。 但是如果你不访问成员variables和虚拟表,大部分编译器都会正确处理这种情况。

让我们看看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寄存器中。 为了简化,你可以假设是作为隐式parameter passing的,我们从来没有在你的例子中使用。
但在第二种情况下,我们计算由于虚拟表的函数地址 – 由于foo地址和获取核心。

一)它的作品,因为它不通过隐含的“这个”指针取消任何东西。 只要你这样做,繁荣。 我不是100%确定的,但是我认为空指针解引用是通过保护第一个1K的内存空间来完成的,所以如果只引用它过去的1K行(即一些实例variables这将得到很大的分配,如:

  class A { char foo[2048]; int i; } 

那么当A为空时,我可能会被取消。

b)没有任何地方,你只声明了一个指针,它被分配在main():栈上。

对say_hi的调用是静态绑定的。 所以计算机实际上只是一个标准的函数调用。 该function不使用任何字段,所以没有问题。

对virtual_say_hi的调用是dynamic绑定的,所以处理器进入虚拟表,并且由于没有虚拟表,因此随机跳转到某个地方并使程序崩溃。

在C ++的原始时代,C ++代码被转换为C.对象方法被转换为非对象方法(就你的情况而言):

 foo_say_hi(Foo* thisPtr, /* other args */) { } 

当然,名字foo_say_hi是简化的。 有关更多详细信息,请查找C ++名称。

正如你所看到的,如果thisPtr永远不会被解除引用,那么代码是好的,并成功。 在你的情况下,没有实例variables或依赖于thisPtr的任何东西被使用。

但是,虚拟function是不同的。 有很多的对象查找来确保正确的对象指针作为parameter passing给函数。 这将取消引用thisPtr并导致exception。

认识到这两个调用产生未定义的行为是很重要的,这种行为可能会以意想不到的方式出现。 即使这个呼叫似乎有效,它可能正在铺设一个雷区。

考虑你的例子的这个小的改变:

 Foo* foo = 0; foo->say_hi(); // appears to work if (foo != 0) foo->say_virtual_hi(); // why does it still crash? 

由于第一次调用foo启用未定义的行为,如果foo为null,编译器现在可以自由地假定foo 不为 null。 这使得if (foo != 0)多余的,编译器可以优化它! 你可能会认为这是一个非常没有意义的优化,但是编译器编写者已经变得非常激进了,在实际的代码中发生了这样的事情。