Подтвердить что ты не робот

Доступ к членам класса по указателю NULL

Я экспериментировал с С++ и нашел код ниже как очень странный.

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?

Любые мысли?

4b9b3361

Ответ 1

Объект foo - это локальная переменная с типом Foo*. Вероятно, эта переменная распределяется в стеке для функции main, как и любая другая локальная переменная. Но значение, хранящееся в foo, является нулевым указателем. Он нигде не указывает. Нет экземпляра типа foo, представленного где угодно.

Чтобы вызвать виртуальную функцию, вызывающий должен знать, к какому объекту будет вызываться функция. Это потому, что сам объект является тем, что говорит о том, какую функцию нужно действительно вызывать. (Это часто реализуется, предоставляя объекту указатель на таблицу vtable, список указателей функций, и вызывающий абонент просто знает, что он должен вызывать первую функцию в списке, не зная заранее, где указывает этот указатель.)

Но для вызова не виртуальной функции вызывающему абоненту не нужно все это знать. Компилятор точно знает, какая функция будет вызвана, поэтому она может сгенерировать инструкцию машинного кода CALL, чтобы перейти непосредственно к желаемой функции. Он просто передает указатель на объект, который функция вызывала как скрытый параметр для функции. Другими словами, компилятор переводит ваш вызов функции в это:

void Foo_say_hi(Foo* this);

Foo_say_hi(foo);

Теперь, поскольку реализация этой функции никогда не ссылается на какие-либо элементы объекта, на которые указывает его аргумент this, вы эффективно избегаете пули разыменования нулевого указателя, потому что вы никогда не разыскиваете ее.

Формально вызов любой функции - даже не виртуальной - по нулевому указателю - это поведение undefined. Одним из допустимых результатов поведения undefined является то, что ваш код работает точно так, как вы планировали. Вы не должны полагаться на это, хотя иногда вы найдете библиотеки от своего поставщика компилятора, которые полагаются на это. Но у поставщика компилятора есть преимущество в том, что он может добавить дополнительное определение того, что в противном случае было бы undefined. Не делайте этого самостоятельно.

Ответ 2

Функция члена say_hi() обычно реализуется компилятором как

void say_hi(Foo *this);

Поскольку вы не получаете доступа к каким-либо членам, ваш вызов преуспевает (даже если вы вводите поведение undefined в соответствии со стандартом).

Foo не выделяется вообще.

Ответ 3

Разыменование указателя NULL вызывает "поведение undefined". Это означает, что что-то может произойти - ваш код может даже казаться корректным. Вы не должны зависеть от этого, однако, если вы запустили тот же код на другой платформе (или, возможно, на той же платформе), он, вероятно, сработает.

В вашем коде нет объекта Foo, только указатель, который инициализируется значением NULL.

Ответ 4

Это поведение undefined. Но большинство компиляторов разработали инструкции, которые будут корректно обрабатывать эту ситуацию, если вы не получите доступ к переменным-членам и виртуальной таблице.

посмотрим на разборку в 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. Для упрощения вы можете предположить, что это прошло как неявный параметр, который мы никогда не используем в вашем примере. Но во втором случае мы вычисляем адрес функции из-за виртуальной таблицы - из-за foo addres и получаем ядро.

Ответ 5

a) Он работает, потому что он не разыгрывает ничего через неявный указатель "this". Как только вы это сделаете, бум. Я не уверен на 100%, но я думаю, что разрывы нулевого указателя выполняются RW, защищающими первое 1 Кбайт пространства памяти, поэтому существует небольшая вероятность отклонения нулевой привязки, если вы только разыгрываете ее за 1K-строкой (т.е. Какая-то переменная экземпляра который будет распределяться очень далеко, например:

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

то a- > я возможно было бы неотобрано, когда A является нулевым.

b) Нигде, вы только объявили указатель, который выделяется в стек main(): s.

Ответ 6

Вызов say_hi статически связан. Таким образом, компьютер фактически выполняет стандартный вызов функции. Функция не использует никаких полей, поэтому нет проблем.

Призыв к virtual_say_hi динамически связан, поэтому процессор переходит к виртуальной таблице, и поскольку там нет виртуальной таблицы, он перескакивает где-то случайно и сбой программы.

Ответ 7

В оригинальные дни С++ код С++ был преобразован в C. Объектные методы преобразуются в неъектные методы, подобные этому (в вашем случае):

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

Конечно, имя foo_say_hi упрощается. Для получения более подробной информации найдите man-код имени С++.

Как вы можете видеть, если thisPtr никогда не разыменовывается, тогда код в порядке и преуспевает. В вашем случае не использовались переменные экземпляра или что-то, что зависит от thisPtr.

Однако виртуальные функции разные. Там много поиска объектов, чтобы убедиться, что правильный указатель объекта передан как параметр для функции. Это приведет к разыменованию thisPtr и вызову исключение.

Ответ 8

Важно понимать, что оба вызова производят поведение undefined, и это поведение может проявляться неожиданными способами. Даже если вызов, похоже, работает, он может заложить минное поле.

Рассмотрим это небольшое изменение в вашем примере:

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

Так как первый вызов foo разрешает поведение undefined, если foo равно null, компилятор теперь может предположить, что foo не является нулевым. Это делает резерв if (foo != 0) избыточным, и компилятор может его оптимизировать! Вы можете подумать, что это очень бессмысленная оптимизация, но писатели-компиляторы становятся очень агрессивными, и что-то вроде этого произошло в реальном коде.