访问一个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查找,只能使用有效的对象。
我有以下问题
- 非虚拟方法
say_hi
如何在NULL指针上工作? -
foo
对象在哪里分配?
有什么想法吗?
对象foo
是一个types为Foo*
的局部variables。 该variables可能被分配到main
函数的堆栈上,就像任何其他局部variables一样。 但是,存储在foo
的值是一个空指针。 它没有指向任何地方。 在任何地方都没有Foo
types的实例。
为了调用一个虚函数,调用者需要知道函数被调用的是哪个对象。 这是因为对象本身就是告诉哪个函数应该被调用。 (这经常通过给对象一个指向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)
多余的,编译器可以优化它! 你可能会认为这是一个非常没有意义的优化,但是编译器编写者已经变得非常激进了,在实际的代码中发生了这样的事情。