代码之家  ›  专栏  ›  技术社区  ›  Navaneeth K N

在空指针上访问类成员

  •  44
  • Navaneeth K N  · 技术社区  · 16 年前

    我正在实验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 使用空指针?
    2. 物体在哪里 foo 得到分配?

    有什么想法吗?

    8 回复  |  直到 10 年前
        1
  •  82
  •   Rob Kennedy    16 年前

    客体 foo 是类型为的局部变量 Foo* . 该变量可能在堆栈上分配给 main 函数,就像其他局部变量一样。但是 价值 存储在 是空指针。它没有指向任何地方。没有类型的实例 Foo 代表任何地方。

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

    但要调用非虚拟函数,调用方不需要知道所有这些。编译器确切知道将调用哪个函数,因此它可以生成 CALL 机器代码指令直接进入所需功能。它只是将一个指针传递给作为函数隐藏参数调用函数的对象。换句话说,编译器将函数调用转换为:

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

    现在,由于该函数的实现从未引用它所指向的对象的任何成员 this 参数,您可以有效地避开取消引用空指针的要点,因为您从未取消引用一个空指针。

    正式的,打电话来 任何 函数“即使是空指针上的非虚拟函数”也是未定义的行为。未定义行为的一个允许结果是您的代码看起来完全按照您的预期运行。 不应该依赖它,尽管有时您会从编译器供应商那里找到库 依靠它。但是编译器供应商的优势是能够为不定义的行为添加进一步的定义。不要自己动手。

        2
  •  16
  •   Niall    10 年前

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

    void say_hi(Foo *this);
    

    因为您不访问任何成员,所以调用成功(即使您按照标准输入了未定义的行为)。

    Foo 根本没有分配。

        3
  •  7
  •   anon    16 年前

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

    在您的代码中没有foo对象,只有一个用值null初始化的指针。

        4
  •  5
  •   bayda    16 年前

    这是未定义的行为。但大多数编译器都会发出指令,如果您不访问成员变量和虚拟表,这些指令将正确处理这种情况。

    请参阅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:说“hi called as normal function but with” 在ECX寄存器中。为了简化,你可以假设 作为隐式参数传递,我们在示例中从未使用过。
    但在第二种情况下,我们计算函数的地址,由于虚表-由于foo addres而得到核心。

        5
  •  2
  •   Pasi Savolainen    16 年前

    a)它工作是因为它不会通过隐式“this”指针取消引用任何内容。一旦你这样做,砰。我不是100%确定,但我认为空指针取消引用是由rw完成的,它保护了内存空间的前1K,因此,如果只在1K行后取消对空指针的引用,则很可能不会捕获空指针取消引用(例如,一些实例变量将被分配得非常远,例如:

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

    那么当a为空时,a->i可能未被捕获。

    b)在任何地方,您只声明了一个指针,它在main():s堆栈上分配。

        6
  •  2
  •   Uri    16 年前

    打电话说你好是静态绑定的。所以计算机实际上只是对一个函数进行一个标准调用。函数不使用任何字段,因此没有问题。

    对virtual-say-hi的调用是动态绑定的,因此处理器将转到虚拟表,由于那里没有虚拟表,因此它会随机跳到某个地方并使程序崩溃。

        7
  •  1
  •   Tommy Hui    16 年前

    在C++最初的日子里,C++代码被转换成C对象方法被转换成非对象方法(如你的例子):

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

    当然,foo-say-hi这个名字被简化了。有关详细信息,请查阅C++名称。

    如您所见,如果从未取消对thisptr的引用,那么代码是好的,并且是成功的。在您的例子中,没有使用实例变量或依赖于thisptr的任何东西。

    但是,虚拟功能是不同的。有很多对象查找来确保将正确的对象指针作为参数传递给函数。这将取消引用thisptr并导致异常。

        8
  •  1
  •   Mark Ransom    10 年前

    重要的是要认识到 二者都 调用产生未定义的行为,并且该行为可能以意外的方式出现。即使电话 出现 为了工作,它可能正在铺设一个雷区。

    考虑一下这个小小的改变:

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

    从第一次打电话到 foo 启用未定义的行为,如果 为空,编译器现在可以自由假定 无效的。这使得 if (foo != 0) 冗余,编译器可以优化它!您可能认为这是一个非常无意义的优化,但是编译器编写人员变得非常积极,在实际的代码中也发生了类似的事情。