C/Wskaźniki - więcej: Różnice pomiędzy wersjami

Usunięta treść Dodana treść
Nie podano opisu zmian
m Update syntaxhighlight tags - remove use of deprecated <source> tags
 
Linia 26:
 
Ponadto, zapis czy odczyt poza przydzielonym obszarem pamięci może prowadzić do nieprzyjemnych skutków takich jak zmiana wartości innych zmiennych czy wręcz natychmiastowe przerwanie programu. Jako przykład można podać ten (błędny) program{{r|err}}:
<sourcesyntaxhighlight lang="C">
#include <stdio.h>
Linia 44:
return 0;
}
</syntaxhighlight>
</source>
Nie można również zapominać, że na niektórych architekturach dane wielobajtowe muszą być odpowiednio wyrównane w pamięci. Np. zmienna dwubajtowa może się znajdować jedynie pod parzystymi adresami. Wówczas, gdybyśmy chcieli adres zmiennej jednobajtowej przypisać wskaźnikowi na zmienną dwubajtową mogłoby dojść do nieprzewidzianych błędów wynikających z próby odczytu niewyrównanej danej.
 
Linia 55:
 
Spójrzmy na przykład:
<sourcesyntaxhighlight lang="C">
int *ptr;
int a[] = {1, 2, 3, 5, 7};
ptr = a; /* to znaczy &a[0] */
</syntaxhighlight>
</source>
 
[[Grafika:Zeigerarithmetik.PNG|thumb|100px|Wskaźnik wskazuje na pierwszą komórkę pamięci]]
Linia 65:
 
Gdy wykonamy:
<sourcesyntaxhighlight lang="C">
ptr += 2;
</syntaxhighlight>
</source>
 
[[Grafika:Zeigerarithmetik2.PNG|thumb|100px|Przesunięcie wskaźnika na kolejne komórki]]
Linia 74:
Wskaźniki można również od siebie odejmować, czego wynikiem jest ''odległość'' dwóch wskazywanych wartości. Odległość zwracana jest jako liczba obiektów danego typu, a nie liczba bajtów. Np.:
 
<sourcesyntaxhighlight lang="C">
int a[] = {1, 2, 3, 5, 7};
int *ptr = a + 2;
int diff = ptr - a; /* diff ma wartość 2 (a nie 2*sizeof(int)) */
</syntaxhighlight>
</source>
 
Wynikiem może być oczywiście liczba ujemna. Operacja jest przydatna do obliczania wielkości tablicy (długości łańcucha znaków) jeżeli mamy wskaźnik na jej pierwszy i ostatni element.
Linia 84:
Operacje arytmetyczne na wskaźnikach mają pewne ograniczenia. Przede wszystkim nie można (tzn. standard tego nie definiuje) skonstruować wskaźnika wskazującego gdzieś poza zadeklarowaną tablicę, chyba, że jest to obiekt zaraz za ostatnim, np.:
 
<sourcesyntaxhighlight lang="C">
int a[] = {1, 2, 3, 5, 7};
int *ptr;
Linia 91:
ptr = a + 5; /* zdefiniowane (element za ostatnim) */
*ptr = 10; /* to już nie! */
</syntaxhighlight>
</source>
 
Nie można {{r|undefined behavior}} również odejmować od siebie wskaźników wskazujących na obiekty znajdujące się w różnych tablicach, np.:
 
<sourcesyntaxhighlight lang="C">
int a[] = {1, 2, 3}, b[] = {5, 7};
int *ptr1 = a, *ptr2 = b;
int diff = a - b; /* niezdefiniowane */
</syntaxhighlight>
</source>
 
== Tablice a wskaźniki ==
Linia 105:
 
Na przykład tablica:
<sourcesyntaxhighlight lang="C">
int tab[] = {100,200,300};
</syntaxhighlight>
</source>
 
