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

я экспериментировал с 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 ответов


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

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

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

void Foo_say_hi(Foo* this);

Foo_say_hi(foo);

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

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


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

void say_hi(Foo *this);

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

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


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

в коде нет объект Foo, только указатель, который initalised со значением null.


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

давайте посмотрим разборку в 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 и получаем ядро.


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

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

тогда a - >я, возможно, был бы не пойман, когда A ноль.

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


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

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


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

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

конечно, имя foo_say_hi упрощено. Для получения более подробной информации найдите c++ name mangling.

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

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

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

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

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