Выделение памяти под вектор c
Deleaker ищет утечки ресурсов в программах на С++: утечки памяти, дескрипторов, GDI объектов и многие другие.Работает как расширение в Visual Studio, так и в виде отдельного приложения.
четверг, июня 30, 2005
Выделение памяти под vector
В описании контейнера vector можно встретить такие функции как size и resize , capacity и reserve . С первого взгляда кажется, что половина явно лишняя, ведь размер у вектора может быть только один.
Почему так? Потому что память для вектора выделяется "про запас"(подробнее ниже), и надо уметь управлять и количеством элементов вектора, и количеством памяти, которое он занимает. size и resize - нужны для работы с реальным числом элементов вектора, capacity и reserve - для работы с памятью.
size - выдает количество элементов в векторе
resize - изменяет количество элементов в векторе
capacity - выдает под сколько элементов выделена память
reserve - резервиует память
Вот что сделает код, скомпиленный Visual C++ 6.0:
vector v;
v.push_back(1); //size==1, capacity==1
v.push_back(17); //size==2, capacity==2
v.push_back(23); //size==3, capacity==4
Зачем нужен этот запас?
Допустим, у нас есть вектор из n элементов. Что происходит, когда программист добавляет еще один? Когда есть запасная память, элемент пишется туда. Когда ее нет, выделяется непрерывный кусок памяти достаточный для n*K элементов, где K - коэффицент. В него копируются предыдущие n , добавляется наш новый элемент, старый кусок размером n освобождается. Если бы запаса не было, то память бы выделялась каждый раз при добавлении нового элемента, что страшно неэффективно.
Зачем нужен reserve , если все и без участия программиста так хорошо работает? Это может быть полезно в некоторых ситуациях. Например, знание того, как выделяется память под вектор, можно использовать и в начале, при его задании. Допустим, я точно знаю, что в векторе будет где-то 100-110 элементов. Я сразу же создам вектор размером 110, это поможет избежать перевыделений памяти, положительно скажется на производительности.
vector v;
v.reserve(110);
Это вовсе не означает, что зарезервировано место для 110-ти элементов ровно. Зарезервировано место как минимум для 110 элементов, возможно больше.
Почему обязательно нужно выделять непрерывный кусок памяти, почему при увеличении размера вектора нельзя выделить кусочек где-нибудь еще? Чтобы можно было передавать vector как параметр в функции, которые требуют массив. Например, в какую-нибудь старую библиотечную С-функцию, которая принимает массив и его размер, можно передать вектор, при условии, что он не пустой. Это было сделано специально, чтобы убедить людей пользоваться векторами. Ведь можно продолжать использовать старые библиотеки.
//если где-то описана такая функция
void myPrint(int* v, int vsize);
//ее можно вызвать так
vector v;
v.push_back(2);
v.push_back(3);
myPrint(&v[0], v.size());
Коэффицент К
(Все приведенные ниже рассуждения верны только для first-fit схем выделения памяти, то есть выделяется первый по порядку подходящий кусок)
Что за коэффицент K , который используется при выделении памяти , чему он равен? Так, как у нас там происходит выделение памяти. Новую выделили, прокопировали туда данные, старую освободили. И так несколько раз. Если память идет подряд, то образуется пробел, который можно было бы использовать. Нужно подобрать такой коэффицент, который позволит эти пробелы использовать. Было вычислено, что коэффицент должен быть меньше, чем (1+sqrt(5))/2 . Что примечательно, выражение (1+sqrt(5))/2 - это же "золотое сечение".
Откуда берется это число?
Я видела вот такое доказательство:
Пусть у нас изначально размер вектора был S , S>0 .
После первого перевыделения памяти, он стал K*S .
После второго K*K*S .
Нам нужно, чтобы K*K*S влезло в S+K*S , то есть K*K*S . S>0 , на него можно сократить.
K^2-K-1
Решаем, получаем
K (второй корень, отрицательный, не интересен)
Правильно? Не правильно. Кусок K*S все еще занят, его нельзя использовать.
Нам нужно, чтобы в момент выделения K^(n+1)*S память размером S+K*S+K^2*S+. +K^(n-1)*S была достаточной, чтобы вместить этот K^(n+1)*S . K^n*S все еще занят, мы его использовать не можем. В итоге получаем.
S+K*S+K^2*S+. +K^(n-1)*S или, зная, что S>0
1+K+K^2+. +K^(n-1)
Решать я это не буду, Andrew Koenig считает, что подходящий в данном случае корень (1+sqrt(5))/2 , доверюсь ему.
Если говорить уж совсем точно, то (1+sqrt(5))/2 не совсем правильное число, нужнен коэффицент поменьше. Потому что нужна еще дополнительная память для разных служебных нужд.
В Visual C++ 6.0 взята константа 2. Такой коэффицент использовать пробелы не дает. А вот в Visual C++ 7 уже используется константа 1.5.
Swap Trick
Про выделение памяти ясно. А как с освобождением памяти? Да, при увеличении размера вектора будет выделена память, а когда вектор уменьшается? Нет, она не будет освобождена. Что несколько неудобно, потому что если в векторе было 10000 элементов, а потом их количество уменьшилось до 10 и осталось где-то в таких пределах, то получается что куча памяти пропадает зря. Что можно сделать в такой ситуации? Сделать новый вектор и туда прокопировать старый. Это можно сделать красиво с помощью swap trick.
vector(v).swap(v);
В этой строчке происходит следующее: создается безымянный vector , который инициализируется элементами из v, а потом он меняется с вектором v местами. Это вовсе не гарантирует, что памяти будет выделено ровно столько, сколько нужно, а не больше. Это зависит от реализации. Но, скорее всего, все будет лучше, чем было.
Я читала у Герба Саттера, что swap сохраняет в корректном состоянии все итераторы, ссылки и указатели.
Может я чего-то не понимаю, но у меня получилось вот что.
std::vector v;
std::vector ::iterator it;
vector(v).swap(v);
cout
Updated 23.11.2005 :
Вычитала, что в Visual C++ 7.1 при вызове метода clear() память таки высвобождается. Не могу проверить для 7.1, зато проверила для Microsoft Visual C++ Toolkit 2003. Действительно, память освобождается.
То есть для такого кода:
vector v;
v.reserve(17);
coutv.clear();
cout
Вывод получается следующим
Microsoft Visual C++ Toolkit 2003:
17
0
MSVC++6.0:
17
17
MinGW gcc 3.4.4:
17
17
Стандарт не указывает должна ли освобождаться память при вызове метода clear(), так что все компиляторы здесь действуют по Стандарту.
привет Ребят,
Задание таково, прога читает слова из файла и в зависимости от выбора опций показывает мне количество уникальных слов, общее количество и печатает мне их в алфавитном порядке.
Когда массив содержал 1000 элементов, все работало, но нужно сделать динамическое выделение памяти,
компилируется, но воспроизвести не могу, Сделан Дамп памяти..
Подскажите, что не так?
скорее всего ошибка в моей insert function
Динамическое выделение памяти для массива
Кто может помочь решить задачу. Для заданного двумерного массива из n строк и m столбцов вывести.
Динамическое выделение памяти для массива
Не могу разобраться, почему программа вызывает точку остановы после выбора действия в функции.
Динамическое выделение памяти для структур
Добрый день. Есть следующие структуры: typedef struct __attribute__((aligned(16)))
Динамическое выделение памяти для строки
Подскажите, пожалуйста , почему в данной программе появляются ошибки в строке return Vector(v1.x.
dzrkot, Вам трудно глянуть на одну функцию?
Программа очень проста,
Я не знаю, о каком оформлении идет речь и комментов нет, т к это лабораторная была, не сдаем ее.
dzrkot, я всегда делаю автоматическое форматирование через терминал..
с миллионами \n мне легче понимать код,
В любом случае спасибо, что хотя бы посмотрели.
сделала сама, сколько смогла
так почему тебе мешает оформление? Программа действительно небольшая!
Добавлено через 1 минуту
Kuzia domovenok,
спасибо, почитаю, попробую дописать
Дело в том, что понятия деструктор нам не давали и тем более конструктор копирования, оператор присвоения..
Я не очень знаю, как и писать, и использовать, и в чем их суть..
погуглю, спасибо )
даже если без них, я для начала проверяю элементарно в файлом в несколько строк,
вероятно у меня где-то итераторы пытаются выйти за границы вектора, но логика проста, как только размер вектора равен количеству уже имющихся эл-ов, я увеличиваю его размер, т е создаю указатель на бОльший вектор, переписываю туда все, удаляю устарый и меняю указатель на новый бОльший вектор..
Динамическое выделение памяти для строки
Добрый день. Такой вопрос возник: При создании строки с помощью указателя на char и ввода с помощью.
Динамическое выделение памяти для массива
Допустим следующее: Имеется массив типа структуры struct stct buf1 Далее массив по ходу.
Динамическое выделение памяти для массива
Есть лаба, задание скриншотом ниже. Я вроде бы как сделал его, но вот второй массив который я.
QMap> копирование QVector в другой вектор
Добрый вечер. У меня есть функция FaceSearchDB. Я хочу копировать значения из vectorа в QMap в.
Std::vector/QVector в классе или std::vector/QVector классов?
Доброе время суток! Собственно вопрос в самой теме, есть некий класс class WorkJornal
QVector
Qvector это что?
QVector to QVector
Господа, есть необходимость копировать целый один QVector (допустим v1) в нужное место QVector v2.
И да наверно это не очень правильно выделять для списка память как пишут выше. В таких контейнерах Qt сам распределяет данные в памяти в удобных ему местах.
И да наверно это не очень правильно выделять для списка память как пишут выше. В таких контейнерах Qt сам распределяет данные в памяти в удобных ему местах.
Если известен будущий размер вектора или размер блока, или предполагаемый, или например, что вектор точно будет больше какого-то размера, то есть смысл выделить заранее заданное количество. Таким образом можно сэкономить на операциях расширения вектора.
Спасибо. А как возвращать указатель на такой вектор? Я пытался так.
А вот тут я не понял, извините. Как произойдет экономия если вектор будет в дальнейшем расширяемый? Выходит зададим допустим ему размер 5 элементов, Qt найдет в памяти место под 5 рядом и зарезервирует, а в дальнейшем мы добавим еще парочку элементов и они будут лежать где-то в другом конце памяти, или я ошибаюсь?
Предположу о экономии времени доступа к элементам при не расширяемом векторе. Но я тут не силен, мысли в слух)
ошибаешься, вектор кутэшный как и стл вектор представляет собой обертку над динамическим массивом, положим добавляются 5 элементов сперва - вектор внутри себя выделил место под пять элементов (и то не факт), далее добавляется еще пять элементов - вектор выделяет в другом месте место под 10 элементный массив, перекопирует те первоначально добавленные 5 элементов и затем дописывает новые пять элементов, первоначально выделенная память высвобождается. Если изначально задать некую емкость то элементы будут записываться во внутренний массив без реалокации, пока не превышена емкость
Создавая объект за объектом, мы часто не обращаем внимания на такую «мелочь», как динамическое выделение памяти. Наравне с копированием и сериализацией, выделение памяти из кучи через new постепенно сводит на нет преимущества C++ в скорости. Чем интенсивнее мы пользуемся заветным new, тем сложнее становится приложению, поскольку память кончается, фрагментируется и всячески стремится утекать. Эта участь уже постигла удобные, но неявно опасные для производительности контейнеры STL: vector, string, deque, map. Особенно обидно терять скорость на выделении небольших объектов в больших количествах. Но есть способ обработать размещение памяти таких объектов на стеке, при этом скрывая детали реализации в специальный класс данных. В этом нам поможет механизм размещающего new — непревзойденный способ оптимизации приложения, полного частых и мелких выделений памяти из кучи.
В прошлом уроке мы делали поразительные вещи: работали с объектами C++ как с контейнерами, содержащими значения типа, вычисленного на этапе выполнения и заполненного динамически. Мы активно использовали надстройку Copy-on-Write над std::shared_ptr, которым ссылались на реальный тип данных, при заполнении объекта. При этом подразумевалось, что память под любую инициализацию данных мы будем выделять также динамически, вызывая new каждый раз, как только нам понадобятся новые данные произвольного типа.
Такой подход имеет свои преимущества. Данные можно разделять между несколькими объектами, откладывая копирование. Можно, в принципе, ничего не знать заранее о типе данных. Однако есть у этого метода и ряд недостатков, из-за которого Copy-on-Write используется, как правило, для объектов, потенциально довольно больших.
Первый недостаток выясняется сразу же. Массовое динамическое выделение памяти серьезно замедляет выполнение программы, особенно массовое неявное выделение памяти через new. Да, я в курсе и про std::string, и про std::vector, которые зачастую, не спрашивая программиста, начинают перераспределять память, вызывая один new за другим (причем про переразмещение данных в std::vector мы еще поговорим). Хороший специалист в C++ разработке всегда знает об этих забавных особенностях стандартных контейнеров и понимает, как избежать лишних затрат на выделение новых сегментов памяти. Чем всегда был хорош чистый си, так это именно тем, что любая работа с памятью выполнялась прозрачно, в C++ всегда нужно держать в голове целый ряд случаев неявной работы с памятью.
Второй недостаток является следствием первого. Частое выделение небольших сегментов памяти в больших количествах приведет к жуткой фрагментации памяти и невозможности выделить даже довольно небольшой блок памяти единым куском, например для инициализации того же std::vector или std::string. В результате мы получаем bad_alloc безо всяких видимых причин. Памяти намного больше, чем нужно, а выделить непрерывный блок даже небольшого размера в условиях сильно фрагментированной памяти не получится.
Таким образом, для небольших объектов, сравнимых с int64_t, которые можно спокойно размещать на стеке, можно и нужно использовать другую технику обработки данных. Такие объекты можно передавать по значению, можно сколько угодно раз копировать, не откладывая до первого изменения, поскольку банально копируется один-два регистра.
При этом мы не должны отходить от практики объявления деталей данных в реализации. Но кое-чем придется пожертвовать: нам нужно будет заранее знать точный размер данных в байтах. Он потребуется для того, чтобы вместе с обычным указателем на данные держать в классе буфер для размещения данных объекта. Теперь подробнее.
Первый класс
Внешне почти ничего не меняется. Все тот же класс, обеспечивающий API объектов. Класс содержит ссылку на данные, класс которых объявлен через forward declaration и будет вынесен в детали реализации. Из-за этого поле класса нельзя объявить объектом данного типа, однако на тип данных можно сослаться простым указателем и заранее завести буфер для хранения данных объекта в самом же объекте. Если объект будет создан, например, на стеке, то и все данные будут храниться на стеке как часть объекта. Теперь рассмотрим пример, чтобы все встало на свои места:
В этом фрагменте кода мы продолжаем идеологию сокрытия данных в реализации, все, что мы знаем о данных класса, — это имя класса и наличие указателя на данные. Однако теперь у нас есть возможность не лезть за памятью в heap. Класс в терминологии C++ все так же хранит данные в виде своих полей. По сути, данные разместятся в буфере m_buffer, память под который выделена уже при создании класса. Осталось лишь объяснить детали, как разместить данные в буфер байт.
Размещающий new
Как правило, немногие вспоминают про такое полезное свойство оператора new, как возможность указать готовую область памяти для размещения создаваемого объекта. Все, что нам потребуется, — это написать new(m_buffer) для создания любого типа объекта в выделенном буфере. Звучит просто, однако нужно помнить, что платим мы высокую цену: заранее указывая максимальный размер буфера. Мало того, размер буфера попадает в заголовочный файл и явно участвует в объявлении API.
Зато мы выигрываем в скорости. Если, выделяя данные в куче на каждую инициализацию, мы рискуем отстать от Java, то, размещая все данные в стеке, мы имеем скорость чистого си, недостижимую скорость для почти любого языка высокого уровня, кроме C++. При этом уровень абстракции крайне высок, мы выстраиваем API на обычных объектах C++, скрывая детали реализации. Единственное ограничение — размер, который мы задаем; мы уже не можем запросто менять в реализации набор полей у класса данных, всегда нужно помнить о размере. Мало того, нам необходимо проверять размер данных, описанных в реализации, на соответствие с указанным в заголовочном файле. Просто потому, что сборка библиотеки может расходиться с версией заголовочных файлов, например при получении из различных источников. Рассмотрим пример, как должна выглядеть подобная проверка, как и создание объекта в подготовленной памяти размещающим new.
Здесь static_assert фактически выполнится на этапе компиляции, поэтому инициализация m_data будет выполнена, только если для object::data достаточно памяти в буфере m_buffer. Аналогично у класса-наследника, например flower, класса object данные также не должны превышать заданную планку, поскольку данные мы храним в реализации базового класса.
Очевидно, что для этого нужен protected-метод get_buffer() для получения адреса m_buffer в базовом классе, а также protected-конструктор object от object::data*. Так же, как и в прошлом выпуске, мы наследуем данные наследников от данных базового класса, поэтому flower::data* совместим с object::data*. Для безопасности стоит в базовый конструктор от object::data* добавить проверку на то, что передан адрес именно заранее выделенного буфера:
В результате, как и раньше, мы имеем возможность эмулировать динамическую типизацию, работая с обычными объектами классов. Например, так:
Объекты с данными большого размера
Осталось выяснить, что делать с объектами, чей размер данных выходит за рамки обозначенного максимума. На самом деле и здесь все довольно просто. Достаточно, чтобы в лимит вписывался размер copy_on_write, который по сути является надстройкой над std::shared_ptr, где impl — реализация класса данных произвольного размера. Поскольку размер std::shared_ptr не зависит от размера самих объектов класса data::impl, мы получаем универсальный способ хранения данных с переходом от хранения по значению к хранению по ссылке.
Однако отвлечемся от решения проблемы единого API для объектов с динамической типизацией и рассмотрим другой пример оптимизации через размещающий new.
copy_on_write::flashback
Если кто-то пропустил прошлый выпуск, то класс copy_on_write — это шаблонный класс для хранения данных с оптимизацией копирования. Эмулируя указатель, этот класс имеет хитрую перегрузку operator-> для const и non-const случаев. При копировании объектов мы ссылаемся на одни и те же данные, не вызывая дорогостоящего копирования. Однако, как только мы вызываем неконстантный метод класса данных, потенциально изменяющий данные, мы отцепляем для текущего объекта свою копию данных. Упрощенно реализация выглядит примерно так:
Таким образом, при выборе максимального размера данных для встроенного буфера стоит учесть размер класса, содержащего copy_on_write в качестве поля.
Поля выборки данных
Самый мощный способ оптимизации через размещающий new — это поля записей выборки в результате SQL-запроса. Выборка запрашивает набор данных самых разнообразных типов, от целочисленных и вещественных до строк и массивов. Хотя сами данные получаются динамически и типы полей, полученные со стороны базы данных, приходится инициализировать с эмуляцией динамической типизации, но зато все записи содержат один и тот же набор типов полей, по которому можно определить общий размер данных для каждой записи. Это позволяет нам выделить память под поля записи лишь однажды, вычислив размер по типам полей, входящим в каждую запись выборки. Можно также выделить память однажды для всех записей единым блоком, однако, как правило, после выборки над записями производят всевозможные операции, в том числе фильтруя и сортируя их, поэтому сами записи имеет смысл описать в виде Copy-on-Write объектов для удобства последующих операций. Выделять же для каждого поля память из кучи неоправданно дорого.
Так будет выглядеть наш класс запись, если упростить объявление и использовать copy_on_write напрямую от класса данных:
Здесь для упрощения пояснения введен вектор типов полей std::vector, массив enum-значений. На самом деле этот массив следует набрать из аргументов через boost::fusion либо, используя Boost.Preprocessor, набрать массив из обобщенных объектов типа object от любого типа аргументов. Нам сейчас важен сам механизм однократного выделения памяти из кучи для каждой записи.
где field::size вычисляет размер данных по переданному field::type, а field::calc_size вычисляет уже суммарный размер, необходимый под весь набор типов записи, переданный как std::vector.
Поле field реализуется аналогично типу object, по сути контейнер динамического содержимого. Большая часть типов: int64_t, bool, double — скаляры и хранятся по значению. Тип std::string также может храниться по значению, однако стоит учитывать то, что почти наверняка данные строки будут храниться в куче и выделяться динамически. Если хочется поддержать некий varchar определенной длины, то здесь, скорее всего, нужен будет свой тип copy_on_write с массивом символов фиксированной длины.
Различные типы полей аналогичны различным типам объектов, унаследованных от класса object. Можно даже не использовать enum, а завязаться напрямую на типы, но, как правило, разбор результата SQL-запроса влечет за собой десериализацию пакета байтов с данными, где все типы полей заранее известны, поэтому enum для удобства здесь никаких ограничений не влечет. Тем более что метапрограммирование — стезя не для слабонервных, и MPL и Boost.Fusion мы здесь рассматривать не будем.
Осталось затронуть последний важный аспект использования размещающего new — пул однотипных объектов в C++.
Пул однотипных объектов
Чтобы разобраться, нам потребуется понятное прикладное объяснение. Как насчет собственно выборки в результате SQL-запроса с пулом для записей? Это позволит оптимизировать массу выделений памяти для построения объектов записей выборки.
Где record::inplace по сути создает данные записи не в куче, а по заданному адресу.
Нам потребуется конструктор record с инициализацией и специальный деструктор, об этом далее. Данный вариант инициализации record делает невозможным использование его в предыдущем варианте, то есть в виде класса, содержащего лишь поле copy_on_write. Мы не сможем, спокойно понадеявшись на динамическое выделение данных в куче, ворочать записями как хотим. С другой стороны, мы получаем сумасшедший прирост производительности при большом наборе данных. Однако есть в размещающем new подвох, о котором следует знать.
Явный вызов деструктора
WARNING
Если кто-то имеет привычку не дочитывать до конца либо читать по диагонали — очень зря. Пропустив этот важный раздел, можно наплодить memory leak’ов — утечек памяти, причем в больших количествах.
Есть еще одно «но» при использовании размещающего new — придется вызывать деструктор самим, вручную, поскольку delete не сделает ровным счетом ничего. Поэтому класс, содержащий данные, выделяющиеся в заранее подготовленную память, должен в деструкторе явно вызвать деструктор созданного в памяти класса. Так, деструктор класса object::~object должен явно вызвать деструктор object::data::~data, а деструктор record::data::~data должен будет позвать целый ряд деструкторов field::~field — по одному на каждое поле. Для того чтобы наглядно показать, как это должно происходить, я более детально распишу класс object.
Поскольку деструктор у класса данных должен быть описан как virtual, то и деинициализация данных пройдет успешно, какой бы наследник object::data ни использовался.
Также нужно переопределить конструктор и оператор копирования, как и перемещения, поскольку в отличие от случая с copy_on_write, где нас устраивал автогенерируемый конструктор, здесь каждый объект смотрит на свою область данных простым указателем. Поэтому поправим поведение по умолчанию:
Здесь наш новый метод desctuct_data() так и просится в деструктор object::~object. Раз просится, значит, там ему самое место. Для конструктора и оператора перемещения поведение похожее:
Итак, опасность memory leak’ов ликвидирована. Пользователи твоего API могут разрабатывать спокойно.
Размещающий new против new в куче
Как ты уже успел заметить, классы, использующие размещающий new, намного сложнее в реализации. Каждый аспект использования класса, реализованного на технике размещения объекта в подготовленную память, должен всесторонне тестироваться. Сложность же обычного new любого класса, как правило, сводится к обертке умного указателя. В чем же тогда выгода, если даже эмуляция динамической типизации усложняется явным указанием максимального размера типа данных?
Размещающий new в стандартной библиотеке
Помнишь, я обещал тебе рассказать про размещающий new в std::vector в начале статьи? Так вот, все конструкторы элементов в std::vector вызываются в подготовленной памяти. И так же активно для элементов вызываются деструкторы. Это не принципиально для векторов от простых POD-типов вроде int или char, но если мы хотим выделить std::vector, причем custom обладает нетривиальным и тяжелым конструктором по умолчанию и не менее тяжелым конструктором копирования, то мы получим массу неприятностей, если не будем знать, как работает std::vector со своими данными.
Итак, что же происходит, когда мы просим вектор изменить размер? Для начала вектор смотрит, что еще не зарезервировал нужное число байтов (буфер вектор всегда выделяет с запасом), после чего выделяет новый буфер. Все существующие элементы переносятся в новый буфер конструктором перемещения через размещающий new по соответствующему адресу. В результате все элементы стоят на своих местах. После чего вектор добирает нужное число элементов в конец массива, создавая каждый размещающим new и конструктором по умолчанию. Так же и в обратную сторону — уменьшение количества элементов вызовет деструкторы «вручную» при удалении элементов.
В отличие от std::vector, контейнер std::string не занимается placement new просто потому, что хранит всегда char, не нуждающийся в конструкторах или деструкторах. Зато целый ряд контейнеров стандартной библиотеки: deque, list, map и другие шаблоны классов для хранения произвольных данных — активно используют размещающий new в своей реализации.
Не нужно думать о размещающем new как о чем-то сродни хаку, это полноценная функция языка, позволяющая инициализировать объект конструктором по указанной памяти. Эта операция аналогична старому трюку языка си, когда выделенный блок байтов объявлялся указателем на некий тип (обычно структуру) и далее работа с этим блоком памяти велась через API этого типа.
Что в итоге?
Конечно, умение пользоваться размещающим new там, где надо, и только тогда, когда это действительно нужно, эффективно и оправданно, приходит не сразу. Одни до последнего отбиваются вредом предварительной оптимизации, другие, наоборот, только прочитав статью, бросятся встраивать new(m_buffer), где надо и где не надо. Со временем и те и другие приходят к золотой середине.
Суть метода проста — если есть возможность и необходимость разместить объект класса в заранее приготовленную память, сделать это относительно просто, если помнить пару несложных правил:
- память должна жить все время, пока в ней живет объект, если память потрут, то объект начнет ссылаться на битый сегмент памяти;
- деструктор класса для объекта, выделенного размещающим new, должен быть вызван вручную, это печально, но delete не делает с памятью по указателю ровным счетом ничего.
среда, ноября 23, 2005
Освобождение памяти, выделенной под vector
Некоторое время назад я писала про выделение памяти под vector и в конце немного затронула тему высвобождения памяти. В большинстве реализаций освободить память, выделенную под vector, можно только с помощью трюка, известного как swap trick. Недавно я вычитала, что в реализации STL из Visual C++ 7.1 память, выделенная под вектор, высвобождается при вызове метода clear(). 7.1 у меня нет, зато у меня есть Microsoft Visual C++ Toolkit 2003. Действительно, освобождается. Для такого кода:
vector v;
v.reserve(17);
coutv.clear();
cout
Вывод получается такой:
Microsoft Visual C++ Toolkit 2003:
17
0 //действительно, освободилась
MSVC++6.0:
17
17
MinGW gcc 3.4.4:
17
17
Я решила копать дальше. Я всегда считала, что метод clear для последовательных контейнеров эквивалентен erase всего контейнера. Нашла упоминание об этом в документации STL на sgi.com. Вот оттуда выдержка:
a.clear() Equivalent to a.erase(a.begin(), a.end())
Запускаю код:
vector v;
v.reserve(17);
coutv.erase(v.begin(), v.end());
cout
Получаю:
Microsoft Visual C++ Toolkit 2003:
17
17 //не освободилась
MSVC++6.0:
17
17
MinGW gcc 3.4.4:
17
17
Получается, что в случае вышеупомянутого тулкита нет обещанной эквивалентности. Так, а что говорит об этом Стандарт? Вот тут интересный момент. Там нет слова "эквивалентно". Там erase(begin(), end()) приписано к clear() в качестве assertion/note для последовательных контейнеров. А вот требование там одно, что post condition: size()==0. Оно тут выполняется.
Так что MSVC++2003 тут прав.
Я пробовала также v.resize(0). Пробовала удалить все элементы вектора с помощью pop_back'ов. Память не освобождается. Это происходит только при вызове clear.
7 коммент.:
For doing this portably, you need to write your own allocator. :-(
Я бы использовал тут слово "стандартно" вместо "правильно", потому как правильно было б не освобождать память. Освобождение исключает возможность повторного использования уже выделенной памяти
size() и capacity() - не есть одно и то же. Память изначально выделяется с некоторым запасом, чтобы при добавлении элементов в вектор не было постоянной переаллокации (ибо это тормоза, чем дальше - тем больше). Так что, capacity() говорит о размере пула памяти, выделеной данному вектору, а вот size() - это кол-во элементов, которые реально используются из этого пула.
Когда capacity() == size(), вставка очередного элемента вызовет
1) аллокацию нового пула (скорее всего, вдвое большего, чем раньше)
2) копирование содержимого старого пула в новый
3) освобождение старого пула
Если Вам не нравится такое поведение - смотрите на другие контейнеры, например, лист (двусвязный список).
2Анонимный:
size() и capacity() - не есть одно и то же.
1) аллокацию нового пула (скорее всего, вдвое большего, чем раньше)
Если Вам не нравится такое поведение
Речь идет об освобождении памяти, выделенной под вектор, а не о выборе контейнеров.
Уменьшение размеров вектора не уменьшает его его емкости. Чтобы вернуть память системе, нужно сделать следующий трюк:
vector tmp = v;
v.swap(tmp);
Ни .clear(), ни reserve(x) у меня память не освобождают.
Освободить память удаётся только так: std::vector().swap(vec);
Просит удаления неиспользуемых мощностей.
Это необязательный просьбой сократить capacity в size. Это зависит от реализации, если запрос выполняется.
void shrink_to_fit(); // (начиная с C++11)
Пример:
std::vector v;
v.clear(); // очищает содержимое вектора .
v.shrink_to_fit(); // сокращает зарезервированный размер памяти вектора .
Deleaker ищет утечки ресурсов в программах на С++: утечки памяти, дескрипторов, GDI объектов и многие другие.Работает как расширение в Visual Studio, так и в виде отдельного приложения.
Читайте также: