Указатель C++ может вызывать функцию-член без объекта

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

class A{    
public:   
       virtual void f1(){cout<<"f1\n";}
       void f2(){cout<<"f2\n";};
};

int main(){    
    A *p=0;
    p->f2();
    return 0;
}

Выход: f2

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

5 ответов

Это не ошибка. Вы вызвали неопределенное поведение. Вы можете получить что угодно, включая ожидаемый результат.

Разыменование NULL указатель является неопределенным поведением.

Кстати, нет такой вещи, как "ошибка C++". Ошибки могут возникать в компиляторах C++ не на том языке, который он сам.

Как уже указывалось, это неопределенное поведение, так что все идет.

Чтобы ответить на вопрос с точки зрения реализации, почему вы видите это поведение?

  1. Невиртуальный вызов реализован как обычный вызов функции, с this указатель (значение null) передается как пареметр. Параметр не разыменовывается (так как переменные-члены не используются), поэтому вызов завершается успешно.

  2. Виртуальный вызов требует поиска в vtable чтобы получить адрес фактической функции для вызова. vtable Адрес хранится в указателе в данных самого объекта. Таким образом, чтобы прочитать его, опровержение this указатель обязателен - ошибка сегментации.

Это будет работать на большинстве компиляторов. Когда вы делаете вызов метода (не виртуального), компилятор переводит:

obj.foo();

к чему-то:

foo(&obj);

куда &obj становится this указатель для foo метод. Когда вы используете указатель:

Obj *pObj = NULL;
pObj->foo();

Для компилятора это не что иное, как:

foo(pObj);

то есть:

foo(NULL);

Вызов любой функции с нулевым указателем не является преступлением, нулевой указатель (т. Е. Указатель с нулевым значением) будет помещен в стек вызовов. Это зависит от целевой функции, чтобы проверить, было ли передано значение null. Это как звонить:

strlen(NULL);

Который скомпилирует, а также запустит, если это обработано:

 size_t strlen(const char* ptr) {

     if (ptr==NULL) return 0;
     ...  // rest of code if `ptr` is not null
}

Таким образом, это очень верно:

((A*)NULL)->f2();

Пока f2 не является виртуальным, и если f2 не читает и не пишет ничего из this, включая любые вызовы виртуальных функций. Статические данные и доступ к функциям все еще будут в порядке.

Однако, если метод является виртуальным, вызов функции не так прост, как кажется. Компилятор помещает дополнительный код для позднего связывания данной функции. Позднее связывание полностью основано на том, на что указывает this указатель. Это зависит от компилятора, но вызов как:

obj->virtual_fun();

Будет включать поиск текущего типа obj с помощью поиска в виртуальной таблице функций. следовательно, obj не должно быть нулевым.

Когда вы создаете класс

class A{    
public:   
       virtual void f1(){cout<<"f1\n";}
       void f2(){cout<<"f2\n";};
};

Компилятор помещает код функций-членов в текстовую область. Когда вы делаете p->MemberFunction() тогда компилятор просто отдает p и пытается найти функцию MemberFunction используя информацию о типе p который Class A,

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

ПРИМЕЧАНИЕ. Все зависит от того, как компилятор реализует доступ к функциям-членам. Некоторые компиляторы могут решить, является ли указатель объекта нулевым, прежде чем получить доступ к функции-члену, но тогда указатель может иметь некоторое значение мусора вместо 0 который компилятор не может проверить, поэтому обычно компиляторы игнорируют эту проверку.

Вы можете многого добиться с неопределенным поведением. Вы даже можете вызвать функцию, которая принимает только 1 аргумент и получить второй, например так:

#include <iostream>

void Func(int x)
{
    uintptr_t ptr = reinterpret_cast<uintptr_t>(&x) + sizeof(x);
    uintptr_t* sPtr = (uintptr_t*)ptr;
    const char* secondArgument = (const char*)*sPtr;
    std::cout << secondArgument << std::endl;
}

int main()
{
    typedef void(*PROCADDR)(int, const char*);
    PROCADDR ext_addr = reinterpret_cast<PROCADDR>(&Func);

    //call the function
    ext_addr(10, "arg");
    return 0;
}

Скомпилируйте и запустите под Windows, и вы получите "arg" в качестве результата для второго аргумента. Это не ошибка в C++, это просто глупо с моей стороны:)

Другие вопросы по тегам