Является ли повторное использование ячейки памяти безопасным?


этот вопрос основан на некотором существующем коде C, перенесенном на C++. Меня просто интересует, является ли это"безопасным". Я уже знаю, что не написал бы это так. Я знаю, что код здесь в основном C, а не C++, но он скомпилирован с компилятором C++, и я знаю, что стандарты иногда немного отличаются.

у меня есть функция, которая выделяет память. Я бросил возвращенный void* до int* и начать использовать его.

позже я бросьте возвращенный void* до Data* и начать использовать это.

это безопасно в C++?

пример:

void* data = malloc(10000);

int* data_i = (int*)data;
*data_i = 123;
printf("%dn", *data_i);

Data* data_d = (Data*)data;
data_d->value = 456;
printf("%dn", data_d->value);

Я никогда не читаю переменные, используемые через другой тип, чем они были сохранены, но беспокоюсь, что компилятор может увидеть это data_i и data_d являются разными типами и поэтому не могут юридически псевдонимы друг друга и решают изменить порядок моего кода, например, поместить магазин в data_d до первого printf. Что бы сломать всё.

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

мой код сломан или это "правильные"?

8 64
c++

8 ответов:

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

некоторые замечания:

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

    obj->~obj();
    
  • при создании объектов, рассмотрим размещение новый синтаксис над простым броском (работает со стручками тоже)

    Object* obj = new (data) Object();
    
  • проверить nullptr (или NULL), Если malloc не, NULL возвращается

  • выравнивание не должно быть проблемой, но всегда помните об этом при создании менеджера памяти и убедитесь, что выравнивание подходит

учитывая, что вы используете компилятор C++, если вы не хотите сохранить природу "C" в коде, вы также можете посмотреть на глобальный operator new().

и как всегда, после этого не забудь free() (или delete при использовании new)


вы упоминаете, что пока не собираетесь конвертировать какой-либо код; но если или когда вы его рассматриваете, есть несколько идиоматических функций в C++, которые вы можете использовать над malloc или даже глобального ::operator new.

вы должны смотреть на смарт-указатель std::unique_ptr<> или std::shared_ptr<> и позвольте им позаботиться о проблемах управления памятью.

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

если Data - Это простой старый тип данных (POD, т. е. typedef для базового типа, структура типов POD и т. д.),и выделенная память правильно выровнена для типа (*), тогда ваш код хорошо определенными, что означает, что он будет "работать" (пока вы инициализации каждый член *data_d перед его использованием), но это не очень хорошая практика. (Смотреть ниже.)

если Data не является типом POD, вы направляетесь к проблеме: назначение указателя не вызывало бы никаких конструкторов, например. data_d, который имеет тип "указатель на Data", фактически врет потому что он указывает на что-то, но что-то не типа Data потому что такой тип не был создан / сконструирован / инициализирован. Неопределенное поведение будет не так уж далеко в этом месте.

решение для правильно строительство объект в данной области памяти называется размещение нового:

Data * data_d = new (data) Data();

это указывает компилятору построить Data объект на месте data. Это будет работать для типов POD и non-POD. Вам также нужно будет вызвать деструктор (data_d->~Data()), чтобы убедиться, что он выполняется перед deleteing память.

будьте осторожны, чтобы никогда не смешивать функции распределения / выпуска. Все, что вы malloc() должен быть free()d, что выделяется с new должен delete, а если new [] вы должны delete []. Любая другая комбинация-UB.


в любом случае, использование "голых" указателей для владения памятью не рекомендуется в C++. Вы должны либо

  1. поставить new в конструкторе и соответствующий delete в деструкторе класса, делая объект владелец памяти (включая правильное освобождение, когда объект выходит из области видимости, например, в случае исключения); или

  2. использовать смарт-указатель который эффективно делает выше для вас.


