Często podczas nauki C słyszymy, że tablica tak naprawdę jest wskaźnikiem. Oczywiście takie uproszczenie pomaga na początku zrozumieć pewne rzeczy, ale w końcu warto poznać różnice.

Skąd ten pomysł?

Jeżeli traktujemy tablice tak samo jak wskaźniki, łatwiej nam zapamiętać, że:

  • Operator indeksu [] możemy używać na wskaźnikach.
  • Arytmetyka wskaźników działa na tablicach.
  • Arytmetyka wskaźników dodaje/odejmuje indeksy tablicy.
  • Tablice przekazujemy do funkcji jako wskaźnik.

No dobra a jakie są różnice?

Najważniejszą różnicą między tablicą wskaźnikiem jest inicjalizacja. Tablica alokuje pamięć na wszystkie swoje elementy, natomiast wskaźnik tylko miejsce do zapisania adresu. Dlatego właśnie wynik operacji sizeof będzie się różnić.

#include <stdio.h>

uint32_t array[10] = {0};
uint32_t *ptr = array;

void main(void)
{
    printf("%d\n", sizeof(array)); //40
    printf("%d\n", sizeof(ptr)); //wynik zależny od procesora, ale na pewno mniejszy niż 40
}

Wskaźnik przechowuje adres w pamięci, a jego typ mówi jak interpretować wartość spod tego adresu. Pod wskaźnik w każdej chwili możemy przypisać inny adres, oczywiście jeśli nie jest const. Pod tablicę natomiast nie możemy przypisać nowego adresu.

Kolejna różnica jest tak naprawdę ciekawostką, bo raczej nie znajdziemy dla niej nigdzie zastosowania. Otóż tablica może być zadeklarowana z modyfikatorem register. Mówi się, że ten modyfikator jest sugestią dla kompilatora, żeby zmienną zadeklarować w rejestrze CPU i wpływa na optymalizację. No ale w końcu kompilator może się do tej sugestii zastosować albo nie. Poza tym zmienne bez tego modyfikatora również może umieścić w rejestrze. Tak naprawdę jedyną konsekwencją użycia register jest uniemożliwienie pobrania adresu tej zmiennej.

Czyli tablica z modyfikatorem register nie może być konwertowana na wskaźnik i np. przekazana do funkcji, o czym za chwilę.

Konwersja tablicy na wskaźnik

No dobra, ale skoro tablica nie jest wskaźnikiem, a w pamięci przechowuje inne dane, to dlaczego w większości przypadków działa jak wskaźnik? Ponieważ w większości przypadków zachodzi niejawne rzutowanie. Po angielsku mówi się na to pointer decay czyli, że tablica degeneruje się do wskaźnika. W praktyce po prostu używany jest wskaźnik do pierwszego elementu.

Tak naprawdę nawet operacja dostępu do elementu spod zadanego indeksu, czyli [], pod spodem wymaga wzięcia adresu początkowego elementu i dodania offsetu (chyba że używamy register, na szczęście da się wtedy dostać do indeksów tablicy).

Tak więc kiedy przekazujemy tablicę do funkcji, przypisujemy pod wskaźnik, albo robimy na niej inną operację, brany jest adres pierwszego elementu.

Tablica jako parametr funkcji

Nad tablicami jako parametry funkcji warto się jeszcze na chwilę zatrzymać. Ponieważ możemy utworzyć nagłówek funkcji na trzy sposoby:

void fun1(uint32_t *array);
void fun2(uint32_t array[]);
void fun3(uint32_t array[5]);

W pierwszym przypadku przekazujemy po prostu wskaźnik, w drugim tablicę o nieznanym rozmiarze, a w trzecim tablicę o zadanym rozmiarze. Wszystkie trzy przypadki są traktowane jako wskaźnik, a informacja o konkretnym rozmiarze nie jest do niczego wykorzystywana.

Czasem taki parametr chcemy zaopatrzyć w dodatkowy modyfikator. W przypadku wskaźnika nie ma z tym problemu:

void fun1(uint32_t *const restrict volatile array);

A jak to zrobić w przypadku parametru jako tablica? Tak:

void fun2(uint32_t array[const restrict volatile]);
void fun3(uint32_t array[const restrict volatile 5]);

Pod warunkiem, że korzystamy ze standardu C99, bo w C90 takiej opcji nie było. C99 dodało jeszcze jedną opcję:

void fun3(uint32_t array[static 5]);

Dodanie static do rozmiaru tablicy informuje kompilator, że zawsze przekazujemy tablicę zawierającą co najmniej 5 elementów. Nie możemy więc przekazać ani mniejszej tablicy ani np. null pointera.

Oznacza to, że kompilator może wykonać dodatkowe optymalizacje, a obowiązek spełnienia tego założenia zostaje po stronie programisty. Na szczęście niektóre kompilatory, jak na przykład clang, potrafią to wykryć i zwracają warning:

#include <stdio.h>
#include <stdint.h>

void fun(uint32_t arg[static 5])
{
   ...
}

int main(void)
{
    uint32_t array[2] = {0};
    fun(array);

    return 0;
}
warning: array argument is too small; contains 2 elements, callee requires at least 5 [-Warray-bounds]

    fun(array);

Niestety GCC jeszcze nie ma takiej opcji. Prawdopodobnie podobnie ma się sytuacja z większością innych kompilatorów, dlatego na razie raczej nie ma sensu jej używać. Ale może w przyszłości to się zmieni.

Podsumowanie

Tak więc odpowiadając na pytanie z tytułu – tablica nie jest wskaźnikiem. Po prostu w większości przypadków jest konwertowana na wskaźnik do pierwszego elementu. Główną różnicą między tablicą a wskaźnikiem jest alokacja pamięci. W C99 dodano również opcję static odróżniającą tablicę od wskaźnika dla argumentów funkcji, ale za bardzo nie działa i raczej nie ma sensu jej używać.

Jeżeli chcesz dowiedzieć się więcej o tym, jak pisać dobry kod w C – przygotowuję właśnie kurs online “C dla zaawansowanych”. Wejdź na https://cdlazaawansowanych.pl/ i zapisz się na mój newsletter. W ten sposób informacje o kursie na pewno Cię nie ominą.