cout << carr[100000] << '\n'; // Оп, кто знает, что дальше
// произойдет
vector<char> v;
v.push_back('a');
v.push_back('b');
v.push_back('c');
v.push_back('d');
v push_back('e');
try {
cout << v.at(10000) << "\n"; // at
проверяет границы и выбрасывает
} catch(out_of_range& е) { // out_of_range, если произошел выход за них
cerr << e.what << '\n';
}
}
Перехват
out_of_range
, определенного в
<stdexcept>
, позволяет грамотно справиться с неправильными индексами. А также можно вызвать метод
what
, позволяющий в зависимости от используемой реализации получить осмысленное сообщение об ошибке, как возвращаемая в коде примера 4.7:
invalid vector<T> subscript
Однако
vector
не является единственной возможностью. В C++ имеется большое количество способов хранить последовательности. Кроме
vector
имеются
list
,
set
и двунаправленные очереди (
deque
— double-ended queue). Все они поддерживают множество одинаковых операций, и каждый поддерживает свои собственные. Кроме того, каждый имеет различную алгоритмическую сложность, требования по хранению и семантику. Так что имеется богатый выбор.
Посмотрите внимательно на пример 4.6. Вы, вероятно, обратите внимание, что я изменяю значение строки
s
до того, как добавляю ее в конец контейнера с помощью
push_back
. Логично ожидать такого вывода этого примера
three
three
three
Я поместил в вектор одну и ту же строку три раза, так что каждый раз, когда я переприсваиваю строку, разве не должны все элементы вектора указывать на одну и ту же строку? Нет. Это важный момент, касающийся контейнеров STL.
Контейнеры STL сохраняют копии объектов, помещаемых в них, а не сами объекты. Так что после помещения в контейнер всех трех строк в памяти остается четыре строки: три копии, созданные и хранящиеся в контейнере, и одна копия, которой присваиваются значения.
Ну и что? Было создано несколько новых копий: большое дело. Но это действительно большое дело, так как если используется большое количество строк, за каждую копию приходится платить процессорным временем, памятью или и тем и другим. Копирование элементов в контейнерах — это намеренное поведение STL, и все контейнеры организованы именно так.
Одним из решений (определенно не единственным) является хранение в контейнере указателей. Но помните, что контейнер не удаляет с помощью
delete
указатели при его уничтожении. Память для указателей выделяет ваш код, так что он и должен ее очищать. Это относится и к ситуации, когда происходит полное удаление контейнера и когда удаляется только один его элемент.
В целях создания альтернативного решения давайте рассмотрим еще одну возможность. Рассмотрим шаблон класса
list
, определенный в
<list>
, который является двусвязным списком (doubly linked list). Если планируется большое количество вставок и удалений элементов в середине последовательности или если требуется гарантировать, что итераторы, указывающие на элементы последовательности, не станут недействительными при ее изменении, используйте
list
. Пример 4.8 вместо
vector
для хранения нескольких строк типа
string
использует
list
. Также он для перебора этих строк и печати вместо оператора индекса, как это делается в случае с простыми массивами, использует
for_each
.
Пример 4.8. Хранение
строк в списке
#include <string>
#include <list>
#include <algorithm>
#include <iostream>
using namespace std;
void write(const string& s) {
cout << s << '\n';
}
int main {
list<string> lst;
string s = "нож";
lst.push_front(s);
s = "вилка";
lst.push_back(s);
s = "ложка";
lst.push_back(s);
// У списка нет произвольного доступа, так что
// требуется использовать for_each
for_each(lst.begin, lst.end, write);
}
Целью этого отступления от первоначальной проблемы (хранения строк в виде последовательностей) является краткое введение в последовательности STL. Здесь невозможно дать полноценное описание этого вопроса. За обзором STL обратитесь к главе 10 книги C++ in a Nutshell Рэя Лишнера (Ray Lischner) (O'Reilly).
4.4. Получение длины строки
Проблема
Требуется узнать длину строки.
Решение
Используйте метод
length
класса
string
.
std::string s = "Raising Arizona";
int i = s.length;
Обсуждение
Получение длины строки — это тривиальная задача, но она является хорошей возможностью обсудить схему размещения
string
(как узких, так и широких символов).
string
, в отличие от массивов строк, завершаемых нулем, в С являются динамическими и увеличиваются по мере надобности. Большая часть реализаций стандартной библиотеки начинают с относительно низкой емкости и увеличивают ее в два раза каждый раз, когда достигается предел. Знание того, как анализировать этот рост, если и не точного алгоритма, помогает диагностировать проблемы производительности, связанные со строками.
Символы в
basic_string
хранятся в буфере, который является единым фрагментом памяти статического размера. Этот буфер, используемый строкой, изначально имеет некий размер, и по мере добавления в строку символов он заполняется до тех пор, пока не будет достигнут предел его емкости. Когда это происходит, буфер увеличивается. В частности, выделяется новый буфер большего размера, символы копируются из старого буфера в новый, и старый буфер удаляется.
Определить размер буфера (не число символов, в нем содержащихся, а его максимальный размер) можно с помощью метода
capacity
. Если требуется вручную установить емкость и избежать ненужных копирований буфера, используйте метод reserve и передайте ему числовой аргумент, указывающий требуемый размер буфера. Также имеется максимально возможный размер буфера, получить который можно с помощью вызова
max_size
. Это все можно использовать, чтобы посмотреть на расходование памяти в данной реализации стандартной библиотеки. Посмотрите на пример 4.9, показывающий, как это сделать.