Почему эта статическая работа на С++ работает?

Представьте этот код:

class Base {
 public:
  virtual void foo(){}
};

class Derived: public Base {
  public:
    int i;
    void foo() override {}
    void do_derived() {
      std::cout << i;
    }
};

int main(){
  Base *ptr = new Base;
  Derived * static_ptr = static_cast<Derived*>(ptr);
  static_ptr->i = 10;  // Why does this work?
  static_ptr->foo(); // Why does this work?
  return 0;
}

Почему я получаю результат 10 на консоли? Интересно, потому что я думал, что ptr является указателем на базовый объект. Поэтому объект не содержит int я или метод do_derived(). Создан ли новый производный объект?

Когда я объявляю виртуальный метод do_derived() в классе Base тоже, то этот выбирается, но почему?

Ответ 1

int* i = new int{1};
delete i;
std::cout << *i << std::endl;

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

Однако, это явно поведение undefined, и нет никаких гарантий относительно того, что может произойти.


В вашем случае код компилируется как static_cast не будет выполнять никаких проверок, он просто преобразует указатель. По-прежнему поведение undefined для доступа к памяти, которая еще не была выделена и инициализирована.

Ответ 2

Как уже упоминалось в комментариях, "происходит то, что вы ожидали", это не то же самое, что "работает".

Сделайте несколько изменений:

#include <iostream>
#include <string>

class Base{
public:
    virtual  void foo(){
        std::cout << "Base::foo" << std::endl;
    }
};

class Derived: public Base{
public:
    int a_chunk_of_other_stuff[1000000] = { 0 };
    std::string s = "a very long string so that we can be sure we have defeated SSO and allocated some memory";
    void foo() override {
        std::cout << "Derived::foo" << std::endl;
    }
    void do_derived() {
        std::cout << s << std::endl;
    }
};

int main(){
    Base *ptr = new Base;
    Derived * static_ptr = static_cast<Derived*>(ptr);
    static_ptr -> foo(); // does it though?
    static_ptr -> do_derived(); // doesn't work?
    static_ptr->a_chunk_of_other_stuff[500000] = 10;  // BOOM!
    return 0;
}

Пример вывода:

Base::foo

Process finished with exit code 11

В этом случае ни одна из операций не сделала то, что мы ожидали. Назначение в массив вызвало segfault.

Ответ 3

Заявление:

Base *ptr = new Base;

Не всегда выделяет sizeof(Base) - он, вероятно, выделит больше памяти. Даже если он выделяет точные байты sizeof(Base), это не обязательно означает, что любой доступ к байтам после этого диапазона (т.е. sizeof(Base)+n, n > 1) будет недействительным.

Следовательно, допустим, что размер класса Base равен 4 байтам (из-за таблицы виртуальных функций в большинстве реализаций компилятора на 32-битной платформе). Тем не менее, оператор new, API управления кучей, управление памятью ОС и/или аппаратное обеспечение выделяют 16 байтов для этого распределения (предположения). Это делает допустимыми дополнительные байты 12! Он делает следующее утверждение действительным:

static_ptr->i = 10;

Так как теперь он пытается записать 4 байта (sizeof(int), обычно) после первых 4 байтов (размер полиморфного класса Base).

Вызов функции:

static_ptr->foo();

просто сделал бы вызов Derived::foo, поскольку указатель имеет тип Derived, и в нем нет ничего плохого. Компилятор должен вызвать Derived::foo. Метод Derived::foo даже не пытается получить доступ к любому элементу данных производного класса (и даже базового класса).

Вы звонили:

static_ptr->do_derived();

который обращается к i члену производного. Это все равно будет иметь силу, поскольку:

  • Вызов функции всегда действителен, пока метод не попытается получить доступ к элементу данных (т.е. обращается к чему-то из указателя this).
  • Доступ к элементам данных стал действительным из-за выделения памяти (UD поведение)

Заметим, что верно следующее:

class Abc
{
public:
void foo() { cout << "Safe"; }
};

int main()
{
   Abc* p = NULL;
   p->foo(); // Safe
}

Вызывайте его правильно, поскольку он переводит на:

    foo(NULL);

где foo:

void foo(Abc* p)
{
    // doesn't read anything out of pointer!
}

Ответ 4

Почему эта статическая работа работает?

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