Является ли повторное использование ячейки памяти безопасным?
этот вопрос основан на некотором существующем коде 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 ответов:
это "ок", он работает так, как вы его написали (предполагая примитивы и простые старые типы данных (стручки)). Это безопасно. Это фактически пользовательский менеджер памяти.
некоторые замечания:
если объекты с нетривиальными деструкторами создаются в расположении выделенной памяти, убедитесь, что он называется
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()
), чтобы убедиться, что он выполняется передdelete
ing память.будьте осторожны, чтобы никогда не смешивать функции распределения / выпуска. Все, что вы
malloc()
должен бытьfree()
d, что выделяется сnew
долженdelete
, а еслиnew []
вы должныdelete []
. Любая другая комбинация-UB.
в любом случае, использование "голых" указателей для владения памятью не рекомендуется в C++. Вы должны либо
поставить
new
в конструкторе и соответствующийdelete
в деструкторе класса, делая объект владелец памяти (включая правильное освобождение, когда объект выходит из области видимости, например, в случае исключения); илииспользовать смарт-указатель который эффективно делает выше для вас.
(*): известно, что реализации определяют "расширенные" типы, требования к выравниванию которых не учитываются 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()
пробел, а затем выделить другую переменную.