Объект foo
является локальной переменной с типом Foo*
. Эта переменная, вероятно, выделяется в стеке для функции main
, как и любая другая локальная переменная. Но значение , сохраненное в foo
, является нулевым указателем. Это никуда не указывает. Нигде не представлено ни одного экземпляра типа Foo
.
Чтобы вызвать виртуальную функцию, вызывающая сторона должна знать, к какому объекту вызывается функция. Это потому, что сам объект - это то, что говорит о том, какая функция действительно должна быть вызвана. (Это часто реализуется, давая объекту указатель на vtable, список указателей на функции, и вызывающий просто знает, что он должен вызвать первую функцию в списке, не зная заранее, куда указывает этот указатель.)
Но для вызова не виртуальной функции вызывающей стороне не нужно знать все это. Компилятор точно знает, какая функция будет вызвана, поэтому он может сгенерировать инструкцию машинного кода CALL
, чтобы перейти непосредственно к нужной функции. Он просто передает указатель на объект, для которого была вызвана функция, как скрытый параметр функции. Другими словами, компилятор переводит ваш вызов функции в это:
void Foo_say_hi(Foo* this);
Foo_say_hi(foo);
Теперь, поскольку реализация этой функции никогда не ссылается на какие-либо элементы объекта, на которые указывает ее аргумент this
, вы эффективно избегаете пули разыменования нулевого указателя, потому что вы никогда не разыменовываете один.
Формально вызов любой функции - даже не виртуальной - для нулевого указателя - это неопределенное поведение. Одним из допустимых результатов неопределенного поведения является то, что ваш код работает точно так, как вы предполагали. Вы не должны полагаться на это, хотя вы иногда найдете библиотеки от вашего поставщика компилятора, которые делают , полагаются на это. Но у поставщика компилятора есть то преимущество, что он может добавить дополнительное определение к тому, что иначе было бы неопределенным поведением. Не делай этого сам.