Tablice w języku C++

Tablice w języku C++ są podstawową strukturą danych, która pozwala na przechowywanie wielu elementów tego samego typu w jednej zmiennej. Dostęp do poszczególnych elementów tablicy odbywa się za pomocą indeksów. Tablice mogą przechowywać dowolny typ danych, w tym typy wbudowane, obiekty klas, wskaźniki itp.
Deklaracja i Inicjalizacja Tablic
Tablica musi być zadeklarowana z określeniem jej typu oraz liczby elementów, które może zawierać. Liczba elementów, czyli rozmiar tablicy, musi być stałą wartością całkowitą. Przykład deklaracji:
int mojaTablica[5];
Tablicę można również zainicjalizować podczas deklaracji, podając wartości jej elementów: Przykład inicjalizacji:
int dniWMiesiacu[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
Jeśli podczas inicjalizacji podasz mniej wartości niż rozmiar tablicy, pozostałe elementy zostaną zainicjalizowane na zero (dla typów podstawowych).
Dostęp do Elementów
Do elementów tablicy dostęp uzyskuje się za pomocą indeksów. Indeksowanie rozpoczyna się od 0. Przykład dostępu do elementu:
cout << "Liczba dni w styczniu: " << dniWMiesiacu[0] << endl;
Iteracja przez Tablicę
Aby przejść przez wszystkie elementy tablicy, często używa się pętli for. Przykład iteracji:
for(int i = 0; i < 12; i++) {
    cout << "Miesiąc " << i + 1 << " ma " << dniWMiesiacu[i] << " dni." << endl;
}
Przykład zapisu do tablicy
Rozważmy tablicę typu int o nazwie mojaTablica i rozmiarze 5 elementów. Aby zapisać wartości do tej tablicy, możesz użyć następującej składni:
int mojaTablica[5]; // Deklaracja tablicy

// Zapis do tablicy
mojaTablica[0] = 10; // Zapisuje wartość 10 do pierwszego elementu tablicy
mojaTablica[1] = 20; // Zapisuje wartość 20 do drugiego elementu tablicy
mojaTablica[2] = 30; // i tak dalej
mojaTablica[3] = 40;
mojaTablica[4] = 50;
Powyższy kod inicjalizuje tablicę, przypisując wartości do każdego z jej elementów. Możesz również zainicjalizować tablicę w momencie deklaracji, jak pokazano wcześniej, ale czasami może być konieczne wprowadzenie wartości do tablicy w późniejszym czasie, na przykład w oparciu o dane wejściowe użytkownika lub wyniki obliczeń.
for(int i = 0; i < 5; i++) {
    mojaTablica[i] = (i + 1) * 10; // Przypisuje wartości 10, 20, 30, 40, 50
}
W tym przykładzie pętla for przechodzi przez każdy indeks tablicy od 0 do 4 i przypisuje do każdego elementu wartość (i + 1) * 10.
Uwagi
  • Próba zapisu do indeksu poza zakresem tablicy (np., mojaTablica[5] = 60; w tablicy o rozmiarze 5) prowadzi do niezdefiniowanego zachowania, co może powodować błędy i awarie programu.
  • Zawsze upewnij się, że indeks, do którego próbujesz zapisać, mieści się w zakresie rozmiaru tablicy.
Zapisywanie wartości w pętli
Często zdarza się, że potrzebujesz zainicjalizować lub zmienić wartości tablicy za pomocą pętli. To ułatwia pracę z dużymi tablicami.
Wielowymiarowe Tablice
Tablice mogą mieć więcej niż jeden wymiar, np. tablice dwuwymiarowe przypominają macierze matematyczne. Przykład deklaracji i inicjalizacji tablicy dwuwymiarowej:
int macierz[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

// Dostęp do elementu z pierwszego wiersza i drugiej kolumny
cout << macierz[0][1] << endl; // Wypisze 2
Inicjalizacja Tablic bez Podania Rozmiaru
Możesz pominąć rozmiar tablicy w deklaracji, jeśli od razu ją inicjalizujesz. Kompilator określi rozmiar na podstawie liczby podanych elementów. Przykład:
int mojaTablica[] = {1, 2, 3, 4, 5}; // Tablica 5-elementowa
Tablice jako Argumenty Funkcji
W języku C++ nie można bezpośrednio przekazać całej tablicy jako argument do funkcji bez używania wskaźników lub referencji. Gdy mówimy o przekazywaniu „zwykłej” tablicy do funkcji, de facto przekazujemy wskaźnik do pierwszego elementu tej tablicy. To zachowanie wynika z faktu, że nazwa tablicy w wyrażeniu jest automatycznie konwertowana (z wyjątkiem kilku specyficznych przypadków) na wskaźnik do jej pierwszego elementu.
Dlaczego nie można przekazać całej tablicy?
Przekazywanie całej tablicy jako kopiowanej wartości byłoby nieefektywne pod względem wykorzystania pamięci i czasu procesora, ponieważ wymagałoby skopiowania każdego elementu tablicy do nowego bloku pamięci na stosie wywołań funkcji. Z tego powodu, w języku C++ przyjęto konwencję, że przekazuje się jedynie wskaźnik do pierwszego elementu tablicy. Dzięki temu funkcje mogą pracować na oryginalnych danych, co jest znacznie szybsze i bardziej efektywne pamięciowo.

Przykład

Poniższy przykład ilustruje, jak „przekazywanie” tablicy do funkcji jest realizowane poprzez przekazanie wskaźnika do jej pierwszego elementu. Należy zauważyć, że funkcja potrzebuje dodatkowego argumentu określającego rozmiar tablicy, aby wiedzieć, ile elementów może bezpiecznie przeczytać lub zmodyfikować. Przykład funkcji wyświetlającej tablicę:
#include <iostream>
using namespace std;
// Funkcja, która przyjmuje "tablicę" poprzez wskaźnik do jej pierwszego elementu
// i rozmiar tej tablicy jako argumenty.
void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main() {
    int myArray[] = {1, 2, 3, 4, 5};
    int myArraySize = sizeof(myArray) / sizeof(myArray[0]);

    printArray(myArray, myArraySize); // Tutaj przekazujemy tablicę (w praktyce wskaźnik do niej) i jej rozmiar

    return 0;
}
W tym przykładzie, gdy przekazujemy myArray do funkcji printArray, język C++ automatycznie konwertuje myArray na wskaźnik do pierwszego elementu tablicy (int*). Funkcja printArray może więc operować na oryginalnej tablicy, ale potrzebuje dodatkowej informacji o jej rozmiarze, ponieważ wskaźnik nie niesie informacji o liczbie elementów w tablicy. W języku C++ nie istnieje mechanizm umożliwiający bezpośrednie przekazanie całej tablicy do funkcji bez wykorzystania wskaźników lub referencji. To, co często nazywane jest „przekazywaniem tablicy”, w rzeczywistości polega na przekazywaniu wskaźnika do pierwszego elementu tablicy, co wymaga od programisty zarządzania rozmiarem tablicy w sposób manualny.
Ograniczenia Tablic
  • Rozmiar tablicy musi być znany w momencie kompilacji i nie może zmieniać się dynamicznie w czasie działania programu.
  • Tablice nie śledzą własnego rozmiaru, więc przy pracy z nimi należy samodziel
Obliczanie rozmiaru tablicy
Instrukcja int n = sizeof(arr)/sizeof(arr[0]); jest często używana w języku C++ do obliczenia liczby elementów w tablicy statycznej, czyli tablicy, której rozmiar jest znany w czasie kompilacji. Pozwala to na dynamiczne dostosowanie operacji wykonywanych na tablicy do jej rzeczywistego rozmiaru bez konieczności ręcznego wprowadzania liczby elementów.
Jak to działa?
  • sizeof(arr) zwraca całkowity rozmiar tablicy w bajtach. Rozmiar ten obejmuje wszystkie elementy tablicy.
  • sizeof(arr[0]) zwraca rozmiar jednego elementu tablicy, również w bajtach. W przypadku arr[0], jest to rozmiar pierwszego elementu.
Dzieląc całkowity rozmiar tablicy przez rozmiar pojedynczego elementu, otrzymujemy liczbę elementów zawartych w tablicy.
Przykład
Załóżmy, że mamy tablicę typu int arr[5];. W większości środowisk programistycznych typ int zajmuje 4 bajty. Dlatego:
  • sizeof(arr) zwróci 20 bajtów (5 elementów razy 4 bajty na element).
  • sizeof(arr[0]) zwróci 4 bajty (rozmiar jednego int).
Dzielenie tych wartości, czyli 20 / 4, daje nam 5, co jest liczbą elementów w tablicy.
Dlaczego to jest przydatne?
Takie obliczenie jest szczególnie przydatne, ponieważ pozwala na pisanie bardziej elastycznego kodu. Nie musisz pamiętać o aktualizacji liczby elementów w tablicy w każdym miejscu kodu, gdy zmieniasz jej rozmiar. Zamiast tego, rozmiar tablicy jest obliczany automatycznie, co minimalizuje ryzyko błędów, zwłaszcza w przypadku tablic o dużych rozmiarach lub gdy tablica jest modyfikowana przez innego programistę, który może nie być świadomy wszystkich miejsc, w których rozmiar tablicy jest używany w kodzie.
Ograniczenia
Metoda sizeof(arr)/sizeof(arr[0]) działa poprawnie tylko dla tablic statycznych (zadeklarowanych bezpośrednio w kodzie, a nie alokowanych dynamicznie). Dla wskaźników i tablic alokowanych dynamicznie (np. za pomocą new w C++), taka metoda nie zadziała, ponieważ sizeof na wskaźniku zwróci rozmiar wskaźnika, a nie rozmiar alokowanej pamięci, do której wskaźnik wskazuje. W takich przypadkach należy przechowywać rozmiar tablicy w osobnej zmiennej lub korzystać z kontenerów STL, takich jak std::vector, które automatycznie zarządzają rozmiarem.