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++.
Spis treści
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
- Wyjście poza zakres indeksu.
- Użycie niezainicjalizowanej tablicy.
- Brak zwolnienia pamięci dynamicznej.
- Brak znaku
'\0'w napisie. - Mylenie
sizeof(tab)zsizeof(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++.