Получение размера динамического массива в стиле C в сравнении с использованием delete []. Противоречие?

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

Как это возможно, что нет способа получить размер динамического массива только из указателя, и в то же время можно освободить всю память, выделенную с помощью delete [] только для указателя, без необходимости указав размер массива?

delete [] должен знать размер массива, верно? Поэтому эта информация должна где-то существовать. Не должно ли это?

Что не так в моих рассуждениях?

Ответ 1

TL; DR Оператор delete[] уничтожает объекты и освобождает память. Информация N ("количество элементов") необходима для разрушения. Информация S ("размер выделенной памяти") требуется для освобождения. S всегда сохраняется и может запрашиваться расширениями компилятора. N сохраняется, только если для уничтожения объектов требуются деструкторы. Если N хранится, то где он хранится, зависит от реализации.


Оператор delete [] должен сделать две вещи:

а) разрушение объектов (вызов деструкторов, если необходимо) и

б) освобождение памяти.

Давайте сначала обсудим (де) распределение, которое делегируется функциям C malloc и free многими компиляторами (например, GCC). Функция malloc принимает количество байтов, которые должны быть выделены в качестве параметра, и возвращает указатель. Функция free берет только указатель; количество байтов не обязательно. Это означает, что функции выделения памяти должны отслеживать, сколько байтов было выделено. Может быть функция для запроса количества выделенных байтов (в Linux это можно сделать с помощью malloc_usable_size, в Windows с _msize). Это не то, что вы хотите, потому что это говорит не о размере массива, а о количестве выделенной памяти. Поскольку malloc не обязательно дает вам столько памяти, сколько вы просили, вы не можете вычислить размер массива из результата malloc_usable_size:

#include <iostream>
#include <malloc.h>

int main()
{
    std::cout << malloc_usable_size(malloc(42)) << std::endl;
}

Этот пример дает вам 56, а не 42: http://cpp.sh/2wdm4

Обратите внимание, что применение malloc_usable_size (или _msize) к результату new является неопределенным поведением.

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

#include <iostream>

struct foo {
    char a;
    ~foo() {}
};

int main()
{
    foo * ptr = new foo[42];
    std::cout << *(((std::size_t*)ptr)-1) << std::endl;
}

Этот код дает вам 42: http://cpp.sh/7mbqq

Только для протокола: это неопределенное поведение, но с текущей версией GCC это работает.

Таким образом, вы можете спросить себя, почему нет функции для запроса этой информации. Ответ в том, что GCC не всегда хранит эту информацию. Могут быть случаи, когда уничтожение объектов не является операцией (и компилятор может это выяснить). Рассмотрим следующий пример:

#include <iostream>

struct foo {
    char a;
    //~foo() {}
};

int main()
{
    foo * ptr = new foo[42];
    std::cout << *(((std::size_t*)ptr)-1) << std::endl;
}

Здесь ответ больше не 42: http://cpp.sh/2rzfb

Ответ просто мусор - код снова был неопределенным поведением.

Зачем? Поскольку компилятору не нужно вызывать деструктор, ему не нужно хранить информацию. И да, в этом случае компилятор не добавляет код, который отслеживает, сколько объектов было создано. Известно только количество выделенных байтов (которое может быть 56, см. Выше).

Ответ 2

Это делает - распределитель, или некоторые детали реализации позади него, точно знает, каков размер блока.

Но эта информация не предоставляется ни вам, ни "уровню кода" вашей программы.

Мог ли язык быть разработан для этого? Конечно! Вероятно, это случай "не платите за то, что вы не используете" - это ваша обязанность запомнить эту информацию. Ведь вы знаете, сколько памяти вы просили! Часто люди не хотят, чтобы стоимость номера передавалась в стек вызовов, в то время как в большинстве случаев им это не нужно.

Существуют некоторые "расширения" для конкретной платформы, которые могут malloc_usable_size вам то, что вы хотите, например, malloc_usable_size в Linux и _msize в Windows, хотя они предполагают, что ваш распределитель использовал malloc и не делал никаких других действий, которые могут увеличить размер выделенного блок на самом низком уровне. Я бы сказал, что вам все равно лучше отслеживать это самостоятельно, если вам это действительно нужно... или использовать вектор.

Ответ 3

Я думаю, что причина этого - слияние трех факторов.

  1. C++ имеет культуру "вы платите только за то, что используете"
  2. C++ начал свою жизнь как препроцессор для C и, следовательно, должен был быть построен поверх того, что предлагал C.
  3. C++ является одним из наиболее распространенных языков. Функции, которые усложняют жизнь для существующих портов, вряд ли будут добавлены.

C позволяет программисту освобождать блоки памяти без указания размера блока памяти для освобождения, но не предоставляет программисту никакого стандартного способа доступа к размеру выделения. Кроме того, фактический объем выделенной памяти может быть больше, чем запрашивал программист.

Следуя принципу "вы платите только за то, что используете", реализации C++ по-разному реализуют new[] для разных типов. Обычно они сохраняют размер только в том случае, если это необходимо, обычно потому, что тип имеет нетривиальный деструктор.

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

Ответ 4

Этот ответ относится только к Microsoft Visual Studio.

Существует функция с именем _msize, которая будет возвращать неверный /calloced/realloced размер указателя.

Его можно найти в заголовке malloc.h, а параметры:

size_t _msize(
   void *memblock
);

Я не уверен, есть ли эквивалент в gcc. Там наверное должно быть.

Ответ 5

Если delete[] не должен знать размер массива во время его вызова, весь ваш аргумент разваливается. И delete[] не должен знать размер массива во время его вызова. Нужно только знать размер, чтобы сделать блок доступным для использования другими, и абсолютно ничто не требует, чтобы он сделал блок доступным для использования другими в момент delete[].

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

То, что delete[] знает, что размер блока в каждой произвольной точке в течение его времени жизни достаточен, чтобы сделать недействительным ваш аргумент.