Подтвердить что ты не робот

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

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

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

Позже я вернул возвращаемый void* в Data* и начал использовать его.

Является ли это безопасным в С++?

Пример: -

void* data = malloc(10000);

int* data_i = (int*)data;
*data_i = 123;
printf("%d\n", *data_i);

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

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

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

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

4b9b3361

Ответ 1

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

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

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

    obj->~obj();
    
  • Если вы создаете объекты, рассмотрите размещение нового синтаксиса поверх простого литья (также работает с POD)

    Object* obj = new (data) Object();
    
  • Проверьте nullptr (или NULL), если malloc завершается сбой, возвращается NULL

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

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

И как всегда, после выполнения не забывайте free() (или delete при использовании new)


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

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

Ответ 2

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

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

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

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

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

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

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


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

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

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


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

Ответ 3

Правила, связанные с строгим псевдонимом, могут быть довольно сложными.

Пример строгой псевдонимы:

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

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

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

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


В С++ время жизни объекта начинается, когда конструктор заканчивается и останавливается при запуске деструктора.

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

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


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

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

Существует ряд исключений из этого правила:

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

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

§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* после

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

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

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