|
Программирование >> Включение нужных заголовков
2. Копирование всех элементов из старой памяти контейнера в новую память. 3. Уничтожение объектов в старой памяти. 4. Освобождение старой памяти. При таком количестве операций не приходится удивляться тому, что динамическое увеличение контейнера порой обходится довольно дорого. Естественно, эту операцию хотелось бы выполнять как можно реже. А если это еще не кажется естественным, вспомните, что при каждом выполнении перечисленных операций все итераторы, указатели и ссылки на содержимое vector или string становятся недействительными. Таким образом, простая вставка элемента в vector/string может потребовать обновления других структур данных, содержащих итераторы, указатели и ссылки расширяемого контейнера. Функция reserve позволяет свести к минимуму количество дополнительных перераспределений памяти и избежать затрат на обновление недействительных итераторов/указателей/ссылок. Но прежде чем объяснять, как это происходит, позвольте напомнить о существовании четырех взаимосвязанных функций, которые иногда HjrraroT друг с другом. Из всех стандартных контейнеров перечисленные функции поддерживаются только контейнерами vector и string. Функция sizeO возвращает текущее количество элементов в контейнере. Она не сообщает, сколько памяти контейнер выделил для хранящихся в нем элементов. Функция capacity О сообщает, сколько элементов поместится в выделенной памяти. Речь идет об общем количестве элементов, а не о том, сколько еще элементов можно разместить без расширения контейнера. Если вас интересует объем свободной памяти vector или string, вычтите size О из capacityO. Если sizeO и capacityO возвращают одинаковые значения, значит, в контейнере не осталось свободного места, и следующая вставка (insert, push back и т. д.) вызовет процедуру перераспределения памяти, описанную выше. Функция resize(size t п) изменяет количество элементов, хранящихся в контейнере. После вызова resize функция size вернет значение п. Если п меньше текущего размера, лишние элементы в конце контейнера уничтожаются. Если п больше текущего размера, в конец контейнера добавляются новые элементы, созданные конструктором по умолчанию. Если п больше текущей емкости контейнера, перед созданием новых элементов происходит перераспределение памяти. Функция reserve(size t п) устанавливает минимальную емкость контейнера равной п - при условии, что п не меньше текущего размера. Обычно это приводит к перераспределению памяти вследствие увеличения емкости (если п меньше текущей емкости, vector игнорирует вызов функции и не делает ничего, а stri ng может уменьшить емкость до большей из величин (size(), п)), но размер string при этом заведомо не изменяется. По собственному опыту знаю, что усечение емкости string вызовом reserve обычно менее надежно, чем фокус с перестановкой , описанный в совете 17. Из краткого описания функций становится ясно, что перераспределение (выделение и освобождение блоков памяти, копирование и уничтожение объектов, обновление недействительных итераторов, указателей и ссылок) происходит каждый раз, когда при вставке нового элемента текущая емкость контейнера оказывается недостаточной. Таким образом, для предотвращения лишних затрат следует установить достаточно большую емкость контейнера функцией reserve, причем сделать это нужно как можно раньше - желательно сразу же после конструирования контейнера. Предположим, вы хотите создать vector<int> с числами из интервала 1-1000. Без использования reserve это делалось бы примерно так: vector<int> v; for (int i=l: i<=1000: ++i) v.push back(i): В большинстве реализаций STL при выполнении этого фрагмента произойдет от 2 до 10 расширений контейнера. Кстати, число 10 объясняется очень просто. Вспомните, что при каждом перераспределении емкость vector обычно увеличивается вдвое, а 1000 примерно равно 2°. vector<int> V: v.reserve(lOOO): for (int i=l: i<=1000: ++i) v.push back(i); В этом случае количество расширений будет равно нулю. Взаимосвязь между size и capacity позволяет узнать, когда вставка в vector или string приведет к расширению контейнера. В свою очередь, это позволяет предсказать, когда вставка приведет к недействительности итераторов, указателей и ссылок в контейнере. Пример: string s: if (s.sizeO < s.capacityO) { s.push back(x); В этом фрагменте вызов pusli baclc не может привести к появлению недействительных итераторов, указателей и ссылок, поскольку емкость string заведомо больше текущего размера. Если бы вместо pusli back выполнялась вставка в произвольной позиции строки функцией insert, это также гарантировало бы отсутствие перераспределений памяти, но в соответствии с обычными правилами действительности итераторов для вставки в string все итераторы/указатели/ссылки от точки вставки до конца строки стали бы недействительными. Вернемся к основной теме настоящего совета. Существуют два основных способа применения функции reserve для предотвращения нежелательного перераспределения памяти. Первый способ используется в ситуации, когда известно точное или приблизительное количество элементов в контейнере. В этом случае, как в приведенном выше примере с vector, нужный объем памяти просто резервируется заранее. Во втором варианте функция reserve резервирует максимальный объем памяти, который может понадобиться, а затем после включения данных в контейнер вся свободная память освобождается. В усечении свободной памяти нет ниче- го сложного, однако я не буду описывать эту операцию здесь, потому что в ней используется особый прием, рассмотренный в совете 17. Совет 15. Помните о различиях в реализации string Бьерн Страуструп однажды написал статью с интригующим названием Sixteen Ways to Stack a Cat [27], в которой были представлены разные варианты реализации стеков. Оказывается, по количеству возможных реализаций контейнеры string не уступают стекам. Конечно, нам, опытным и квалифицированным программистам, положено презирать подробности реализации , но если Эйнштейн был прав, и Бог действительно проявляется в мелочах... Даже если подробности действительно несущественны, в них все же желательно разбираться. Только тогда можно быть полностью уверенным в том, что они действительно несущественны. Например, сколько памяти занимает объект string? Иначе говоря, чему равен результат sizeof (string)? Ответ на этот вопрос может быть весьма важным, особенно если вы внимательно следите за расходами памяти и думаете о замене низкоуровневого указателя char* объектом string. Оказывается, результат sizeof (string) неоднозначен - и если вы действительно следите за расходами памяти, вряд ли этот ответ вас устроит. Хотя у некоторых реализаций контейнер string по размеру совпадает с char*, так же часто встречаются реализации, у которой string занимает в семь раз больше памяти. Чем объясняются подобные различия? Чтобы понять это, необходимо знать, какие данные и каким образом будут храниться в объекте string. Практически каждая реализация string хранит следующую информацию: размер строки, то есть количество символов; емкость блока памяти, содержащего символы строки (различия между размером и емкостью описаны в совете 14); содержимое строки, то есть символы, непосредственно входящие в строку. Кроме того, в контейнере string может храниться: копия распределителя памяти. В совете 10 рассказано, почему это поле не является обязательным. Там же описаны странные правила, по которым работают распределители памяти. Реализации string, основанные на подсчете ссылок, также содержат: счетчик ссылок для текущего содержимого. В разных реализациях string эти данные хранятся по-разному. Для наглядности мы рассмотрим структуры данных, используемые в четырех вариантах реализации stri ng. В выборе нет ничего особенного, все варианты позаимствованы из широко распространенных реализаций STL. Просто они оказались первыми, попавшимися мне на глаза. В реализации А каждый объект string содержит копию своего распределителя памяти, размер строки, ее емкость и указатель на динамически выделенный буфер
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |