W dużym uproszczeniu vector można napisać tak:
template < typename T >
class vector
{
public:
private:
T * Array;
int Size;
int Capacity;
};
jak widzisz jest to w zasadzie zwykła tablica, teraz jak mogłaby wyglądać funckja push_back
template < typename T >
void vector < T > push_back( T & arg )
{
if( Size == Capacity )
{
T * OldArray;
copy( Array, OldArray );
delete[] Array;
Capacity *= 2;
Array = new T[ Capacity ];
copy( OldArray, Array );
}
Array[ Size++ ] = arg;
}
widzisz tutaj, że jeżeli osiągniemy limit elementów w tablicy to zostanie ona rozszerzona co wiąże się z zrobieniem kopii oryginalnej tablicy, skasowaniem jej, zaalokowanej nowej, większej tablicy i przekopiowaniem elementów do nowej tablicy, na końcu jest zapisywany argument do kontenera. Stąd prosty wniosek, że dodawanie elementów do vectora wiąże się z (potencjalnie) częstym kopiowaniem elementów vectora (w tym uproszczonym przykładzie pomijam przenoszenie).
Teraz wyobraź sobie, że mamy taką klasę którą chcemy umieścić w vectorze
class MyClass
{
private:
int a, b, c, d, e, f, g;
double p1, p2, p3, p4, p5, p6, p7, p8;
std::string s1, s2, s3, s4, s5, s6, s7, s8;
SomeBigStruct ss1, ss2, ss3, ss4, ss5;
};
jak widzisz jest to klasa z masą różnych składowych, w tym z kilkoma stringami (które same w sobie są tablicą charów). Jak pewnie możesz sobie wyobrazić kopiowanie takiej klasy jest dość kosztowne dlatego chcielibyśmy tego uniknąć, jeżeli jednak będziesz trzymał taką klasę w vectorze przez wartość to nie unikniesz tego kopiowania i co kilka wstawień nowego elementu będziesz miał kilkanaście wywołań konstruktora kopiującego. Dlatego lepiej taką klasę trzymać w vectorze korzystając ze wskaźników. Wskaźniki to nic innego jak liczby, kopiowanie liczb jest szybkie, na pewno dużo szybsze od kopiowania takiej klasy. Kopiując wskaźniki nie tracisz aderu obiektu na który one wskazują bo ten adres się nie zmienia, nie przenosisz nigdzie obiektu wcześniej zaalokowanego, on zawsze jest w tym samym miejscu. Dlatego właśnie często używa się vectora wskaźników na obiekty, żeby pozbyć się tego kosztownego kopiowania, zresztą niektórych klas nie da się kopiować albo chcemy mieć pełną kontrolę nad tym kiedy jest wykonywana kopia, wtedy wskaźniki są jedynym rozwiązaniem.
Teraz jeżeli chodzi o zwalnianie pamięci. Vector nie wie jakie dane trzyma, nie wie czy ma wskąźniki na obiekty (i czy wskaźnik na coś faktycznie pokazuje), nie wie czy trzyma obiekty przez wartość, nie wie czy trzyma klasę, czy strukturę czy typ wbudowany, to jest szablon i musi być maksymalnie elastyczny. Z tego powodu vector sam nic nie zwalnia, oczywiście nie licząc swojej wewnętrznej tablicy, dlatego jeżeli trzymasz obiekty w vectorze przez wartość to w destruktorze vectora, przy kasowaniu wew. tablicy automatycznie wywoływane są destruktory obiektów T, jeżeli trzymamy tam wskaźniki to jedyne co się dzieje to usunięcie tych wskaźników (ale nie tego na co wskazują!), dlatego w takim wypadku trzeba samemu zadbać o zwolnienie pamięci. W tym miejscu warto też powiedzieć o smart pointerach które załatwią to za nas (smart pointer w swoim destruktorze usuwa obiekt na który wskazuje).
Dlatego lepiej pisać
std::vector < std::unique_ptr < MyClass >> vec;
vec.push_back( std::make_unique < MyClass >() );
i tyle, destruktor vectora wywoła destruktor unique_ptr a ten z kolei wywoła delete które usunie dynamicznie zaalokowany obiekt klasy MyClass.
I na koniec, nie
std::vector < int >
to nie jest vector wskaźników na int i nie dodajemy do niego elementów poprzez new
std::vector < int > vec1;
vec1.push_back( 5 );
std::vector < int *> vec2;
vec2.push_back( new int( 10 ) );
w pierwszym przykładzie nie musimy martwić się o pamięć, wszystko jest zarządzane automatycznie, nic dynamicznie nie alokowaliśmy, vector trzyma int przez wartość i przy kopiowaniu będzie kopiowana liczba 5. W drugim przykładzie musimy martwić się o pamięć bo sami ją dynamicznie zaalokowaliśmy, więc musimy ją też zwolnić (destruktor vectora tego nie zrobi automatycznie), vector trzyma adresy intów i przy kopiowaniu te adresy będą kopiowane, czyli np.
new int(10) mogło stworzyć liczbę 10 w pamięci i zwrócić jej adres 0x231af15cd351, wtedy ten adres (a nie wartość liczby) będzie kopiowany.