•Функция reserve(size_t n)
устанавливает минимальную емкость контейнера равной n
— при условии, что n
не меньше текущего размера. Обычно это приводит к перераспределению памяти вследствие увеличения емкости (если n
меньше текущей емкости, vector
игнорирует вызов функции и не делает ничего, а string
size, n
)), но размер string
при этом заведомо не изменяется. По собственному опыту знаю, что усечение емкости string
вызовом reserve
обычно менее надежно, чем «фокус с перестановкой», описанный в совете 17.
Из краткого описания функций становится ясно, что перераспределение (выделение и освобождение блоков памяти, копирование и уничтожение объектов, обновление недействительных итераторов, указателей и ссылок) происходит каждый раз, когда при вставке нового элемента текущая емкость контейнера оказывается недостаточной. Таким образом, для предотвращения лишних затрат следует установить достаточно большую емкость контейнера функцией reserve
, причем сделать это нужно как можно раньше — желательно сразу же после конструирования контейнера.
Предположим, вы хотите создать vector
с числами из интервала 1–1000. Без использования reserve
это делалось бы примерно так:
vector
for (int i=1; i<=1000; ++i) v.push_back(i):
В большинстве реализаций STL при выполнении этого фрагмента произойдет от 2 до 10 расширений контейнера. Кстати, число 10 объясняется очень просто. Вспомните, что при каждом перераспределении емкость vector
обычно увеличивается вдвое, а 1000
примерно равно 210.
vector
v.reserve(1000);
for (int i=1; i<=1000; ++i) v.push_back(i);
В этом случае количество расширений будет равно нулю.
Взаимосвязь между size
и capacity
позволяет узнать, когда вставка в vector
или string
приведет к расширению контейнера. В свою очередь, это позволяет предсказать, когда вставка приведет к недействительности итераторов, указателей и ссылок в контейнере. Пример:
string s;
if (s.size < s.capacity) {
s.push_back('x');
}
В этом фрагменте вызов push_back
не может привести к появлению недействительных итераторов, указателей и ссылок, поскольку емкость string
заведомо больше текущего размера. Если бы вместо push_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
.