Tablice

Tablica C jest jedną z podstawowych struktur danych w językach proceduralnych. Umożliwia przechowywanie wielu elementów tego samego typu w ciągłym obszarze pamięci. W kontekście języków niskiego poziomu jej zrozumienie wymaga spojrzenia zarówno na składnię, jak i na model pamięci. W językach takich jak C i C++ tablica nie jest obiektem z dodatkowymi informacjami – to po prostu blok kolejnych komórek pamięci. W dalszej części notatek pojęcie tablic będzie rozpatrywane głównie na gruncie C oraz C++.

Tablice jako ciągły blok pamięci i zależność między indeksem a adresem

Tablica to uporządkowany zbiór elementów jednego typu, przechowywanych obok siebie w pamięci. Jeżeli pierwszy element znajduje się pod adresem A, to kolejny znajduje się pod adresem A + sizeof(typ).

Dla przykładu:

int tab[4] = {10, 20, 30, 40};

Zakładając, że int zajmuje 4 bajty, rozmieszczenie w pamięci będzie wyglądać logicznie tak:

  • tab[0] → adres A
  • tab[1] → A + 4
  • tab[2] → A + 8
  • tab[3] → A + 12

Indeks jest więc tylko wygodnym sposobem obliczania przesunięcia względem początku tablicy.

Ważne: język C nie przechowuje informacji o długości tablicy. Kompilator zna ją tylko w miejscu deklaracji. Po przekazaniu do funkcji tablica „degeneruje” do wskaźnika.

Tablica C – definicja, własności i zachowanie w kontekście wskaźników

Tablica C to konstrukcja statyczna o ustalonej liczbie elementów, określonej w czasie kompilacji (z wyjątkiem VLA w standardzie C99). Jej nazwa jest w większości kontekstów traktowana jak wskaźnik do pierwszego elementu.

Przykład:

int tab[5];
int *p = tab;

tab jest adresem pierwszego elementu (&tab[0]).

Istnieje subtelna różnica:

  • tab – typ: „tablica 5 elementów int”
  • &tab – wskaźnik do całej tablicy

To ma znaczenie przy operacjach arytmetycznych.

Rozmiar tablicy można obliczyć:

int n = sizeof(tab) / sizeof(tab[0]);

Działa to tylko w tym samym zakresie, w którym tablica została zadeklarowana.

Deklaracja tablicy C oraz inicjalizacja statyczna i dynamiczna

Deklaracja tablicy C wymaga podania typu oraz liczby elementów.

Podstawowa forma:

int liczby[10];

Inicjalizacja przy deklaracji:

int liczby[5] = {1, 2, 3, 4, 5};

Możliwe jest pominięcie rozmiaru, gdy podajemy inicjalizator:

int liczby[] = {1, 2, 3};

Kompilator sam ustali długość.

Jeżeli inicjalizator jest krótszy:

int liczby[5] = {1, 2};

Pozostałe elementy zostaną wyzerowane.

Tablica dynamiczna w C

Dynamiczna alokacja:

#include <stdlib.h>

int *tab = malloc(10 * sizeof(int));

Po zakończeniu pracy:

free(tab);

Brak free prowadzi do wycieku pamięci.

Tablice C i przekazywanie do funkcji oraz utrata informacji o rozmiarze

Gdy przekazujemy tablicę do funkcji:

void wypisz(int t[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d\n", t[i]);
    }
}

Parametr t[] jest równoważny int *t.

Informacja o długości nie jest przekazywana automatycznie, dlatego zawsze należy przekazywać rozmiar jako osobny argument.

Częsty błąd:

void f(int t[]) {
    int n = sizeof(t) / sizeof(t[0]);
}

Tutaj sizeof(t) zwróci rozmiar wskaźnika, a nie całej tablicy.

Tablice w języku C a tablice wielowymiarowe i sposób ich przechowywania

Tablice wielowymiarowe są przechowywane w sposób liniowy (tzw. row-major order).

Przykład:

int macierz[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

W pamięci:

1, 2, 3, 4, 5, 6

Indeksowanie:

macierz[i][j]

Jest tłumaczone na:

*( *(macierz + i) + j )

Przy przekazywaniu do funkcji trzeba znać drugi wymiar:

void f(int m[][3]) {
}

Lub:

void f(int (*m)[3]) {
}

Tablica char jako reprezentacja napisu i zakończenie znakiem null

Tablica char jest używana do przechowywania tekstu w C. Łańcuch znaków to tablica znaków zakończona znakiem '\0'.

Przykład:

char napis[6] = "Hello";

W pamięci:

’H’ 'e’ 'l’ 'l’ 'o’ '\0′

Brak znaku null powoduje niekontrolowane czytanie pamięci przez funkcje takie jak printf("%s", napis);.

Dynamiczna wersja:

char *napis = malloc(6);

Należy pamiętać o ręcznym wpisaniu '\0'.

wprowadzanie danych do tablicy przy użyciu pętli i funkcji wejścia

Wprowadzanie danych do tablicy odbywa się zwykle przy użyciu pętli.

Przykład dla liczb całkowitych:

int tab[5];

for (int i = 0; i < 5; i++) {
    scanf("%d", &tab[i]);
}

Dla napisu:

char napis[100];
fgets(napis, 100, stdin);

gets nie powinno być używane – brak kontroli długości.

Częsty błąd to brak sprawdzenia, czy użytkownik nie przekroczył rozmiaru tablicy.

Porównanie implementacji tablic w C, C++ i Python

C++

W C++ istnieje klasyczna tablica:

int tab[5];

Ale dostępne są też kontenery:

#include <vector>

std::vector<int> v = {1, 2, 3};

vector przechowuje rozmiar i zarządza pamięcią automatycznie.

Python

W Python nie ma klasycznej tablicy o stałym rozmiarze. Lista jest strukturą dynamiczną:

tab = [1, 2, 3]
tab.append(4)

Nie występuje ręczne zarządzanie pamięcią ani problem z przekroczeniem zakresu w sensie naruszenia pamięci – pojawi się wyjątek.

Typowe błędy przy pracy z tablicami i konsekwencje naruszenia pamięci

  1. Wyjście poza zakres indeksu.
  2. Użycie niezainicjalizowanej tablicy.
  3. Brak zwolnienia pamięci dynamicznej.
  4. Brak znaku '\0' w napisie.
  5. Mylenie sizeof(tab) z sizeof(wsaznik).

Wyjście poza zakres w C nie generuje wyjątku. Może nadpisać inne dane lub doprowadzić do trudnych do wykrycia błędów.

Tablice C w językach niskopoziomowych jest konstrukcją prostą, ale wymaga ścisłej kontroli nad rozmiarem i zakresem dostępu. Zrozumienie zależności między indeksem a adresem, między nazwą tablicy a wskaźnikiem oraz między alokacją statyczną i dynamiczną jest kluczowe dla poprawnego programowania w C i C++.