Векторы, используемые в С++ в качестве массивов, в очень старых компиляторах не поддерживаются. Поэтому чтобы изучать те векторы, которые используют в С++, нужно использовать компиляторы поддерживающие их. Вы легко можете понять, поддерживает или нет ваш компилятор векторы С++, просто попробовав выполнить следующую программу:
C++
1
2
3
4
5
6
7
#include <vector>
usingnamespacestd;//Используем пространство имён стандартной библиотеки шаблонов (standart library template)
intmain(){
vector<int>v;//Объявили объект-вектор
}
Строчку using namespace std; в старых компиляторах написать нельзя, потому что из-за неё код не будет компилироваться, но во всех распространённых ныне компиляторах она пишется почти всеми новичками.
В отличие от геометрического вектора у вектора из языка С++ другое назначение — это динамический массив.
Динамический массив — это такой массив, который может и расширать и сужать свою фактическую вместимость. Обычный массив всегда имеет фиксированную вместимость, изменить которую по ходу программы нельзя. Из-за того что по ходу программы вместимость обычного массива изменить нельзя, мы даже не можем использовать вот такой код:
C++
1
2
3
4
intmain(){
intN=10;
intarr[N];
}
В некоторых компиляторах показанный код будет компилироваться, но это благодаря расширениям компиляторов, но не по правилам языка. Да и изменить вместимость в ходе работы программы всё равно не получится:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
intmain(){
intN=10;
intarr[N];
N=10000;
arr[N];//что-то вроде попытки переиначит вместимость массиву arr
for(inti=0;i<N;i++){//выход за границу массива, arr вмещает только 10 элементов
arr[i]=i;
}
std::cin.get();
}
Обычному массиву нужно объяснять вместимость константой времени компиляции. Если значение константы определяется во время выполнения программы, то нельзя:
C++
1
2
3
4
5
6
7
8
voidfoo(){
constintN=100;
intarr[N];
}
intmain(){
}
И много всяких ограничений мы можем встретить с обычным массивом ещё. Эта тема всё-таки о векторе, поэтому описание обычного массива прекращаю. Возвращаюсь к вектору. В отличие от обычного массива объекты вектора умеют влиять на память: расширять при необходимости или наоборот: сужать. Но как и с обычным массивом мы можем ненароком нарушить существующие границы. Всё-таки вектор прежде всего массив и актуальный размер его ограничивается, и если мы выйдем за допустимую норму до того момента как векторв будет расширен, то мы нарушим границу, котоую нарушать нельзя.
Векторы являются частью STL и относятся к последовательным контейнерам.
Последовательные контейнеры — это упорядоченные коллекции, в которых каждый элемент занимает определенную позицию.
Поскольку вектор прежде всего массив (массив с расширенной функциональностью), то с объектами-векторами можно работать как с обычным массивом. Вектор всегда может вычислить свою действительную вместимость с помощью функции size(). В разные моменты времени вместимость может изменяться (о чём говорит определение вектора как динамического массива) и всегда можно узнать последнюю актуальную.
Чтобы создать вектор с определённо выбранной вместимостью, чтобы было похоже на создание массива, когда мы знаем, сколько элементов нам сразу надо, можно использовать вот такой код:
C++
1
2
3
4
5
6
7
8
9
//Листинг #1 Объявление вектора
#include <vector> //всегда подключается, если используется вектор
usingstd::vector;//можно использовать более уточнённую форму нежели namespace std
intmain(){
vector<int>v(100);//Создали вектор, определили ему ёмкость в 100 элементов
vector<double>v;//Создали пустой вектор, определили ему ёмкость в 0 элементов
}
Чтобы было наглядно очевидное сходство векторов и обычных массивов, используем обе формы масивов в одном коде:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Листинг #2 Сходство вектора и массива при написании кода
#include <vector> //Всегда подключается, когда используется вектор
#include <iostream>
usingnamespacestd;//Используем пространство имен std
intmain(void)
{
vector<int>v(10);//Объявили вектор в десять элементов типа int
for(inti=0;i<10;i++)v[i]=i+1;//Записали в вектор числа 1,2,3...10
for(inti=0;i<10;i++)cout<<v[i]<<" ";//Вывод всех элементов вектора на экран
cout<<endl;
intarr[10];//Объявили массив в десять элементов типа int
for(inti=0;i<10;i++)arr[i]=i+1;//Записали в массив числа 1,2,3...10
for(inti=0;i<10;i++)cout<<arr[i]<<" ";//Вывели массив на экран
cout<<endl;
cin.get();
}
В отличие от обычных массивов векторы обладают свойством дополнительной функциональности. Поскольку векторы — это объекты класса, а в том классе описаны специальные функции для объектов, то мы можем использовать те описанные функции для всех объектов-векторов.
Часть основных функций объектов-векторов:
push_back(element) — добавить элемент в конец объект-вектора
pop_back(element) — удалить последний элемент объект-вектора
sizeof() — Число элементов в векторе
capacity() — Вместимость вектора
insert(…) — Существует три варианта вставки в какую-либо область в объект-векторе. Первый аргумент — позиция вставки, заданная итератором, остальные указывают на контейнер, или количество и контейнер, или пару итераторов, указывающих от какой и до какой позиции из другого контейнера взять данные.
erase(iterator или iterator от, и до) — Удаляет элемент или последовательность элементов из объект-вектора.
begin() — Возвращает итератор, указывающий на начало коллекции.
end() — Возвращает итератор, указывающий на конец коллекции. При этом он указывает не самый последний элемент, а на "воображаемый элемент за последним.
at(index) — Метод доступа к элементам коллекции, в отличие от операции [ ] проверяет выход из-за границ коллекции, и в случае чего генерит исключение.
clear() — Удаляет все элементы коллекции, при этом если в объект-векторе содержатся объекты классов, вызывает их деструкторы. А если же содержатся указатели на объекты, то может произойти утечка памяти (memory leaks=), так как delete, если будет нужен, ничто и никто не зазовёт.
reserve(число) — Резервирует определённый объём памяти под указанную вместимость
resize(число) — Изменяет актуальный размер вектора под указанное число элементов.
shrink_to_fit() — Выравнивает фактический размер и зарезервированный размер памти, путём усечения резервных ячеек.(С++11 и новее)
Моё знакомство с векторами началось с методов push_back и pop_back. Моя первая ошибка демонстрируется в листинге #b1
C++
1
2
3
4
5
6
7
8
9
10
11
12
//Листинг #b1 Смысловая (семантическая) ошибка в коде, предполагалось заполнить первые 10 значений вектора значениями по порядку
#include <vector>
#include <iostream>
usingnamespacestd;
intmain()
{
vector<int>v(10);//Объявили вектор в десять элементов
for(inti=0;i<10;i++)v.push_back(i);//Добавили к вектору десять чисел 1,2,3...10
for(inti=0;i<10;i++)cout<<v[i]<<" ";//Вывели первые десять чисел вектора на экран
}
Когда я писал такой код, то не совсем понимал метод push_back, поэтому на экран получил нули и стал искать в чём дело. Возможно кто-то из читателей мог как и я невнимательно прочитать, что это за метод такой push_back. Это метод для добавления элементов в конец коллекции. Элементы приставляются в хвост массива, удлинняя сам массив. Другими словами, эта моя программа увеличила размер массива путем добавления новых к нему элементов, и чтобы увидеть весь массив, нужно этот массив выводить с правильным размером:
C++
1
2
3
4
5
6
7
8
9
10
11
12
//Листинг #b2
#include <vector> //подключаем директиву vector
#include <iostream>
usingnamespacestd;//для работы с вектором нужно пространство имен std
intmain(void)
{
vector<int>v(10);//объявили вектор в десять элементов
for(inti=0;i<10;i++)v.push_back(i);//к существующим десяти элементам дописали новые элементы
for(inti=0;i<v.size();i++)cout<<v[i]<<" ";//Выводим весь вектор, указывая его размер
}
Использовать метод size всяко удобнее, чем указывать размер цифрами.
Одним из первых заданий, с которым, как я понимаю, может столкнуться обучающийся на стадии изучения векторов — это копирование значений обычного массива в объект-вектор. Можно использовать разные способы, такие как поэлементное копирование с помощью цикла (как копирование обычных массивов), копирование с помощью memcpy (такое копирование не приветствуется, лучше memmove) и можно использовать поэлементное заполнение, этот код я сейчас покажу, а также тучу всяких способов, которые я пока не покажу.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
//Листинг #c1
#include <vector> //подключили дирекиву vector
#include <iostream>
usingnamespacestd;
intmain(void)
{
intArr[10]={1,2,3,4,5,6,7,8,9,10};//объявляем и инициализируем обычный массив
vector<int>v(Arr,Arr+10);//копируем десять элментов массива Arr в вектор v
for(inti=0;i<v.size();i++)cout<<v[i]<<" ";//Выводим поэлементно все элементы вектора на экран
}
В отличие от обычных массивов, где мы или используем прямое указане номера позиции или двигаем указатель, по элементам векторов можно ходить с помощью так называемых итераторов.
Итератор — это специальный элемент, подобный указателю, предназначенный для навигации по элементам коллекции.
Итераторы — это не указатели как таковые, а объекты обобщённых классов, выполняющие роль указательных переменных, подходящих обобщённому классу.
Понятие итератора очень важно в начале изучения STL. Первое, что вы должны принять, это то, что итераторы дают нам доступ к эллементам коллекций (коллекции — это структуры данных, умеющие хранить множество элементов, кроме массива существуют и другие структуры). Любой контейнер стандартной библиотеки шаблонов (а вектор — это контейнер такой библиотеки) всегда содержит методы begin() и end(), по которым всегда можно отследить начало расположения объекта-вектора в памяти и конец. Эти методы возвращают соответствующие указательные переменные на адреса. И эти адреса можно использовать, используя итераторы в качестве бегунков.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Листинг #c2 Использование итератора для обхода вектора
vector<int>v(Arr,Arr+10);//Копирование массива в Вектор
vector<int>::iterator it;//Объявляем итератор (тип итератора должен соответствовать типу объекта, который обходится)
for(it=v.begin();it!=v.end();it++)cout<<*it<<" ";//с помощью итератора выводим элементы вектора на экран
cout<<'\n';
cin.get();
}
Если у вектора известны границы (во избежание выхода в память в неразрешённую зону вектора), можно использовать специальные функции, которые избавят нас от необходимости своими силами гонять итераторы по коллекции:
vector<int>v(Arr,Arr+10);//Копирование массива в Вектор
vector<int>::iterator it;//Объявляем итератор
for(it=v.begin();it!=v.end();it++)cout<<*it<<'\t';//с помощью итератора выводим элементы вектора на экран
cout<<'\n';
it=v.begin();//установили итератор в начало вектора
advance(it,3);//загнали итератор на три позиции вправо (0 + 3 = 3, v[3] == 4)
cout<<"element from vector: "<<*it<<'\n';//посмотрели значение третьего элемента вектора
*it=122;//изменили значение текущего элемента, на который направлен итератор
for(it=v.begin();it!=v.end();it++)cout<<*it<<'\t';//с помощью итератора выводим элементы вектора на экран
cout<<'\n';
cin.get();
}
Мне очень удобно называть итераторы бегунками. Но это моё личное название, в технической терминологии такого нет.
Со времён принятия стандарта С++11 стало возможно инициализировать вектор списковой инициализацией. Это такая инициализация, которая напоминает инициализацию обычных массивов с помощью фигурных скобок:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
//Требуется поддержка стандарта С++11 и любого новее
//Листинг #e1
#include <vector>
usingnamespacestd;
intmain()
{
intarr[5]={1,2,3,4,5};//Обычный массив можно инициализироват так в любом компиляторе
vector<int>v={1,2,3,4,5};//Объявили пустой вектор и инициализировали его массивом, описанным в фигурных скобках
//это списковая инициализация, требуется С++11 или новее
}
Объект-вектор может по мере необходимости при добавлении очередного в себя элемента расширять себе используемую память автоматически, но никогда своими силами не сужает излишки, появление которых возможно по ходу работы программы после удалений. Удаляемые элементы оставляют после себя использованную собой память в резерве объекта. Связано это с тем, что очистка памяти наряду с выделением — операции дорогостоящие, т. е. времязатратные, поэтому в угоду эффективности остаётся резерв ячейки памяти за объектом. Так не придётся перевыделять и переуничтожать многократно, что благоприятно сказывается на скорости работы программы. Но при желании мы своими силами можем насильно подчистить память, оказавшуюся в резерве объекта-вектора.
Из-за динамичности вектора следует различать понятия ёмкости и размера вектора. Ёмкость вектора предполагает собой общую вместимость элементов, а размер вектора представляет собой количество элементов, расположившихся внутри вектора. (есть ещё второе понятие размера, размер типа vector, а не объекта вектора, не cпутайте, речь дальше о размере объекта вектора)
size() — функция объект-вектора, позволяющая узнать количество элементов, расположившехся внутри объекта-вектора.
capacity() — функция объект-вектора, позволяющая узнать, на сколько элементов заточен объём объект-вектора.
capacity() отображает общее число ячеек, которые используются и забронированы вектором, но не число ячеек, которые могут быть подвержены какому-нибудь влияюнию модификаций. Если мы выйдем за разрешённые границы актуального размера, то программа будет вести себя непредсказуемо.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Листинг #e2
#include <vector>
#include <iostream>
usingnamespacestd;
intmain()
{
intarr[5]={1,2,3,4,5};//Обычный массив можно инициализироват так в любом компиляторе
v.reserve(200);//забронировали вектору 200 элементов
for(inti=0;i<200;i++)v[i]=i;//Непредсказуемое поведение, актуальный размер можно узнать по size(), выход за этот предел в таком виде неправильно
cout<<v.size()<<'\n';//скорее всего значение меньше чем 200
cin.get();
}
Если мы бронируем ячейки под объекты-вектора, но используем их до того момента, как массив уширился, то мы используем сырую память, из-за которой объекты, так сказать, недоготавливаются, если у объектов есть описанные конструкторы. Но эта внутрення кухня вектора не предполагается сейчас для описания совсем новичку. Не будем углубляться. Просто пока что достаточно знать, что выход за актуальный размер объект-вектора и попытка повлиять на ячейку ведёт к непрогнозируемости работы программы.
Для того, чтобы расширить объект-вектор вручную, нужно использовать метод resize(). Метод resize() легко расширит фактическую вместимость вектора, но не сможет убрать излишки резерва.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Листинг #f1
#include <vector>
#include <iostream>
usingnamespacestd;
intmain()
{
vector<int>v;
v.reserve(100);
v.resize(200);//Резерв увеличился
v.resize(55);//Резерв остался
cout<<"v.size() == "<<v.size()<<'\n';
cout<<"v.capacity() == "<<v.capacity()<<'\n';
cin.get();
}
Чтобы убрать излишки резерва, нужно использовать метод shrink_to_fit(), если ваш компилятор поддерживает С++11:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Листинг #f2 Убираем излишки резерва, только С++11 и старше
#include <vector>
#include <iostream>
usingnamespacestd;
intmain()
{
vector<int>v;
v.reserve(100);
v.resize(200);//Резерв увеличился
v.resize(55);//Резерв остался
v.shrink_to_fit();
cout<<"v.size() == "<<v.size()<<'\n';
cout<<"v.capacity() == "<<v.capacity()<<'\n';
cin.get();
}
Поскольку на день написания статьи не все компиляторы поддерживают С++11, то для исполнения такой цели используют трюк, называемый Swap Trick (трюк обмена).
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Листинг #f3 Убираем излишки резерва, подход используемый раньше
Написанию части этой статьи сильно помогла статья из блога Алёны.
Для предотвращения лишних операций перераспределения памяти рекомендуется использовать reserve().
К особенностям работы с векторами можно добавить и то, что скорость работы векторов уступает скорости работе массивов, и то, что ёмкость вектора больше, чем объём занимаемой памяти обычным массивом. Не трудно понять, что это часто может быть действительно недостатком. Хотя на практике векторы бывают быстрее и эффективнее обычных массивов.
Вывести на экран вектор можно очень разнообразными способами, один из выглядит вот так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Листинг #g Вывод вектора на экран
#include <iostream>
#include <vector>
#include <iterator>
usingnamespacestd;
intmain()
{
vector<int>v;
v.push_back(1);
v.push_back(4);
v.push_back(7);
copy(v.begin(),v.end(),ostream_iterator<int>(cout," "));//Вот такой вот способ вывода на экран всех значений вектора
cin.get();
}
В листинге #g происходит следующее: cout представляет собой объект, т. е. обычную переменную определённого типа. Следовательно в эту переменную можно запихать данные. Чтобы запихать данные, нужно, чтобы тип данных совпадал с типом, могущимя храниться в объекте. cout является объектом класса ostream, вот и видим, что ostream_iterator, т. е. итератор для класса ostream, под объект, хранящий int значения (значения типа int хранит вектор, следовательно копировать надо значения с типом int). Так и происходит копирование значений в cout, после чего мы видим результат на экране.
Добавить комментарий