I was experimenting with C++ and found the below code as very strange.
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; }
I know that the virtual method call crashes because it requires a vtable lookup and can only work with valid objects.
I have the following questions
- How does the non virtual method
say_hiwork on a NULL pointer? - Where does the object
fooget allocated?
Any thoughts?
The object
foois a local variable with typeFoo*. That variable likely gets allocated on the stack for themainfunction, just like any other local variable. But the value stored infoois a null pointer. It doesn’t point anywhere. There is no instance of typeFoorepresented anywhere.To call a virtual function, the caller needs to know which object the function is being called on. That’s because the object itself is what tells which function should really be called. (That’s frequently implemented by giving the object a pointer to a vtable, a list of function-pointers, and the caller just knows it’s supposed to call the first function on the list, without knowing in advance where that pointer points.)
But to call a non-virtual function, the caller doesn’t need to know all that. The compiler knows exactly which function will get called, so it can generate a
CALLmachine-code instruction to go directly to the desired function. It simply passes a pointer to the object the function was called on as a hidden parameter to the function. In other words, the compiler translates your function call into this:Now, since the implementation of that function never makes reference to any members of the object pointed to by its
thisargument, you effectively dodge the bullet of dereferencing a null pointer because you never dereference one.Formally, calling any function — even a non-virtual one — on a null pointer is undefined behavior. One of the allowed results of undefined behavior is that your code appears to run exactly as you intended. You shouldn’t rely on that, although you will sometimes find libraries from your compiler vendor that do rely on that. But the compiler vendor has the advantage of being able to add further definition to what would otherwise be undefined behavior. Don’t do it yourself.