występuje w pamięci w sześciu komórkach{{r|little endian}}:
Linia 116:
+--------+--------+--------+--------+--------+--------+
Stąd do trzeciej wartości można się dostać tak <small>(komórki w tablicy numeruje się od zera)</small>:
<sourcesyntaxhighlight lang="C">
zmienna = tab[2];
</syntaxhighlight>
</source>
 
albo wykorzystując metodę wskaźnikową:
<sourcesyntaxhighlight lang="C">
zmienna = *(tab + 2);
</syntaxhighlight>
</source>
 
Z definicji obie te metody są równoważne.
Linia 132:
 
Co więcej, można pójść w drugą stronę i potraktować wskaźnik jak tablicę:
<sourcesyntaxhighlight lang="C">
int *wskaznik;
wskaznik = tab + 1;
/* lub wskaznik = &tab[1]; */
zmienna = wskaznik[1]; /* przypisze 300 */
</syntaxhighlight>
</source>
 
Jako ciekawostkę podamy, iż w języku C można odnosić się do elementów tablicy jeszcze w inny sposób:
<sourcesyntaxhighlight lang="C">
printf ("%d\n", 1[tab]);
</syntaxhighlight>
</source>
 
Skąd ta dziwna notacja? Uzasadnienie jest proste:
<sourcesyntaxhighlight lang="C">
tab[1] = *(tab + 1) = *(1 + tab) = 1[tab]
</syntaxhighlight>
</source>
 
Podobną składnię stosuje m.in. asembler GNU.
 
Należy uważać, aby nie odwoływać się do komórek poza przydzieloną pamięcią, np.:
<sourcesyntaxhighlight lang="C">
int tab[] = { 0, 1, 2 };
tab[3] = 3; /* Błąd off by one */
</syntaxhighlight>
</source>
 
== Argument funkcji jako wskaźnik na tablicę ==
Poniższy program obrazuje przekazywanie adresu tablicy do funkcji oczekującej wskaźnika:
<sourcesyntaxhighlight lang="c">
#include <stdio.h>
void func (int *tablica)
Linia 172:
return 0;
}
</syntaxhighlight>
</source>
 
{{Uwaga|Zwróćmy uwagę na wywołanie <tt>func(tablica '''<big>+</big>''' 4)</tt>. Należy pamiętać, by do funkcji przekazać adres elementu a nie sam element. Robi się to inaczej niż w przypadku pojedynczych zmiennych - używamy operatora dodawania '''+'''. Jak on działa dla tablic? Pobierany jest adres pierwszego elementu i jest zwiększany na odpowiedni indeks. Tak więc by uzyskać adres pierwszego elementu tablicy wystarczy napisać po prostu <tt>tablica</tt>.}}
Linia 183:
 
Poniższy kod jest poprawny zarówno w C jak i w C++:
<sourcesyntaxhighlight lang="c">
void* wskaznik = malloc(sizeof *wskaznik * 10);
</syntaxhighlight>
</source>
{{Uwaga|W C++ przy odwoływaniu się do tego wskaźnika należy użyć rzutowania.}}
 
== Na co wskazuje NULL? ==
Analizując kody źródłowe programów, często można spotkać taki oto zapis:
<sourcesyntaxhighlight lang="C">
void *wskaznik = NULL;
/* lub void *wskaznik = 0 */
</syntaxhighlight>
</source>
 
Wiesz już, że nie możemy odwołać się pod komórkę pamięci wskazywaną przez wskaźnik NULL. Po co zatem przypisywać wskaźnikowi 0? Odpowiedź może być zaskakująca: właśnie po to, aby uniknąć błędów! Wydaje się to zabawne, ale większość (jeśli nie wszystkie) funkcji, które zwracają wskaźnik, w przypadku błędu zwróci właśnie NULL, czyli zero. Tutaj rodzi się kolejna wskazówka: jeśli w danej zmiennej przechowujemy wskaźnik, zwrócony wcześniej przez jakąś funkcję zawsze sprawdzajmy, czy nie jest on równy 0 (NULL). Wtedy mamy pewność, że funkcja zadziałała poprawnie.
Linia 200:
 
