Rząd wartości o stałym rozmiarze
Tablica to ciągły blok pamięci, który przechowuje stałą liczbę wartości tego samego typu. Pojedynczy int przechowuje jedną liczbę, a tablica int przechowuje ich wiele, jedna za drugą, a do każdej sięgasz przez indeks całkowity.
Deklarujesz ją, podając typ elementów, nazwę i rozmiar w nawiasach kwadratowych. Dodaj listę w klamrach, aby ją wypełnić:
Indeksowanie zaczyna się od zera: pierwszy element to scores[0], a tablica o rozmiarze 4 ma poprawne indeksy od 0 do 3. Rozmiar musi być stałą czasu kompilacji: w standardowym C++ nie możesz napisać int n = readInput(); int a[n]; (to nieprzenośne rozszerzenie). Gdy rozmiar ma być ustalany w czasie działania programu, sięgnij po vector.
Inicjalizacja tablic
Tablicę możesz wypełnić na kilka sposobów, a kilka skrótów warto znać:
Pułapka do zapamiętania: gdy podasz mniej inicjalizatorów niż rozmiar, pozostałe elementy są inicjalizowane wartością (zerem dla typów liczbowych), a nie zostają śmieciami. Ale tablica bez żadnego inicjalizatora, na przykład lokalne int e[4];, zawiera nieokreślone wartości, a odczytanie ich przed przypisaniem to niezdefiniowane zachowanie.
Pętla po tablicy
Ponieważ elementy leżą w pamięci jeden za drugim, przechodzisz tablicę zwykłą pętlą z indeksem. Aby nie wyjść poza zakres, steruj pętlą prawdziwą długością tablicy zamiast wpisanej na sztywno liczby:
sizeof(scores) to łączna liczba bajtów całej tablicy; podzielenie jej przez sizeof(scores[0]) (rozmiar jednego elementu) daje liczbę elementów. Od C++17 jest czytelniejszy zapis, std::size(scores), który lepiej się czyta i nie skompiluje się, jeśli przez przypadek podasz mu wskaźnik. Jeszcze prościej, gdy potrzebujesz tylko wartości: pętla for po zakresie całkowicie pomija rachunki na indeksach.
Pułapka wyjścia poza zakres
C++ nie sprawdza zakresu przy arr[i]. Indeksowanie za ostatnim elementem nie rzuca wyjątku ani nie ostrzega: odczytuje lub zapisuje pamięć, która akurat tam leży. To najczęstszy błąd związany z tablicami i klasyczne niezdefiniowane zachowanie:
int a[3] = {1, 2, 3};
a[3] = 99; // OOPS - valid indices are 0..2, not 3
cout << a[5]; // garbage, crash, or corruption - undefined behavior
Błąd o jeden zwykle kryje się w warunku pętli. Zapis i <= n zamiast i < n wykonuje o jeden krok za dużo i sięga do arr[n], który nie istnieje:
for (int i = 0; i <= n; i++) // BUG: when i == n, arr[i] is out of bounds
cout << arr[i];
Rozwiązaniem jest dyscyplina z poprzedniej sekcji: w pętli używaj i < size, nigdy <=, i wyliczaj rozmiar z tablicy, zamiast przepisywać literał, który rozjedzie się z rzeczywistością po dodaniu elementu.
Rozpad tablicy: ukryty wskaźnik
Najbardziej podchwytliwe zachowanie tablic w C++ to rozpad (decay): gdy przekazujesz tablicę do funkcji, po cichu zamienia się ona we wskaźnik na pierwszy element. Informacja o rozmiarze ginie, więc sizeof wewnątrz funkcji mierzy wskaźnik, a nie tablicę.
Zauważ, że jako parametry funkcji int arr[] i int* arr są identyczne; nawiasy są tylko ozdobą. Ponieważ liczba elementów ginie, musisz samodzielnie przekazać długość obok tablicy:
int sum(const int* arr, int n) {
int total = 0;
for (int i = 0; i < n; i++) total += arr[i];
return total;
}
Ten wzorzec "przekaż wskaźnik i długość, i miej nadzieję, że się zgadzają" to właśnie tarcie, które popycha większość kodu C++ w stronę std::array i std::vector: one pamiętają własny rozmiar i nigdy się nie rozpadają.
Kilka słów o tablicach wielowymiarowych
Nawiasy możesz zagnieżdżać, aby utworzyć siatkę. Tablica 2D to w rzeczywistości tablica tablic, ułożona w pamięci wiersz po wierszu:
Indeksujesz ją jako grid[row][col]. Obowiązują te same zastrzeżenia dotyczące zakresu i rozpadu, a nawet gorsze, bo przekazanie tablicy 2D do funkcji wymaga wypisania każdego wymiaru poza pierwszym (void f(int g[][3])). Dla wszystkiego poza małą, stałą siatką vector złożony z vectorów jest o wiele mniej podatny na błędy.
Dalej: vector
Surowe tablice są szybkie i przewidywalne, ale stały rozmiar, brak ochrony zakresu i rozpad do wskaźnika sprawiają, że są niewygodne w codziennym kodzie. Następnie poznasz std::vector: tablicę o zmiennym rozmiarze, która rośnie na żądanie, pamięta własny rozmiar i od razu współpracuje z algorytmami STL, dając prawie wszystko, co oferują tablice, przy znacznie mniejszej liczbie sposobów na strzelenie sobie w stopę.
Najczęściej zadawane pytania
Jak zadeklarować i zainicjalizować tablicę w C++?
Napisz typ elementów, nazwę i rozmiar w nawiasach kwadratowych, a opcjonalnie listę w klamrach: int scores[4] = {90, 75, 100, 60};. Rozmiar możesz pominąć, gdy podajesz inicjalizatory: int scores[] = {90, 75, 100, 60}; pozwala kompilatorowi policzyć je za ciebie.
Jak poznać długość tablicy w C++?
Dla prawdziwej tablicy, która jest nadal w zasięgu, użyj std::size(arr) (C++17) albo sizeof(arr) / sizeof(arr[0]). To nie działa, gdy tablica rozpadła się już do wskaźnika (np. w funkcji, która przyjęła int arr[]), bo wtedy sizeof podaje rozmiar wskaźnika, a nie tablicy.
Co się stanie po wyjściu poza zakres tablicy w C++?
To niezdefiniowane zachowanie. C++ nie sprawdza zakresu przy arr[i], więc odczyt lub zapis za końcem może wywołać awarię, zwrócić śmieci albo po cichu uszkodzić sąsiednią pamięć. Zawsze trzymaj indeks w zakresie od 0 do size - 1.