Что такое динамическое распределение памяти на С++?

Я изучаю динамическое распределение памяти на С++ и упоминаются ключевые слова new и new[]. Говорят, что он позволяет пользователям указывать размер выделения памяти во время выполнения, в отличие от простого объявления переменной или массива с фиксированным размером в исходном коде.

Я не понимаю эту концепцию. Как это работает? Мне просто нужно разъяснить идею, и пример будет полезен!

Ответ 1

Итак, если вам нужен массив из 10 целых чисел, вы должны написать:

int arr[10]; 

Но что, если вы хотите сделать что-то подобное:

cout << "How many?";
cin >> num;

int arr[num];

Ну, язык С++ этого не позволяет. Вместо этого вам нужно сделать:

int *arr = new int[num]; 

чтобы создать массив. И позже вы ДОЛЖНЫ [1] использовать:

delete [] arr; 

чтобы освободить память.

Итак, как это работает? Когда вы вызываете new, библиотека времени выполнения С++ [код, который вам не нужно писать, составляющий основу С++], будет определять, сколько пробелов в пространстве num заняло бы, и найти для этого некоторое пространство в памяти. Я не буду вдаваться в подробности о том, "как вы находите какую-то память". На данный момент, просто доверяйте мне, есть некоторая память, доступная где-то, которая может быть использована для хранения некоторых целых чисел.

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

Конечно, если у вас есть машина с, скажем, 256 МБ памяти, и вы пытаетесь попросить пространство для хранения 250 миллионов целых чисел, имея в виду, что целое число занимает более одного байта, оно не собирается работайте - здесь нет "волшебства" - память по-прежнему ограничена тем, насколько доступно в машине... Вы просто имеете право определять в программе, когда она работает, сколько памяти вам нужно, а чем решать, когда ПИСЬЕТ программу.

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

 std::vector<int> arr;

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

 std::shared_ptr<int> arr = new int[num]; 

- это еще один случай, когда "shared_ptr" больше не используется [он отслеживает, что внутри класса общего указателя, поэтому вам не нужно заботиться о освобождении памяти].

[1] Если вы не хотите утечки памяти, и это "плохой стиль" для утечки памяти. Не делайте никого счастливым, если вы это сделаете.

Ответ 2

Я видел много сообщений о распределении памяти на С++, вопросы о "новом операторе" и "операторе new", вопросы о new int(100) vs new int[100], вопросы об инициализации памяти... Я думаю, что должно быть ответ, который суммирует все ясно раз и навсегда, и я выбираю этот вопрос, чтобы написать это резюме. Речь идет о динамическом распределении памяти, т.е. Распределении по куче во время выполнения. Я также предоставляю итоговую реализацию (public domain).


C vs С++

Основные функции для распределения динамической памяти:

  • В C (заголовок <cstdlib>) мы имеем главным образом malloc и calloc и free. Я не буду говорить о realloc.
  • в С++ (заголовок <new>), мы имеем:
    • Шаблон размещения одного объекта с аргументами инициализации:
      • new T( args )
      • new (std::nothrow) T( args )
      • delete ( T* )
    • Распределение шаблонов нескольких объектов с инициализацией по умолчанию:
      • new T[ size_t ]
      • new (std::nothrow) T[ size_t ]
      • delete[] ( T* )
    • Инициализация памяти шаблона без выделения для одного или нескольких объектов:
      • new (void*) T( args )
      • new (void*) T[ size_t ]
    • Внутренние новые выражения для:
      • Распределение исходной памяти ::operator new( size_t );
      • Исходное распределение памяти без исключения ::operator new( size_t, std::nothrow );
      • Инициализация исходной памяти без выделения ::operator new( size_t, ptr ).

Пожалуйста, посмотрите этот пост для краткого сравнения.


Динамические распределения Legacy C

Основные моменты: полное стирание типа (void* указатели), и поэтому нет конструкции/уничтожения, размер указан в байтах (обычно с использованием sizeof).

malloc( size_t ) не инициализирует память вообще (необработанная память содержит мусор, всегда инициализируется вручную перед использованием). calloc( size_t, size_t ) инициализирует все биты до 0 (небольшие служебные данные, но полезны для числовых типов POD). Любая выделенная память должна быть освобождена с помощью free ТОЛЬКО.

Построение/уничтожение экземпляров класса должно выполняться вручную перед использованием/перед выпуском памяти.


Динамические распределения С++

Основные моменты: запутанный из-за похожих синтаксисов, выполняющих разные вещи, все delete -статы называют деструктор, все delete -statements принимают полностью типизированные указатели, некоторые new -statements возвращают полностью типизированные указатели, some new -statements вызывают некоторый конструктор.

Предупреждение: как вы увидите ниже, new может быть либо функцией ключевого слова ИЛИ. Лучше не говорить о "новом операторе" и/или "операторе new", чтобы избегать путаницы. Я называю "new -statements" любыми действительными операторами, которые содержат new как функцию или ключевое слово. Люди также говорят о "new -выражениях", где new - это ключевое слово, а не функция.

Распределение сырой памяти (без инициализации)

Не используйте это самостоятельно. Это внутреннее использование новых выражений (см. ниже).

  • ::operator new( size_t ) и ::operator new( size_t, std::nothrow ) взять размер в байтах и ​​вернуть void* в случае успеха.
  • В случае сбоя, первое исключает std::bad_alloc, последний возвращает NULL.
  • Используйте ::operator new( sizeof(T) ) для одиночного объекта типа Tdelete для выпуска) и ::operator new( n*sizeof(T) ) для несколько объектов (и delete[] для выпуска).

Эти распределения не не инициализируют память, и, в частности, они не вызывают конструктор по умолчанию для выделенных объектов. Поэтому вы ДОЛЖНЫ инициализировать ВСЕ элементы вручную, прежде чем освободить выделение, используя delete или delete[].

Примечание. Я не мог подчеркнуть, что вы НЕ должны использовать это самостоятельно. Однако, если вы должны использовать его, убедитесь, что вы указали указатель на void вместо введенного указателя при вызове delete или delete[] на таких распределениях (всегда после инициализации вручную). Я лично испытал ошибки времени выполнения с не-POD-типами с некоторыми компиляторами (возможно, моя ошибка).

Инициализация исходной памяти (без распределения)

Не используйте это самостоятельно. Это используется внутренне с помощью новых выражений (см. ниже). В следующем случае я предполагаю void *ptr = ::operator new( n*sizeof(T) ) для некоторого типа T и размера n.

Затем ::operator new( n*sizeof(T), (T*) ptr ) инициализирует элементы n типа T, начиная с ptr, используя конструктор по умолчанию T::T(). Здесь нет распределения, только инициализация с использованием конструктора по умолчанию.

Выделение и инициализация одного объекта

  • new T( args ) выделяет и инициализирует память для одного объекта типа T с помощью конструктора T::T( args ). Конструктор по умолчанию не будет вызываться, если аргументы не будут опущены (т.е. new T() или даже new T). Выдает исключение std::bad_alloc при сбое.
  • То же самое для new (std::nothrow) T( args ), за исключением того, что он возвращает NULL в случае сбоя.
  • Используйте delete для вызова деструктора T::~T() и отпустите соответствующую память.

Распределение и инициализация нескольких объектов

  • new T[n] выделяет и инициализирует память для объектов n типа T с использованием конструктора по умолчанию. Выдает исключение std::bad_alloc при сбое.
  • Идем для new (std::nothrow) T[n], за исключением того, что он возвращает NULL в случае сбоя.
  • Используйте delete[] для вызова деструктора T::~T() для каждого элемента и отпустите соответствующую память.

Инициализация памяти (также называемая "размещение новой" )

Здесь нет выделения. Независимо от того, как было выполнено выделение:

  • new (ptr) T(args) вызывает конструктор T::T(args) в памяти, хранящейся в ptr. Конструктор по умолчанию не вызывается, если аргументы не опущены.
  • new (ptr) T[n] вызывает конструктор по умолчанию T::T() на n объекты типа T, хранящиеся от ptr до ptr+n (т.е. n*sizeof(T) байты).

Связанные записи