Доступ к членам класса по нулевому указателю - PullRequest
44 голосов
/ 21 марта 2009

Я экспериментировал с 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 работает с указателем NULL?
  2. Где выделяется объект foo?

Есть мысли?

Ответы [ 8 ]

82 голосов
/ 21 марта 2009

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

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

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

void Foo_say_hi(Foo* this);

Foo_say_hi(foo);

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

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

16 голосов
/ 21 марта 2009

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

void say_hi(Foo *this);

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

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

7 голосов
/ 21 марта 2009

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

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

5 голосов
/ 21 марта 2009

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

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

2 голосов
/ 24 апреля 2015

Важно понимать, что оба вызова вызывают неопределенное поведение, и это поведение может проявиться неожиданным образом. Даже если вызов кажется действующим, возможно, он устанавливает минное поле.

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

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) избыточным, и компилятор может оптимизировать его! Вы можете подумать, что это очень бессмысленная оптимизация, но авторы компиляторов становятся очень агрессивными, и что-то подобное произошло в реальном коде.

2 голосов
/ 21 марта 2009

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

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

2 голосов
/ 21 марта 2009

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

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

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

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

1 голос
/ 21 марта 2009

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

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

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

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

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

...