Menu

Tablice w C++: deklaracja, indeksowanie i częste pułapki

Surowe tablice w C++: jak je deklarować i inicjalizować, bezpiecznie indeksować, przechodzić pętlą z użyciem rozmiaru, pułapka rozpadu tablicy do wskaźnika i dlaczego std::array oraz vector zwykle wygrywają.

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

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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