(*): известно, что реализации определяют "расширенные" типы, требования к выравниванию которых не учитываются malloc(). Я не уверен, если язык адвокаты все равно назвали бы их "стручок", на самом деле. MSVC, например, делает 8-байтовое выравнивание на malloc () но определяет расширенный тип SSE __m128 как 16-байтовое выравнивание требование.

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

пример строгого сглаживания:

int a = 0;
float* f = reinterpret_cast<float*>(&a);
f = 0.3;
printf("%d", a);

это строгое нарушение псевдонимов, потому что:

  • время жизни переменных (и их использование) перекрытия
  • они интерпретируют один и тот же фрагмент памяти через две разные "линзы"

если вы не делаете и в то же время, тогда ваш код не нарушает строгое сглаживание.


в C++ время жизни объекта начинается при завершении конструктора и останавливается при запуске деструктора.

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

Примечание: это специально для поддержки записи менеджеров памяти; в конце концов malloc пишется на C и operator new написано в C++ и им явно разрешено объединять память.


я специально использовал объективы вместо типы потому что правило немного сложнее.

C++ обычно используется номинальное ввода: если два типа имеют разные имена, они разные. Если вы получаете доступ к значению динамического типа T как U, то вы нарушаете алиасинг.

есть ряд исключений к этому правилу:

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

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

§9.2/18 если объединение стандартной компоновки содержит две или более структур стандартной компоновки, которые совместно используют общую начальную последовательность, и если объект объединения стандартной компоновки в настоящее время содержит одну из этих структур стандартной компоновки, разрешается проверить общую начальную часть любой из них. Две структуры стандартной компоновки совместно используют общую начальную последовательность, если соответствующие члены имеют типы, совместимые с компоновкой, и либо ни один из членов не является битовым полем, либо оба являются битовыми полями одинаковая ширина для последовательности из одного или нескольких начальных элементов.

дано:

  • struct A { int a; };
  • struct B: A { char c; double d; };
  • struct C { int a; char c; char* z; };

внутри union X { B b; C c; }; вы можете получить доступ к x.b.a,x.b.c и x.c.a,x.c.c в то же время, однако доступ x.b.d (соответственно x.c.z) является нарушением псевдонимов, если текущий сохраненный тип не B (соответственно не C).

Примечание: неофициально структурная типизация похожа на сопоставление типа с кортежем его полей (сглаживание их).

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


в вашем случае, без определения Data я не могу сказать, Может ли быть нарушено правило "линзы", однако, поскольку вы являются:

  • перезапись памяти с помощью Data перед доступом к нему через Data*
  • не доступ к нему через int* после

тогда вы соответствуете правилу жизни, и, таким образом, нет никакого сглаживания, имеющего место в отношении языка.

пока память используется только для одной вещи за раз, это безопасно. Вы в основном используете выделенные данные как union.

Если вы хотите использовать память для экземпляров классов, а не только простых структур C-стиля или типов данных, вы должны помнить, чтобы сделать размещение нового чтобы "выделить" объекты, так как это фактически вызовет конструктор объекта. Деструктор, который вы должны вызвать явно, когда вы закончите с объектом, вы не можете delete оно.

пока вы обрабатываете только"C" -типы, это будет нормально. Но как только вы используете классы C++, у вас возникнут проблемы с правильной инициализацией. Если мы предположим, что Data будет std::string например, код был бы очень неправильным.

компилятор не может действительно переместить хранилище через вызов printf, потому что это видимый побочный эффект. Результат должен быть таким, как будто побочные эффекты производятся в том порядке, который предписывает программа.

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

пока a Data остается стручок это должно быть хорошо. В противном случае вам придется переключиться на размещение новых.

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

Я не нахожу никакой ошибки в повторном использовании пространства памяти. Только то, что меня волнует, - это свисающая ссылка. Повторное использование памяти, как вы сказали, я думаю, что это не имеет никакого влияния на программу.
Вы можете продолжать свое Программирование. Но это всегда предпочтительнее free() пробел, а затем выделить другую переменную.