Menu

vector w C++: dynamiczne tablice z std::vector

std::vector to tablica o zmiennym rozmiarze w C++ i kontener, po który warto sięgać domyślnie. Dowiedz się, jak tworzyć vector, odczytywać elementy, powiększać go i przechodzić po nim pętlą, a także poznaj pułapki unieważniania iteratorów i wychodzenia poza zakres.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Dlaczego vector zamiast surowej tablicy

Surowa tablica ma rozmiar ustalony na sztywno w czasie kompilacji i zapomina swoją długość w chwili, gdy przekażesz ją do funkcji. std::vector rozwiązuje oba problemy: to tablica o zmiennym rozmiarze, która śledzi własną długość, rośnie w razie potrzeby i sama sprząta swoją pamięć. We współczesnym C++ vector to kontener domyślny: po surową tablicę sięgaj tylko wtedy, gdy masz konkretny powód.

Dołącz <vector>, a potem zadeklaruj vector z typem elementów w nawiasach ostrych:

scores.size() zawsze podaje bieżącą długość: nie trzeba osobnego int n do synchronizowania ani sztuczek z sizeof. {90, 75, 100, 60} to inicjalizator klamrowy, a vector sam ustala, że potrzebuje czterech miejsc.

Tworzenie i inicjalizacja vectora

Jest kilka sposobów na zbudowanie vectora, zależnie od tego, co wiesz na starcie:

Uważaj na pułapkę nawiasów okrągłych i klamrowych: vector<int> tens(5, 10) tworzy pięć kopii 10, a vector<int> tens{5, 10} tworzy dwuelementowy vector z wartościami 5 i 10. Nawiasy okrągłe znaczą "rozmiar i wartość wypełnienia", a klamry "dokładnie te elementy".

Dodawanie i usuwanie elementów

Cały sens vectora polega na tym, że rośnie. push_back dopisuje element na końcu, a pop_back usuwa go z końca:

back() zwraca ostatni element, a front() pierwszy: to czytelniejsze niż v[v.size() - 1] i v[0]. Od C++11 możesz też użyć emplace_back(args...), żeby utworzyć element w miejscu, co przy cięższych typach pozwala uniknąć tymczasowej kopii.

Częsty błąd początkujących to wywołanie front() albo back() na pustym vectorze. To niezdefiniowane zachowanie, a nie błąd: zawsze najpierw zabezpiecz się warunkiem if (!v.empty()).

Odczyt elementów: [] a at()

Vector indeksujesz dokładnie jak tablicę, za pomocą []. Ale [] nie sprawdza zakresu: indeks spoza zakresu to niezdefiniowane zachowanie, które może po cichu odczytać śmieci albo wysypać program później, w mylącym miejscu:

vector<int> v = {1, 2, 3};
cout << v[10];   // NIEZDEFINIOWANE ZACHOWANIE: bez sprawdzenia, bez błędu

Gdy zależy ci na bezpieczeństwie, użyj at(). Sprawdza indeks i przy złym dostępie rzuca std::out_of_range, jeden z wyjątków biblioteki standardowej, które możesz złapać, więc dostajesz wyraźny błąd zamiast uszkodzonych danych:

Praktyczna zasada: używaj [] w ciasnych pętlach, gdzie poprawność indeksu jest już pewna, a at() na granicach, przez które może się prześlizgnąć złe wejście.

Pętla po vectorze

Najczystszy sposób przejścia po vectorze to pętla for po zakresie. Bierz elementy przez const auto&, żeby czytać bez kopiowania, albo przez auto&, żeby edytować je w miejscu:

Jeśli naprawdę potrzebujesz indeksu (na przykład do porównywania sąsiadów), użyj klasycznej pętli z licznikiem, ale pamiętaj, że size() zwraca typ bez znaku (size_t). Porównywanie z nim int i ze znakiem może wywołać ostrzeżenia kompilatora i zaskakujące zawijanie wartości, więc gdy tylko się da, wybieraj size_t i albo pętlę po zakresie:

for (size_t i = 0; i < v.size(); i++) {   // size_t, nie int
    cout << v[i];
}

size, capacity i reserve

Vector przechowuje dwie liczby: size() (ile elementów zawiera) i capacity() (ile może pomieścić, zanim będzie musiał urosnąć). Gdy push_back przekroczy pojemność, vector alokuje większy blok, kopiuje do niego wszystkie elementy i zwalnia stary blok. Dlatego wielokrotne push_back jest tanie w ujęciu zamortyzowanym, ale każda pojedyncza realokacja coś kosztuje:

Jeśli wiesz mniej więcej, ile elementów dodasz, najpierw wywołaj reserve(), żeby pominąć powtarzane realokacje. Zauważ, że reserve() zmienia pojemność, a nie rozmiar: vector nadal ma zero elementów, dopóki ich nie dodasz.

Ta realokacja jest też źródłem najpaskudniejszego błędu z vectorem. Ponieważ wzrost przenosi dane w inne miejsce, każdy zapisany wskaźnik, referencja lub iterator do vectora staje się wiszący po push_back, który wywołał realokację:

vector<int> v = {1, 2, 3};
int& first = v[0];     // referencja do elementu vectora
v.push_back(4);        // może realokować...
cout << first;         // WISZĄCA: może wskazywać na zwolnioną pamięć

To samo dotyczy iteratorów: nie wywołuj push_back ani erase podczas iteracji z zapisanym iteratorem. Jeśli musisz usuwać elementy w pętli, używaj wartości zwracanej przez erase albo idiomu erase-remove z std::remove.

Dalej: map

vector sprawdza się idealnie, gdy szukasz elementów według pozycji: element 0, element 1 i tak dalej. Często jednak chcesz szukać według klucza: nazwy użytkownika, identyfikatora produktu, słowa. Do tego służy std::map. Następnie omówimy map, kontener klucz-wartość w C++, w tym wstawianie, wyszukiwanie i iterowanie po wpisach oraz pułapkę "[] tworzy wartość domyślną", na której potyka się prawie każdy.

Najczęściej zadawane pytania

Czym jest vector w C++?

std::vector to dynamiczna tablica (o zmiennym rozmiarze) z biblioteki standardowej C++. W przeciwieństwie do surowej tablicy zna swój rozmiar, sama się powiększa, gdy dodajesz elementy przez push_back, i sama zwalnia pamięć. Dołącz <vector> i napisz vector<int> v;, żeby ją utworzyć.

Jaka jest różnica między [] a at() w vectorze w C++?

v[i] nie sprawdza zakresu: indeks spoza zakresu to niezdefiniowane zachowanie (awaria albo ciche uszkodzenie danych). v.at(i) sprawdza indeks i rzuca std::out_of_range, gdy jest niepoprawny. Używaj [] w gorących pętlach, gdzie indeks jest już sprawdzony, a at() wtedy, gdy chcesz bezpiecznego błędu, który łatwo zdebugować.

Czy push_back unieważnia wskaźniki i referencje do elementów vectora w C++?

Tak, potencjalnie. Gdy vector wyczerpie pojemność, push_back przenosi jego dane do nowego bloku pamięci, co unieważnia każdy wskaźnik, referencję i iterator do starych elementów. Nie trzymaj referencji do elementu przez push_back i, jeśli możesz, wywołaj najpierw reserve(), żeby uniknąć niespodziewanych realokacji.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