CodeGym /Kursy /C++ SELF /Poznanie tablic: T arr[N]

Poznanie tablic: T arr[N]

C++ SELF
Poziom 11, Lekcja 1
Dostępny

1. Wstęp

Gdy piszesz program, szybko okazuje się, że jedna liczba to za mało. Musisz zachować np. 7 wartości kroków za tydzień, 12 miesięcy wydatków, 10 ocen z kartkówek. Oczywiście możesz stworzyć zmienne x1, x2, x3… ale to droga w ciemność, gdzie kod zaczyna przypominać serial o 500 odcinkach bez scenarzysty.

Tablica w C — to „pudełko” o stałym rozmiarze, zawierające N elementów tego samego typu, do każdego elementu można odwołać się poprzez numer (indeks).

Składnia T arr[N]

Zap zapis wygląda tak:

T arr[N];

Gdzie:

  • T — typ elementów (int, double, char itp.)
  • arr — nazwa tablicy
  • N — liczba elementów (stała)

Ważne, by od razu „załapać myśl dnia”: tablica to jeden obiekt zawierający N elementów. Nie „N oddzielnych zmiennych”, lecz jeden spójny obiekt.

Rozmiar ustalany jest od razu i się nie zmienia

Częsta „pułapka myślowa” początkującego: wydaje się, że tablica to jak lista, którą można rozciągać. Ale tablica w stylu C nie działa tak. Jej rozmiar jest określony w momencie deklaracji i później się nie zmienia. Jeśli zadeklarowałeś int a[5];, to zawsze będzie to 5 komórek i nie da się „dodać szóstej” bez zmiany narzędzia (o innych narzędziach później, dziś trzymamy fokus).

W zadaniach ćwiczebnych i w realnym kodzie dobrą praktyką jest trzymanie rozmiaru tablicy nie jako „magic number”, lecz jako stałej czasu kompilacji. Tutaj przydaje się constexpr i typ std::size_t (już omawialiśmy, że rozmiary i indeksy zwykle są właśnie w size_t).

Mini‑przykład: zadeklarujmy tygodniowy tracker kroków (nasz mini‑projekt na dziś nazwiemy StepStats).

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};            // wszystkie elementy = 0
    std::cout << DAYS << '\n';       // 7
}

Zwróć uwagę na = {} — to nie „dekoracja”, to pas bezpieczeństwa: tablica od razu dostaje przewidywalne wartości.

Tablica tworzona przez kompilator

Długość tablicy musi być znana w czasie kompilacji programu. Tak, tak. Taki kod nie zadziała:

#include <cstddef>
#include <iostream>

int main() {
    int n;
    std::cin >> n;
    int arr[n];  // ❌ w standardowym C++ to błąd
}

Wszystko przez to, że C++ to bardzo stary język i w momencie jego powstania takie podejście było normą.

2. Indeksowanie: od 0 do N - 1

Teraz najważniejsza część, przez którą łamią się losy, terminy i czasami klawiatury: indeksy.

W tablicy o N elementach dopuszczalne indeksy to:

  • pierwszy element: arr[0]
  • ostatni element: arr[N - 1]

Czyli zakres indeksów: od 0 do N-1 włącznie.

To historyczna tradycja C/C++: indeks to „przesunięcie od początku”. Przesunięcie do pierwszego elementu wynosi zero, stąd pierwszy indeks to 0.

Oto pomocna tabelka do utrwalenia:

Liczba elementów N Pierwszy indeks Ostatni indeks
1
0
0
2
0
1
5
0
4
7
0
6

Pokażemy to w kodzie (i przy okazji poćwiczymy, żeby nie pisać „ostatni element = arr[N]”, bo to nieprawda):

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int a[N] = {1, 2, 3, 4, 5};

    std::cout << a[0] << '\n';       // 1
    std::cout << a[N - 1] << '\n';   // 5
}

3. Iterowanie tablicy pętlą: granica i < N

Gdy mamy tablicę, prawie zawsze następnym krokiem jest przejście po niej pętlą. I tutaj pojawia się legendarny błąd „off‑by‑one”: o jedno się pomyliłeś — witaj w krainie niespodzianek.

Prawidłowa forma pętli dla tablicy długości N:

for (std::size_t i = 0; i < N; ++i) {
    // pracujemy z arr[i]
}

Dlaczego ściśle < N, a nie <= N? Bo arr[N] — to już nie element tablicy. Ostatni to arr[N - 1].

Schemat, który warto mieć w głowie:

flowchart LR
    A["i = 0"] --> B{"i < N ?"}
    B -- tak --> C["używamy arr[i]"]
    C --> D["++i"]
    D --> B
    B -- nie --> E["koniec pętli"]

