
Autor: 15.05.2023
Wskaźniki w języku C
Wskaźniki potrafią sprawić wiele problemów - i to nie tylko początkującym programistom. To taki element, który z jednej strony oferuje wielkie możliwości, ale jednocześnie wymaga sporej wprawy. W tym artykule poznasz najważniejsze techniki pracy ze wskaźnikami w języku C. Zaczynamy!
Podstawowe zasady pracy ze wskaźnikami
Wskaźnikiem nazywamy zmienną, która posiada specjalną moc. Zmienna taka jest w stanie przechowywać adres zmiennej czyli miejsce gdzie przechowywana jest konkretna informacja. Możesz sobie wyobrazić, że wskaźnik to taka specjalna zakładka w Twojej książce. Aby dostać się do ulubionego rozdziału nie musisz wertować wszystkich stron. Wystarczy, że otworzysz książkę w miejscu gdzie znajduje się zakładka i gotowe.
Aby dobrze operować na wskaźnikach musisz zrozumieć współpracę dwóch specjalnych operatorów:
& - operator adresu
* - operator wyłuskania
Spójrz do czego może służyć operator & (czyli operator adresu):
#include
int main() {
int number = 123;
printf("Wartość number to: %d \n", number);
printf("Adres number to: %p \n", &number);
return 0;
}
Używając modyfikatora %p możemy wyprowadzić na ekran odpowiednio sformatowany adres. A to efekt działania programu:
Wartość number to: 123
Adres number to: 0x7fff7ff1be3c
Na początku deklarujemy zmienną number. Następnie wyświetlamy jej zawartość, a na końcu odwołujemy się do jej adresu przy pomocy operatora &.
Teraz kolejny przykład:
#include
int main() {
int number = 123;
int *ptr; // 1
ptr = &number; // 2
// &
printf("Adres number to: %p \n", &number);
printf("Wartość number to: %d \n", number);
// *
printf("Adres number to (ptr): %p \n", ptr);
printf("Wartość number to (*ptr): %d \n", *ptr);
return 0;
}
Popatrz na taką deklarację: int *ptr. Jest to wskaźnik (*) o nazwie ptr, który może wskazywać na adres zmiennej o typie int.
Teraz kolejna deklaracja: ptr = &number. Oznacza ona, że wskaźnik ptr będzie pokazywał na adres zmiennej number.
Po analizie kolejnych linii kodu możemy stwierdzić, że: ptr to jest to samo co &number, a z kolei *ptr to jest to samo co number. I to cała podstawowa filozofia użycia wskaźników. W praktyce jednak ich użycie nie będzie takie proste, jak w powyższym przykładzie. Dlatego przejdźmy od razu do kolejnych zagadnień.
Wskaźniki i tablice
Wskaźników często używa się w połączeniu z tablicami. Najpierw przypomnijmy sobie podstawy pracy z tablicami w języku C:
#include
int main() {
int tab[3] = { 1, 2, 3 };
printf("tab[0] = %d \n", tab[0]);
printf("tab[1] = %d \n", tab[1]);
printf("tab[2] = %d \n", tab[2]);
return 0;
}
Mamy trójelementową tablicę liczb całkowitych. Do elementów możemy dostać się za pomocą indeksu. Na pewno pamiętasz, że pierwszy element tablicy indeksowany jest od 0. Efekt działania naszego programu wygląda tak:
tab[0] = 1
tab[1] = 2
tab[2] = 3
Wyobraź sobie, że elementy w tablicy są poukładane blisko siebie. Możemy tą właściwość wykorzystać i zastosować wskaźniki. Postaramy się uzyskać identyczny efekt umiejętnie je przesuwając.
Spójrz na przykład:
#include
int main() {
int tab[3] = { 1, 2, 3 };
int *ptr = &tab[0];
printf("tab[0] = %d \n", *ptr);
ptr++;
printf("tab[1] = %d \n", *ptr);
ptr++;
printf("tab[2] = %d \n", *ptr);
return 0;
}
Efekt jest identyczny jak w poprzednim przykładzie, ale kod jest trochę inny. W pierwszej kolejności inicjalizujemy wskaźnik na adres pierwszego elementu tablicy:
int *ptr = &tab[0];
Następnie staramy się odwołać do wartości, która kryje się pod pierwszym elementem tablicy używając operatora * (gwiazdka):
printf("tab[0] = %d \n", *ptr);
W kolejnej linijce przesuwamy nasz wskaźnik o jedno miejsce w prawo. Dzięki takiej operacji wskazujemy na adres kolejnego elementu tablicy, czyli adres elementu kryjącego się pod indeksem 1.
ptr++;
Następne operacje już znasz. Przeanalizuj na spokojnie kod i postaraj się zrozumieć prawdziwą moc wskaźników.
Wskaźniki do funkcji
Wskaźniki do funkcji są zaawansowanym mechanizmem w języku C, który pozwala na przechowywanie adresów funkcji i wywoływanie ich dynamicznie w czasie wykonania programu.
Zanim pokażemy ci, w jaki sposób można zadeklarować wskaźnik do funkcji, przygotujemy wstępny kod z prostymi funkcjami odpowiedzialnymi za sumowanie i odejmowanie liczb całkowitych.
#include
void add(int a, int b) {
printf("Add: %d \n", a + b);
}
void sub(int a, int b) {
printf("Sub: %d \n", a - b);
}
int main(void) {
int a = 4, b = 3;
add(a, b);
sub(a, b);
return 0;
}
Rezultat działania programu jest następujący:
Add: 7
Sub: 1
Ten fragment kodu nie powinien sprawiać ci wiele problemów. Mamy do dyspozycji dwie funkcje: add() oraz sub(). Funkcje wykonują operacje dodawania i odejmowania. Za chwilę będziemy chcieli użyć uniwersalnego wskaźnika do funkcji, który obsłuży obie te funkcje za jednym zamachem. Spójrz na poniższy przykład, który pokaże, jak wskaźniki są potężne:
#include
void add(int a, int b) {
printf("Add: %d \n", a + b);
}
void sub(int a, int b) {
printf("Sub: %d \n", a - b);
}
int main(void) {
int a = 4, b = 3;
void (* operation) (int, int);
operation = add;
operation(a, b);
operation = sub;
operation(a, b);
return 0;
}
Na pierwszy rzut oka kod wydaje się bardzo skomplikowany. Tak naprawdę, kluczowa jest tutaj tylko jedna linijka. Przeanalizujmy deklarację wskaźnika do funkcji i rozłóżmy go na czynniki pierwsze:
void (* operation) (int, int);
Oto opis wskaźnika operation o typie void (* operation) (int, int)
- void oznacza, że wskaźnik wskazuje na funkcję, która nie zwraca żadnej wartości,
- nazwa * operation to nazwa wskaźnika (nie przywiązuj się do niej, nazwę wskaźnika ustala programista - w tym konkretnym przypadku nazwaliśmy ten wskaźnik operation),
- (int, int) oznacza, że funkcja, na którą wskazuje wskaźnik, przyjmuje dwa argumenty typu int.
W skrócie, void (* operation) (int, int) to deklaracja wskaźnika do funkcji, która nie zwraca wartości i przyjmuje dwa argumenty typu int. Możemy użyć takiego wskaźnika do przechowywania adresu konkretnej funkcji i późniejszego wywołania tej funkcji przy jego pomocy.
Jak już mamy zadeklarowany taki wskaźnik to szukamy funkcji, która jest podobna do jego deklaracji. Akurat funkcje sub() i add() spełniają opisane warunki. Obie funkcje nie zwracają wartości (void) oraz przyjmują dwa parametry typu int. Ostatecznie możemy taki wskaźnik ustawić na wybraną funkcję:
operation = add;
Nazwa funkcji jest jej adresem w pamięci, więc możemy taki wskaźnik przypisać do jej nazwy. Teraz nic nie stoi na przeszkodzie, aby wywołać taką funkcją posługując się wskaźnikiem:
operation(a, b);
Wywołanie funkcji za pomocą wskaźnika jest analogiczne ze standardowym wywołaniem funkcji. Zwróć uwagę, że nasz wskaźnik można w każdej chwili “przepiąć” na inną funkcję pasującą do deklaracji typu wskaźnika.
operation = sub;
O to tyle, jeśli chodzi o podstawy użycia wskaźników do funkcji. To bardzo interesujący mechanizm.
Podsumowanie
Wskaźniki w języku C to obszerny temat. Możemy ich używać do pracy z tablicami, funkcjami i nie tylko. W artykule pokazaliśmy tylko część dostępnych możliwości. Jeśli chcesz opanować wskaźniki i wiele innych, praktycznych technik pracy z językiem C, to rozpocznij Ścieżkę Kariery C Developer. Z tą ścieżką poznasz język C od postaw do poziomu średniozaawansowanego. Ścieżka kończy się Egzaminem, który uprawnia cię do posługiwania się Certyfikatem Specjalisty.