Почему эта статическая работа на С++ работает?
Представьте этот код:
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
Почему эта статическая работа работает?
Потому что статический кастинг - это проверка времени компиляции. Существует связь между базой и производным. Так как у него есть отношения, статичные актеры считают, что отношения и верят программисту тоже. Итак, как программист, вы должны убедиться, что базовый объект не должен быть статическим приложением к объекту производного класса.