I przykład dla StepStats: wypiszemy wszystkie 7 wartości (na razie są zerowe).

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t i = 0; i < DAYS; ++i) {
        std::cout << steps[i] << ' ';    // 0 0 0 0 0 0 0
    }
    std::cout << '\n';
}

4. Inicjalizacja i wczytywanie tablicy

Przechodzimy do tematu, gdzie początkujący często otrzymują „śmieci” w wyjściu i zaczynają podejrzewać, że komputer ich nienawidzi. Komputer nie nienawidzi. On po prostu dosłownie wykonuje to, co napisałeś.

Pełna inicjalizacja {...}

Jeśli podałeś wartości dla wszystkich elementów — wszystko proste i schludne:

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 3;
    int a[N] = {10, 20, 30};

    std::cout << a[1] << '\n'; // 20
}

Częściowa inicjalizacja {10, 20}

Jeżeli podałeś mniej wartości niż rozmiar tablicy, „ogon” zostanie wypełniony zerami. To przyjemna cecha inicjalizacji listowej, na której można polegać w zadaniach naukowych.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int a[N] = {10, 20};   // pozostałe staną się 0

    for (std::size_t i = 0; i < N; ++i) {
        std::cout << a[i] << ' ';    // 10 20 0 0 0
    }
    std::cout << '\n';
}

Inicjalizacja zerowa = {}

Jeżeli chcesz, żeby „wszystkie elementy były 0” — napisz = {}. To jeden z najlepszych sposobów uzyskać przewidywalność.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 4;
    double x[N] = {};  // 0.0 0.0 0.0 0.0

    std::cout << x[0] << '\n'; // 0
}

Dlaczego „niezinicjalizowana” tablica jest niebezpieczna

Ważne ostrzeżenie. Jeśli napiszesz:

int a[5];

…to elementy nie muszą być zerami. Tam może być cokolwiek. Nie dlatego, że C++ jest „zły”, ale dlatego, że poprosiłeś o utworzenie tablicy i nie powiedziałeś, jak ją wypełnić.

Nie będziemy pokazywać przykładu, który „wyświetla śmieci”, bo to zależy od kompilatora i środowiska, a przykład powinien być odtwarzalny. Reguła prosta: jeśli tablica ma zaczynać „z czystego konta”, użyj = {}.

W StepStats to szczególnie logiczne: dopóki użytkownik nie wprowadzi kroków, wartości powinny być zerowe, a nie „losowe liczby, które świetnie motywują do sportu, ale tylko jeśli nie wiesz, że to śmieci”.

Wzorzec „wczytaj N wartości”

Zróbmy teraz pierwszy użyteczny krok w naszym StepStats: wczytamy kroki za tydzień.

Ważna uwaga: wczytywanie przez std::cin nie gwarantuje, że użytkownik poda poprawne liczby, ale dziś nie komplikujemy obsługi błędów wejścia — trenujemy mechanikę tablic i pętli.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t d = 0; d < DAYS; ++d) {
        std::cin >> steps[d];        // wczytujemy 7 liczb
    }
}

Teraz dodajmy wypis, żeby sprawdzić, że „naprawdę zapisaliśmy” (nie tylko wczytaliśmy i zapomnieliśmy):

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t d = 0; d < DAYS; ++d) std::cin >> steps[d];
    for (std::size_t d = 0; d < DAYS; ++d) std::cout << steps[d] << ' ';
    std::cout << '\n';
}

5. Dlaczego wyjście poza granice jest gorsze niż błąd

W językach z twardą kontrolą granic (lub w kontenerach, które potrafią sprawdzać granice) zwykle otrzymasz czytelny błąd. W tablicach w stylu C jest surowiej: jeśli odwołasz się do arr[N] lub arr[999], kompilator często nie wykryje tego, a w czasie wykonania możesz dostać nieprzewidywalne zachowanie.

To nazywa się UB (undefined behavior) — zachowanie, którego standard nie musi określać. Po ludzku: „może działać, może się zawiesić, może cicho uszkodzić dane, a może udawać, że wszystko OK, by zepsuć się jutro”.

Temat UB jest ogromny, ale dziś wystarczy jedna praktyczna zasada: nigdy nie odwołuj się do elementów poza zakresem 0..N-1. Tak, to obejmuje „przypadkowe <= zamiast <”.

Przy okazji, fakt, że w standardzie osobno omawia się indeksowanie i jego niuanse (np. kwestie „array subscripting and xvalues”), sugeruje, że operacja nie jest tak prosta, jak się wydaje.

6. Częste błędy w kodzie: przykłady i prewencja

