удалить и освободить против удаления без освобождения - PullRequest
2 голосов
/ 16 апреля 2020

Я читаю документацию по вектору . Здесь я обнаружил, что:

~vector(); вызывает allocator_traits::destroy для каждого из содержащихся элементов и освобождает всю емкость памяти, выделенную вектором, используя его распределитель.

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

Идти дальше с помощью destroy (allocator_type& alloc, T* p);:

Уничтожить элемент объект, на который указывает p, не освобождая его хранилище. В неспециализированном определении allocator_traits эта функция-член вызывает alloc.destroy(p), если такой вызов правильно сформирован. В противном случае он вызывает p->~T().

  • Я не понимаю, что они подразумевают под уничтожением элемента без освобождения его хранилища (снова).
  • Как это involve p->~T(), если мой vector<T> obj {T(), T()} состоит из автоматически объектов, а не указателей?
  • Как он может вызвать destroy для T**, если мой вектор vector<T*>... ?

Я пытаюсь провести параллель между вызовом деструктора для таких объектов, как:

  1. MyClass obj() соответственно
  2. MyClass obj = new MyClass()

против

  1. vector<T> obj {T(), T()}
  2. vector<T*> obj {new T(), new T()}

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

Ответы [ 3 ]

2 голосов
/ 16 апреля 2020

A vector<T> содержит кусок памяти, и в этой памяти содержится некоторое количество T объектов. Вектор управляет хранилищем отдельно от объектов.

Когда вы вызываете v.pop_back(), он уничтожает последний элемент в векторе, но не освобождает хранилище, занимаемое этим элементом. Если вы затем вызовете v.push_back(), он поместит новый (последний) элемент в место, ранее занятое стертым элементом.

1 голос
/ 16 апреля 2020

Я хотел бы дать другое представление здесь и сосредоточиться на векторе указателей

vector<T*> obj {new T(), new T()}

и разнице между уничтожением и освобождением указателей в нем. (Вот как я понимаю ваш вопрос, но, возможно, вы уже знаете все это: D)

Прежде всего: этот вектор содержит только указатели. Указатель обычно имеет длину 4 или 8 байтов. Давайте предположим, что это 8, а вектор содержит 2 указателя, поэтому он должен выделить 16 байтов независимо от sizeof(T). Два объекта, выделенных с помощью new, находятся где-то еще в куче.

Давайте предположим, что этот вектор уничтожен, например, потому что он выходит из области видимости в некоторой функции. Он уничтожит каждый элемент внутри вектора и освободит память, выделенную вектором (например, 16 байтов в этом примере).

  • Уничтожение указателя ничего не делает, так как указатель не намного больше, чем числовое значение c, которое указывает место в памяти. Хотя T может иметь деструктор, T* не имеет .
  • Удаление означает только освобождение памяти, которая используется для хранения указателей . Память, на которую указывают эти указатели, полностью не тронута.

Это похоже на другой пример, который вы привели:

MyClass *obj = new MyClass();

Если obj выходит из области видимости, деструктор MyClass будет не вызываться, сама obj (4 или 8 байт, как указано выше) будет освобождена, но память, на которую указывает obj, не будет затронута. Это приводит к утечке памяти, и то же самое происходит для вектора выше.

Только указатели пропали, память, на которую они указывали, остается.


Это почти то же самое для вектора со значениями

vector<T> obj {T(), T()}

Тот же сценарий, что и выше , но теперь объекты хранятся в памяти, выделенной вектором. Таким образом, вектор должен теперь выделить 2 * sizeof(T) байт, чтобы содержать два объекта. Когда вектор уничтожается, происходит то же, что и выше:

  • Каждый элемент уничтожается. Если у T есть деструктор, он называется.
  • Память, выделенная вектором (2 * sizeof(T) байт), освобождается

Это похоже на

MyClass obj;

Когда это выходит из области видимости, будет вызван деструктор ~MyClass и будет освобождена память obj (sizeof(MyClass) байт). Не происходит утечки памяти, как в приведенном выше векторе.

1 голос
/ 16 апреля 2020

Я не понимаю, как вы можете удалить элемент, не освобождая его.

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

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

char memory[2 * sizeof(T)]; // No Ts yet. Keep in mind alignment is important too.
new(memory) T{/* constructor arguments */}; // One T at the start of the memory, rest unused. 
new(memory + sizeof(T)) T{}; // Two Ts next to each other in the memory.
static_cast<T*>(memory)->~T(); // First part of memory now unused again.
// Destroy any other objects appropriately.
// Free store memory would be explicitly deallocated here.

Как бы это [вызвать] p-> ~ T (), если мой вектор obj {T (), T ()} автоматически состоит из объектов, а не указателей?

Вектор предоставляет этой функции указатель к этому значению. Вектор знает, где находится элемент памяти, и знает, что там находится объект T, поэтому он может получить правильный указатель. destroy берет указатель и возвращается к вызову деструктора через этот указатель. Это безопасно, потому что объект был построен в подходящем поместье, а не через new T(…).

Как он может вызвать разрушение на T **, если мой вектор - вектор ...?

Работает так же, когда тип элемента является указателем. Вектор получает указатель на этот элемент (в данном случае указатель на указатель) и передает его в функцию destroy. Функция уничтожения вызывает деструктор через этот указатель. Теперь вам может быть интересно, почему это работает, когда у указателей нет деструкторов, но есть специальное правило, гласящее, что вызов деструктора для фундаментального типа допустим через псевдоним (в данном случае это имя T) и ничего не делает. Таким образом, код generi c работает без кода для разных T s.

...