Что с помощью массивов динамически в C++? [дубликат]

этот вопрос уже есть ответ здесь:

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

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

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

9 ответов


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

  1. использование голых, владеющих указателями.

  2. использование naked new.

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

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

(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), динамические массивы являются несоответствием в C++ и в основном никогда не должны использоваться. Существует несколько стандартных дефектов, относящихся только к динамическим массивам (и не считающихся заслуживающими исправления). Простой аргумент заключается в том, что вы не можете использовать массивы, не зная их размер. Вы можете сказать, что можете использовать значение sentinel или tombstone для динамического обозначения конца массива, но это делает правильность вашей программы стоимостью-зависимая, а не тип-зависимый и, следовательно, не статически проверяемый (само определение "небезопасный"). Вы не можете статически утверждать, что это не ваша вина.

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

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

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

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

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

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

насколько я вижу, есть никогда в случае где массив new уместно.


Я слышал, что такое использование (не этот код точно, а динамическое выделение в целом) может быть небезопасным в некоторых случаях и должно использоваться только с 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, и это очень плохо Вещь).

TLDR: использование необработанных указателей в C++ для управления памятью является плохой практикой (хотя для реализации роли наблюдателя, реализация с необработанными указателями, прекрасна). Управление ресурсами с сырым poiners нарушает SRP и сухой принципы).


есть два основных недостатка этого -

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

  2. во-вторых, память динамически выделяется, что означает, что она размещена в heap не в stack. Разница между heap и stack это то, что стеки очищаются, когда переменная выходит из области видимости но!--3-->s не очищаются автоматически, а также C++ не содержит встроенный сборщик мусора, что означает, если таковые имеются, как delete вызов пропущен, вы оказались с утечкой памяти.


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

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

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


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


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


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


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