Что не так с использованием динамически распределенных массивов на С++?

Как и следующий код:

int size = myGetSize();
std::string* foo;
foo = new std::string[size];
//...
// using the table
//...
delete[] foo;

Я слышал, что такое использование (а не этот код точно, но динамическое распределение в целом) может быть небезопасным в некоторых случаях и должно использоваться только с RAII. Почему?

Ответ 1

Я вижу три основные проблемы с вашим кодом:

  • Использование голой, владеющих указателей.

  • Использование голой new.

  • Использование динамических массивов.

Каждый из них нежелателен по своим причинам. Я попытаюсь объяснить каждый из них по очереди.

(1) нарушает то, что мне нравится называть субэкспрессивной правильностью, и (2) нарушает правильность формулировки. Идея здесь состоит в том, что никакое утверждение, и даже любое подвыражение, само по себе не должно быть ошибкой. Я принимаю термин "ошибка" свободно, чтобы означать "может быть ошибкой".

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

  • new std::string[25] - ошибка, потому что она создает динамически выделенный объект, который просочился. Этот код может условно стать не ошибкой, если кто-то другой, где-то еще и в каждом случае помнит, чтобы очистить.

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

  • foo = new std::string[125]; - ошибка, потому что снова foo утечка ресурса, если только звезды не выравниваются, и кто-то помнит, в каждом случае и в нужное время, чтобы очистить.

Правильный способ написания этого кода до сих пор был бы следующим:

std::unique_ptr<std::string[]> foo(std::make_unique<std::string[]>(25));

Обратите внимание, что каждое подвыражение в этом выражении не является основной причиной ошибки программы. Это не ваша ошибка.

Наконец, что касается (3), динамические массивы являются ошибками в С++ и в принципе никогда не будут использоваться. Существует несколько стандартных дефектов, относящихся только к динамическим массивам (и не считающиеся заслуживающими фиксации). Простой аргумент заключается в том, что вы не можете использовать массивы, не зная их размера. Вы могли бы сказать, что вы можете использовать значение часового или надгробного камня для динамического разметки конца массива, но это делает правильность вашей программы зависящей от стоимости, а не зависящей от типа и, следовательно, не статически проверяемой (само определение "небезопасно" "). Вы не можете утверждать, что это не ваша ошибка.

Таким образом, вам все равно придется поддерживать отдельное хранилище для размера массива. И угадайте, что, ваша реализация должна дублировать это знание в любом случае, чтобы он мог назвать деструкторов, когда вы говорите delete[], чтобы распутать пустую. Вместо этого правильный способ - не использовать динамические массивы, а вместо этого выделять выделение памяти (и сделать ее настраиваемой с помощью распределителей, почему мы на ней), от создания элементарного объекта. Обертывание всего этого (распределитель, хранилище, количество элементов) в один удобный класс является способом С++.

Таким образом, окончательная версия вашего кода такова:

std::vector<std::string> foo(25);

Ответ 2

Код, который вы предлагаете, не является исключением и альтернативой:

std::vector<std::string> foo( 125 );
//  no delete necessary

есть. И, конечно, vector знает размер позже и может проверка границ в режиме отладки; он может быть передан (по ссылке или даже по значению) к функции, которая затем сможет использовать он, без каких-либо дополнительных аргументов. Новый массив следует за C для массивов и массивов в C серьезно нарушены.

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

Ответ 3

Я слышал, что такое использование (а не этот код точно, но динамическое распределение в целом) может быть небезопасным в некоторых случаях и должно использоваться только с RAII. Почему?

Возьмите этот пример (похожий на ваш):

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    delete [] local_buffer;
    return x;
}

Это тривиально.

Даже если вы правильно напишете код, кто-то может прийти через год и добавить в свою функцию условное или десять или двадцать:

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    if(x == 25)
    {
        delete[] local_buffer;   
        return 2;
    }
    if(x < 0)
    {
        delete[] local_buffer; // oops: duplicated code
        return -x;
    }
    if(x || 4)
    {
        return x/4; // oops: developer forgot to add the delete line
    }
    delete[] local_buffer; // triplicated code
    return x;
}

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

Это по-прежнему тривиальный случай, когда только один ресурс (local_buffer), и он (наивно) предполагает, что код не выдает никаких исключений между распределением и освобождением. Проблема приводит к недостижимому коду, когда ваша функция выделяет ~ 10 локальных ресурсов, может бросать и имеет несколько путей возврата.

Более того, прогрессия выше (простой, тривиальный случай, расширенный до более сложной функции с несколькими путями выхода, расширенный до нескольких ресурсов и т.д.) является естественным прогрессированием кода при разработке большинства проектов. Не используя RAII, разработчик создает естественный способ обновить код, который будет снижать качество, в течение всего жизненного цикла проекта (это называется cruft, и это очень плохое).

TL;DR: использование необработанных указателей в С++ для управления памятью - это плохая практика (хотя для реализации роли наблюдателя реализация с исходными указателями в порядке). Управление ресурсами с помощью raw poiners нарушает SRP и DRY принципы).

Ответ 4

Есть два основных недостатка:

  • new не гарантирует, что выделенная память инициализируется с помощью 0 или null. Они будут иметь значения undefined, если вы не инициализируете их.

  • Во-вторых, память динамически распределяется, что означает, что она размещена в heap не в stack. Разница между heap и stack заключается в том, что стеки очищаются, когда переменная выходит из области видимости, но heap не очищаются автоматически, а также С++ не содержит встроенного сборщика мусора, что означает, t28 > вызов пропущен, у вас закончилась утечка памяти.

Ответ 5

необработанный указатель трудно обрабатывать правильно, например. WRT. копирование объектов.

гораздо проще и безопаснее использовать хорошо проверенную абстракцию, например std::vector.

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

Ответ 6

Если выделенная память не освобождается, когда она больше не нужна, это приведет к утечке памяти. Не указано, что произойдет с утечкой памяти, но современные операционные системы собирают ее, когда программа завершается. Утечки памяти могут быть очень опасными, поскольку в системе может закончиться нехватка памяти.

Ответ 7

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

Ответ 8

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

Ответ 9

См. Стандарты кодирования JPL. Динамическое распределение памяти приводит к непредсказуемому исполнению. Я видел проблемы с распределением динамической памяти в отлично закодированных системах - что со временем происходит фрагментация памяти, как жесткий диск. Выделение блоков памяти из кучи займет больше времени и дольше, пока не будет невозможно выделить запрошенный размер. В этот момент вы начинаете возвращать NULL-указатели, и вся программа вылетает из-за того, что мало кто проверяет условия отсутствия памяти. Важно отметить, что в книге у вас может быть достаточно памяти, однако ее фрагментация предотвращает выделение. Это рассматривается в .NET CLI с использованием "handles" вместо указателей, где время выполнения может собирать мусор, используя метки и -собирайте сборщик мусора, перемещайте память вокруг. Во время развертки он уплотняет память для предотвращения фрагментации и обновления ручек. В то время как указатели (адреса памяти) не могут быть обновлены. Это проблема, потому что сбор мусора больше не детерминирован. Хотя,.NET добавил механизмы, чтобы сделать его более детерминированным. Однако, если вы следуете рекомендациям JPL (раздел 2.5), вам не нужна причудливая сборка мусора. Вы динамически выделяете все необходимое для инициализации, затем повторно используете выделенную память, не освобождая ее, тогда нет риска фрагментации, и вы все еще можете иметь детерминированную сборку мусора.