2009-03-21 4 views
41

Я экспериментировал с C++ и нашел приведенный ниже код очень странным.Доступ к членам класса по указателю 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; 
} 

Я знаю, что происходит сбой виртуального вызова метода, поскольку он требует поиска и виртуальных таблиц может работать только с действительными объектами.

У меня есть следующие вопросы

  1. Как неофициальный виртуальный метод say_hi работы по указателю NULL?
  2. Где размещается объект foo?

Любые мысли?

+3

См. [Это] (http://stackoverflow.com/questions/2474018/when-does-invoking-a-member-function-on-a-null-instance-result-in-undefined-behav) для чего язык говорит об этом. Оба являются неопределенным поведением. – GManNickG

ответ

80

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

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

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

void Foo_say_hi(Foo* this); 

Foo_say_hi(foo); 

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

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

+1

Хороший ответ. Спасибо –

+0

Там также кажется путаницей в том, что код функции и данные объекта - это две разные вещи. Взгляните на этот http://stackoverflow.com/questions/1966920/more-info-on-memory-layout-of-an-executable-program-process. Данные объекта недоступны после инициализации в этом случае из-за нулевого указателя, но код всегда доступен в памяти в другом месте. – Loki

+0

FYI это происходит от '[C++ 11: 9.3.1/2]': «Если для объекта, который не относится к типу' X', вызывается нестатическая функция-член класса 'X', или типа, полученного из 'X', поведение не определено." Ясно, что '* foo' не имеет типа' Foo' (как его не существует). –

7

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

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

+0

Спасибо. Что вы думаете о втором вопросе? Где * Foo * получает выделение? –

+0

foo не является объектом, это указатель. Этот указатель выделяется в стеке (например, любая переменная, которая не помечена как «статическая» или выделена «новым». И она никогда не указывает на действительный объект. – jalf

+0

Нет Foo :-) Foo - это указатель. – 2009-03-21 18:54:12

16

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

void say_hi(Foo *this); 

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

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

+0

Спасибо. Если * Foo * не распределяется, как происходит вызов? Я немного смущен .. –

+1

Процессор или сборка, соответственно, не имеют понятия о деталях HLL кода. C++ не виртуальные функции - это просто нормальные функции с контрактом, что «этот» указатель находится в заданном месте (регистр или стек, зависит от компиляторов). Пока вы не получаете доступ к этому «указателю», все в порядке. – arul

2

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

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

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

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

2

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

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

+0

Это имеет смысл. Спасибо –

1

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

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

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

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

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

5

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

позволяют увидеть разборку в визуальной студии понять, что происходит

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 и получаем ядро.

+0

Спасибо. Можете ли вы рассказать мне, как я могу получить эту разборку в Visual Studio? Я использую VS2008 –

+2

Debug-> Windows-> Демонтаж под отладчиком – bayda

1

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

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

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

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

Смежные вопросы