Warto zauważyć, że pomimo przypisywania wskaźnikowi zera, nie oznacza to, że wskaźnik NULL jest reprezentowany przez same zerowe bity. Co więcej, wskaźniki NULL różnych typów mogą mieć różną wartość! Z tego powodu poniższy kod jest niepoprawny:
<sourcesyntaxhighlight lang="C">
int **tablica_wskaznikow = calloc(100, sizeof *tablica_wskaznikow);
</syntaxhighlight>
</source>
 
Zakłada on, że w reprezentacji wskaźnika NULL występują same zera. Poprawnym zainicjowaniem dynamicznej tablicy wskaźników wartościami NULL jest (pomijamy sprawdzanie wartości zwróconej przez malloc()):
<sourcesyntaxhighlight lang="C">
int **tablica_wskaznikow = malloc(100 * sizeof *tablica_wskaznikow);
int i = 0;
while (i<100)
tablica_wskaznikow[i++] = 0;
</syntaxhighlight>
</source>
 
== Tablice wielowymiarowe ==
Linia 216:
 
W rozdziale [[../Tablice/]] pokazaliśmy, jak tworzyć tablice wielowymiarowe, gdy ich rozmiar jest znany w czasie kompilacji. Teraz zaprezentujemy, jak to wykonać za pomocą wskaźników<ref>[http://patrikstas.com/2017/06/14/allocating-arrays-of-function-pointers-in-c/ allocating-arrays-of-function-pointers-in-c]</ref> i to w sytuacji, gdy rozmiar może być dowolny. Załóżmy, że chcemy stworzyć tabliczkę mnożenia - utworzymy do tego celu tablicę dwuwymiarową, w której oba wymiary będą miały ten sam rozmiar, pobrany od użytkownika:
<sourcesyntaxhighlight lang="C">
int rozmiar, i;
printf("Podaj rozmiar tabliczki mnozenia: ");
Linia 234:
tabliczka = NULL;
</syntaxhighlight>
</source>
 
Przydzielanie pamięci wygląda następująco: najpierw dla pierwszego wymiaru tablicy, który jest zarazem "tablicą tablic" (1) - tak jak na rysunku, jest to tablica przechowywująca wskaźniki do szeregu kolejnych tablic, w których przechowywane są już liczby. W drugim kroku (2) przydzielamy pamięć wszystkim tym tablicom. Tablica jest typu int* (wskaźnik na pierwszy element tablicy), natomiast tablica tablic jest typu int** (wskaźnik na pierwszy element, którym jest wskaźnik na tablicę). Jako że malloc jako argument przyjmuje rozmiar pamięci w bajtach, posługujemy się konstrukcją sizeof element. Pobiera ona typ i zwraca jego rozmiar, w naszym przypadku - (*tabliczka) oznacza to samo, co tabliczka[0], więc jest pierwszą podtablicą i ma typ int*. Jest to typ wskaźnikowy, dlatego sizeof zwróci ilość bajtów jakie wymaga wskaźnik. Drugim razem używamy jako argumentu (**tabliczka), co odpowiada użyciu tabliczka[0][0] (pierwsza liczba w pierwszej tablicy), które to ma już typ int - sizeof zwróci ilość bajtów odpowiadających zmiennej liczbowej. Dla systemu x86 dla obu sizeof powinniśmy otrzymać wielkość 4 bajty, jednak może to się zmienić dla różnych kompilatorów, a tym bardziej dla systemu x64.<br />
Linia 240:
 
Możemy również symulować tablicę dwuwymiarową za pomocą tablicy jednowymiarowej:
<sourcesyntaxhighlight lang="c">
int *tabliczka = (int *)malloc(rozmiar * rozmiar * sizeof (*tabliczka));
</syntaxhighlight>
</source>
W tym przypadku alokujemy pamięć równą liczbie elementów tablicy dwuwymiarowej, jednak w tablicy jednowymiarowej. Na przykład, dla rozmiaru równego 5 zaalokujemy tablicę jednowymiarową z 25 elementami. W ten sposób wszystkie elementy tablicy znajdą się w pamięci obok siebie, jednak utrudnia to programiście dostęp do nich, a także operacje na nich (potrzebna jest do tego [[#Arytmetyka wskaźników|Arytmetyka wskaźników]]).
 
Linia 252:
lub tablicy o dowolnym innym rozkładzie długości wierszy, np.:
 
<sourcesyntaxhighlight lang="C">
const size_t wymiary[] = { 2, 4, 6, 8, 1, 3, 5, 7, 9 };
const size_t ilosc_podtablic = sizeof (wymiary) / sizeof (*wymiary);
Linia 260:
tablica[i] = malloc(wymiary[i] * sizeof **tablica);
}
</syntaxhighlight>
</source>
 
Gdy nabierzesz wprawy w używaniu wskaźników oraz innych funkcji malloc i realloc, nauczysz się wykonywać różne inne operacje, takie jak dodawanie kolejnych wierszy, usuwanie wierszy, zmiana rozmiaru wierszy, zamiana wierszy miejscami itp.
Linia 274:
 
Oczywiście argumentów może być więcej (albo też w ogóle może ich nie być). Oto przykład wykorzystania wskaźnika na funkcję:
<sourcesyntaxhighlight lang="C">
#include <stdio.h>
Linia 289:
return 0;
}
</syntaxhighlight>
</source>
 
Zwróćmy uwagę na dwie rzeczy:
Linia 297:
=== Do czego można użyć wskaźników na funkcje? ===
Język C jest językiem strukturalnym, jednak dzięki wskaźnikom istnieje w nim możliwość "zaszczepienia" pewnych obiektowych właściwości. Wskaźnik na funkcję może być np. elementem struktury - wtedy mamy bardzo prymitywną namiastkę [[C++/Czym jest obiekt|klasy]], którą dobrze znają programiści, piszący w języku [[C++]]. Ponadto dzięki wskaźnikom możemy tworzyć mechanizmy działające na zasadzie funkcji zwrotnej<ref>Funkcje zwrotne znalazły zastosowanie głównie w programowaniu [[w:GUI|GUI]]</ref>. Dobrym przykładem może być np. tworzenie sterowników, gdzie musimy poinformować różne podsystemy, jakie funkcje w naszym kodzie służą do wykonywania określonych czynności. Przykład:
<sourcesyntaxhighlight lang="C">
struct urzadzenie {
int (*otworz)(void);
Linia 328:
return moje_urzadzenie;
}
</syntaxhighlight>
</source>
 
W ten sposób w pamięci każda ''klasa'' musi przechowywać wszystkie wskaźniki do wszystkich ''metod''. Innym rozwiązaniem może być stworzenie statycznej struktury ze wskaźnikami do funkcji i wówczas w strukturze będzie przechowywany jedynie wskaźnik do tej struktury, np.:
<sourcesyntaxhighlight lang="C">
struct urzadzenie_metody {
int (*otworz)(void);
Linia 372:
return moje_urzadzenie;
}
</syntaxhighlight>
</source>
 
== Ciekawostki ==
* w rozdziale [[Programowanie:C:Zmienne#Stałe|Zmienne]] pisaliśmy o stałych. Normalnie nie mamy możliwości zmiany ich wartości, ale z użyciem wskaźników staje się to możliwe:
 
<sourcesyntaxhighlight lang="C">
const int CONST = 0;
int *c = &CONST;
*c = 1;
printf("%i\n", CONST); /* wypisuje 1 */
</syntaxhighlight>
</source>
 
Konstrukcja taka może jednak wywołać ostrzeżenie kompilatora bądź nawet jego błąd - wtedy może pomóc jawne rzutowanie z <code>const int*</code> na <code>int*</code>.