Teraz sekcja, która oszczędza godziny życia. Błędy tu są maksymalnie ludzkie: nie jesteś „złym programistą”, po prostu po raz pierwszy żyjesz w świecie, gdzie 0 to „pierwszy”, a N to „już poza”.

i <= N zamiast i < N

Błąd wygląda niewinnie: „chcę przejść N elementów”. Ale przy i <= N wykona się N+1 iteracja i sięgniesz do arr[N].

Zły kod (nie rób tak):

for (std::size_t i = 0; i <= N; ++i) {
    std::cout << arr[i];
}

Dobry kod:

for (std::size_t i = 0; i < N; ++i) {
    std::cout << arr[i];
}

Pomyłka „ostatni element” i arr[N]

To prawie zawsze „błąd myślenia”: wydaje się, że jeśli jest N elementów, to ostatni indeks to N. Ale indeksy zaczynają się od 0, więc ostatni to N - 1.

W StepStats ostatni dzień tygodnia (siódmy dla człowieka) ma indeks 6.

„Magic numbers” rozsiane po kodzie

Jeśli wpisujesz 7 w pięciu miejscach, prawie na pewno kiedyś zmienisz 7 na 10 w czterech miejscach i zapomnisz o piątym. Klasyka.

Zamiast tego trzymaj stałą:

constexpr std::size_t DAYS = 7;

I używaj jej wszędzie.

Indeks jako int i dziwne porównania

Technicznie int i = 0; i < DAYS; ++i często „działa”. Ale przy mieszaniu typów można trafić na nieprzyjemne ukryte konwersje.

Widzieliśmy już, że rozmiary to zwykle std::size_t, więc indeks w takich pętlach sensownie zrobić std::size_t. To nie „bo tak się przyjęło”, a dlatego, że mniejsze ryzyko przypadkowego porównania typu ze znakiem z typem bez znaku i nieprzyjemnych niespodzianek.

7. Typowe błędy przy pracy z tablicami w stylu C

Błąd nr 1: wyjście poza granice tablicy przez użycie <=.
Ten błąd jest szczególnie podstępny, bo wygląda „prawie dobrze”, a konsekwencje mogą wystąpić później. Jeden nadmiarowy krok pętli — i odwołujesz się do pamięci, która nie należy do tablicy. Efekt może być dowolny: od „wydaje się działać” do nagłego crashu.

Błąd nr 2: czytanie niezinicjalizowanych elementów.
Jeżeli zadeklarowałeś int a[10]; i od razu wypisujesz a[0], czytasz wartość, której sam nie ustawiłeś. Czasami to „0”, czasami nie — nie można na to liczyć. Jeśli potrzebujesz przewidywalnego startu, użyj = {} lub wypełnij tablicę wejściem przed pierwszym odczytem.

Błąd nr 3: „magic numbers” zamiast jednej stałej rozmiaru.
Gdy rozmiar jest wpisany jako 7 w różnych miejscach, kod szybko staje się niezarządzalny. Raz zmienisz rozmiar i zapomnisz poprawić pętlę lub wypis. Stała constexpr std::size_t N = ...; czyni kod dużo bardziej niezawodnym.

Błąd nr 4: błędne rozumienie „ostatniego elementu”.
Początkujący często piszą arr[N], myśląc, że to ostatni element. W rzeczywistości ostatni element to arr[N - 1], bo indeksacja zaczyna się od zera. Błąd ten jest dokuczliwy, bo wydaje się „ludzko” logiczny, ale w programowaniu logika jest inna.

Błąd nr 5: próba uczynienia tablicy „dynamiczną” przez int n; cin >> n; int a[n];.
W standardzie C++ rozmiar tablicy w stylu C musi być znany podczas kompilacji. Niektóre kompilatory tolerują taki kod jako rozszerzenie, ale nie jest on przenośny i w logice dydaktycznej zwykle uznaje się to za zły pomysł. Jeśli rozmiar jest znany wcześniej — używaj tablicy C; jeśli nie — potrzebne jest inne narzędzie, ale to już temat na inny wykład.

1
Zadanie
C++ SELF, poziom 11, lekcja 1
Niedostępne
Pierwszy i ostatni
Pierwszy i ostatni
1
Zadanie
C++ SELF, poziom 11, lekcja 1
Niedostępne
Odwracanie listy
Odwracanie listy
1
Zadanie
C++ SELF, poziom 11, lekcja 1
Niedostępne
Przesunięcie grafiku
Przesunięcie grafiku
1
Zadanie
C++ SELF, poziom 11, lekcja 1
Niedostępne
Ekstrema tygodnia
Ekstrema tygodnia
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION