C Language
Wskaźniki
Szukaj…
Wprowadzenie
Wskaźnik jest rodzajem zmiennej, która może przechowywać adres innego obiektu lub funkcji.
Składnia
- <Typ danych> * <Nazwa zmiennej>;
- int * ptrToInt;
- void * ptrToVoid; / * C89 + * /
- struct someStruct * ptrToStruct;
- int ** ptrToPtrToInt;
- int arr [długość]; int * ptrToFirstElem = arr; / * Dla <C99 „długość” musi być stałą czasową kompilacji, dla> = C11 może to być jedna. * /
- int * arrayOfPtrsToInt [length]; / * Dla <C99 „długość” musi być stałą czasową kompilacji, dla> = C11 może to być jedna. * /
Uwagi
Pozycja gwiazdki nie wpływa na znaczenie definicji:
/* The * operator binds to right and therefore these are all equivalent. */
int *i;
int * i;
int* i;
Jednak podczas definiowania wielu wskaźników jednocześnie każdy z nich wymaga własnej gwiazdki:
int *i, *j; /* i and j are both pointers */
int* i, j; /* i is a pointer, but j is an int not a pointer variable */
Możliwa jest również tablica wskaźników, gdzie przed nazwą zmiennej tablicowej znajduje się gwiazdka:
int *foo[2]; /* foo is a array of pointers, can be accessed as *foo[0] and *foo[1] */
Powszechne błędy
Niewłaściwe użycie wskaźników jest często źródłem błędów, które mogą obejmować błędy bezpieczeństwa lub awarie programów, najczęściej z powodu błędów segmentacji.
Nie sprawdzanie błędów alokacji
Przydział pamięci nie jest gwarantowany i może zamiast tego zwrócić wskaźnik NULL
. Korzystanie ze zwróconej wartości bez sprawdzania, czy alokacja się powiodła, wywołuje niezdefiniowane zachowanie . Zwykle prowadzi to do awarii, ale nie ma gwarancji, że nastąpi awaria, więc poleganie na tym może również prowadzić do problemów.
Na przykład niebezpieczny sposób:
struct SomeStruct *s = malloc(sizeof *s);
s->someValue = 0; /* UNSAFE, because s might be a null pointer */
Bezpieczna droga:
struct SomeStruct *s = malloc(sizeof *s);
if (s)
{
s->someValue = 0; /* This is safe, we have checked that s is valid */
}
Używanie literowych liczb zamiast sizeof przy żądaniu pamięci
Dla danej konfiguracji kompilatora / maszyny typy mają znany rozmiar; nie ma jednak żadnego standardu, który określałby, że rozmiar danego typu (innego niż char
) będzie taki sam dla wszystkich konfiguracji kompilatora / maszyny. Jeśli kod używa 4 zamiast sizeof(int)
do alokacji pamięci, może działać na oryginalnej maszynie, ale kod niekoniecznie jest przenośny na inne maszyny lub kompilatory. Stałe rozmiary dla typów powinny być zastąpione przez sizeof(that_type)
lub sizeof(*var_ptr_to_that_type)
.
Przydział nieprzenośny:
int *intPtr = malloc(4*1000); /* allocating storage for 1000 int */
long *longPtr = malloc(8*1000); /* allocating storage for 1000 long */
Przenośny przydział:
int *intPtr = malloc(sizeof(int)*1000); /* allocating storage for 1000 int */
long *longPtr = malloc(sizeof(long)*1000); /* allocating storage for 1000 long */
Lub jeszcze lepiej:
int *intPtr = malloc(sizeof(*intPtr)*1000); /* allocating storage for 1000 int */
long *longPtr = malloc(sizeof(*longPtr)*1000); /* allocating storage for 1000 long */
Wycieki pamięci
Nieprzydzielenie pamięci przy użyciu free
prowadzi do nagromadzenia pamięci, której nie można ponownie użyć, która nie jest już używana przez program; nazywa się to wyciekiem pamięci . Wycieki pamięci marnują zasoby pamięci i mogą prowadzić do błędów alokacji.
Błędy logiczne
Wszystkie alokacje muszą być zgodne z tym samym schematem:
- Przydział za pomocą
malloc
(lubcalloc
) - Wykorzystanie do przechowywania danych
- Cofnięcie przydziału za
free
Niezastosowanie się do tego wzorca, na przykład użycie pamięci po wywołaniu free
( zwisający wskaźnik ) lub przed wezwaniem do malloc
( dziki wskaźnik ), wywołanie free
dwukrotnie („double free”) itp., Zwykle powoduje błąd segmentacji i powoduje awarię programu.
Błędy te mogą być przejściowe i trudne do debugowania - na przykład zwolniona pamięć zwykle nie jest natychmiast odzyskiwana przez system operacyjny, dlatego zwisające wskaźniki mogą utrzymywać się przez chwilę i sprawiać wrażenie, jakby działały.
W systemach, w których działa, Valgrind jest nieocenionym narzędziem do identyfikowania wycieku pamięci i miejsca, w którym została pierwotnie przydzielona.
Tworzenie wskaźników do zestawiania zmiennych
Utworzenie wskaźnika nie wydłuża żywotności wskazywanej zmiennej. Na przykład:
int* myFunction()
{
int x = 10;
return &x;
}
Tutaj x
ma automatyczny czas przechowywania (powszechnie znany jako alokacja stosu ). Ponieważ jest on przydzielony na stosie, jego żywotność trwa tylko tak długo, jak długo działa myFunction
; po wyjściu myFunction
zmienna x
zostaje zniszczona. Ta funkcja pobiera adres x
(używając &x
) i zwraca go do wywołującego, pozostawiając wywołującego wskaźnik do nieistniejącej zmiennej. Próba uzyskania dostępu do tej zmiennej wywoła wówczas niezdefiniowane zachowanie .
Większość kompilatorów tak naprawdę nie usuwa ramki stosu po wyjściu z funkcji, dlatego odłożenie zwróconego wskaźnika często daje oczekiwane dane. Jednak po wywołaniu innej funkcji wskazana pamięć może zostać nadpisana i wygląda na to, że wskazane dane zostały uszkodzone.
Aby rozwiązać ten problem, albo malloc
pamięć dla zmiennej, która ma zostać zwrócona, i zwróć wskaźnik do nowo utworzonej pamięci, albo wymagaj, aby poprawny wskaźnik został przekazany do funkcji zamiast zwracać jeden, na przykład:
#include <stdlib.h>
#include <stdio.h>
int *solution1(void)
{
int *x = malloc(sizeof *x);
if (x == NULL)
{
/* Something went wrong */
return NULL;
}
*x = 10;
return x;
}
void solution2(int *x)
{
/* NB: calling this function with an invalid or null pointer
causes undefined behaviour. */
*x = 10;
}
int main(void)
{
{
/* Use solution1() */
int *foo = solution1();
if (foo == NULL)
{
/* Something went wrong */
return 1;
}
printf("The value set by solution1() is %i\n", *foo);
/* Will output: "The value set by solution1() is 10" */
free(foo); /* Tidy up */
}
{
/* Use solution2() */
int bar;
solution2(&bar);
printf("The value set by solution2() is %i\n", bar);
/* Will output: "The value set by solution2() is 10" */
}
return 0;
}
Zwiększanie / zmniejszanie i dereferencje
Jeśli piszesz *p++
aby zwiększyć to, na co wskazuje p
, jesteś w błędzie.
Po inkrementacji / dekrementacji jest wykonywane przed dereferencją. Dlatego to wyrażenie zwiększy sam wskaźnik p
i zwróci to, na co wskazywał p
przed zwiększeniem bez zmiany.
Powinieneś napisać (*p)++
aby zwiększyć to, na co wskazuje p
.
Zasada ta odnosi się również do posta zmniejszanie: *p--
będzie zmniejszać się wskaźnik p
sama, co nie jest skierowany przez p
.
Dereferencje wskaźnika
int a = 1;
int *a_pointer = &a;
Aby a_pointer
i zmienić wartość a, używamy następującej operacji
*a_pointer = 2;
Można to zweryfikować za pomocą następujących instrukcji drukowania.
printf("%d\n", a); /* Prints 2 */
printf("%d\n", *a_pointer); /* Also prints 2 */
Jednak błędem byłoby zlekceważenie wskaźnika NULL
lub innego nieprawidłowego wskaźnika. To
int *p1, *p2;
p1 = (int *) 0xbad;
p2 = NULL;
*p1 = 42;
*p2 = *p1 + 1;
jest zwykle nieokreślonym zachowaniem . p1
może nie być dereferencyjny, ponieważ wskazuje na adres 0xbad
który może nie być prawidłowym adresem. Kto wie co tam jest? Może to być pamięć systemu operacyjnego lub pamięć innego programu. Jedynym takim kodem czasowym, jaki jest używany, jest wbudowane programowanie, które przechowuje określone informacje pod zakodowanymi adresami. p2
nie może być odwołany, ponieważ ma NULL
, co jest nieprawidłowe.
Dereferencje wskaźnika do struktury
Powiedzmy, że mamy następującą strukturę:
struct MY_STRUCT
{
int my_int;
float my_float;
};
Możemy zdefiniować MY_STRUCT
aby pominąć słowo kluczowe struct
więc nie musimy wpisywać struct MY_STRUCT
każdym razem, gdy go używamy. Jest to jednak opcjonalne.
typedef struct MY_STRUCT MY_STRUCT;
Jeśli mamy wskaźnik do wystąpienia tej struktury
MY_STRUCT *instance;
Jeśli ta instrukcja pojawi się w zakresie pliku, instance
zostanie zainicjowana wskaźnikiem zerowym podczas uruchamiania programu. Jeśli to polecenie pojawia się w funkcji, jej wartość jest niezdefiniowana. Zmienna musi zostać zainicjowana, aby wskazywała prawidłową zmienną MY_STRUCT
lub dynamicznie przydzielaną przestrzeń, zanim będzie można ją odrzucić. Na przykład:
MY_STRUCT info = { 1, 3.141593F };
MY_STRUCT *instance = &info;
Gdy wskaźnik jest poprawny, możemy wyrejestrować go, aby uzyskać dostęp do jego członków, używając jednej z dwóch różnych notacji:
int a = (*instance).my_int;
float b = instance->my_float;
Chociaż obie te metody działają, lepiej jest używać operatora strzałki ->
niż kombinacji nawiasów, operatora dereferencji *
i kropki .
operatora, ponieważ jest łatwiejszy do odczytania i zrozumienia, zwłaszcza w przypadku zagnieżdżonych zastosowań.
Kolejna ważna różnica jest pokazana poniżej:
MY_STRUCT copy = *instance;
copy.my_int = 2;
W takim przypadku copy
zawiera kopię zawartości instance
. Zmiana my_int
copy
nie zmieni jej na instance
.
MY_STRUCT *ref = instance;
ref->my_int = 2;
W takim przypadku ref
jest odniesieniem do instance
. Zmiana my_int
za pomocą odwołania zmieni go na instance
.
Powszechną praktyką jest używanie wskaźników do struktur jako parametrów funkcji, a nie samych struktur. Użycie struktur jako parametrów funkcji może spowodować przepełnienie stosu, jeśli struktura jest duża. Użycie wskaźnika do struktury wykorzystuje tylko wystarczającą przestrzeń stosu dla wskaźnika, ale może powodować skutki uboczne, jeśli funkcja zmieni strukturę, która jest przekazywana do funkcji.
Wskaźniki funkcji
Wskaźniki mogą być również używane do wskazywania funkcji.
Weźmy podstawową funkcję:
int my_function(int a, int b) { return 2 * a + 3 * b; }
Teraz zdefiniujmy wskaźnik typu tej funkcji:
int (*my_pointer)(int, int);
Aby go utworzyć, użyj tego szablonu:
return_type_of_func (*my_func_pointer)(type_arg1, type_arg2, ...)
Następnie musimy przypisać ten wskaźnik do funkcji:
my_pointer = &my_function;
Tego wskaźnika można teraz użyć do wywołania funkcji:
/* Calling the pointed function */ int result = (*my_pointer)(4, 2); ... /* Using the function pointer as an argument to another function */ void another_function(int (*another_pointer)(int, int)) { int a = 4; int b = 2; int result = (*another_pointer)(a, b); printf("%d\n", result); }
Chociaż ta składnia wydaje się bardziej naturalna i spójna z podstawowymi typami, wskaźniki funkcji przypisywania i dereferencji nie wymagają użycia operatorów &
i *
. Poniższy fragment kodu jest jednakowo ważny:
/* Attribution without the & operator */ my_pointer = my_function; /* Dereferencing without the * operator */ int result = my_pointer(4, 2);
Aby zwiększyć czytelność wskaźników funkcji, można zastosować typedefs.
typedef void (*Callback)(int a); void some_function(Callback callback) { int a = 4; callback(a); }
Inną sztuczką w zakresie czytelności jest to, że standard C pozwala uprościć wskaźnik funkcji w argumentach takich jak powyżej (ale nie w deklaracji zmiennej) do czegoś, co wygląda jak prototyp funkcji; dlatego w definicjach funkcji i deklaracjach można w sposób równoważny zastosować:
void some_function(void callback(int))
{
int a = 4;
callback(a);
}
Zobacz też
Inicjowanie wskaźników
Inicjalizacja wskaźnika jest dobrym sposobem na uniknięcie dzikich wskaźników. Inicjalizacja jest prosta i nie różni się niczym od inicjalizacji zmiennej.
#include <stddef.h>
int main()
{
int *p1 = NULL;
char *p2 = NULL;
float *p3 = NULL;
/* NULL is a macro defined in stddef.h, stdio.h, stdlib.h, and string.h */
...
}
W większości systemów operacyjnych nieumyślne użycie wskaźnika, który został zainicjowany na NULL
, często powoduje natychmiastowe zawieszenie programu, co ułatwia identyfikację przyczyny problemu. Używanie niezainicjowanego wskaźnika może często powodować trudne do zdiagnozowania błędy.
Uwaga:
Wynik dereferencji wskaźnika NULL
jest niezdefiniowany, więc niekoniecznie spowoduje awarię, nawet jeśli jest to naturalne zachowanie systemu operacyjnego, na którym działa program. Optymalizacje kompilatora mogą maskować awarię, powodować awarię przed lub po punkcie w kodzie źródłowym, w którym wystąpiło dereferencje wskaźnika zerowego, lub spowodować, że części kodu zawierające dereferencję wskaźnika zerowego zostaną nieoczekiwanie usunięte z programu. Kompilacje debugowania zwykle nie wykazują takich zachowań, ale nie gwarantuje tego standard językowy. Inne nieoczekiwane i / lub niepożądane zachowania są również dozwolone.
Ponieważ NULL
nigdy nie wskazuje zmiennej, przydzielonej pamięci ani funkcji, można bezpiecznie używać jej jako wartości zabezpieczającej.
Uwaga:
Zwykle NULL
jest definiowana jako (void *)0
. Ale to nie oznacza, że przypisany adres pamięci to 0x0
. Aby uzyskać więcej wyjaśnień, patrz C-faq dla wskaźników NULL
Pamiętaj, że możesz także zainicjować wskaźniki, aby zawierały wartości inne niż NULL.
int i1;
int main()
{
int *p1 = &i1;
const char *p2 = "A constant string to point to";
float *p3 = malloc(10 * sizeof(float));
}
Adres operatora (i)
Dla dowolnego obiektu (tj. Zmiennej, tablicy, unii, struktury, wskaźnika lub funkcji) operator adresu jednoargumentowego może być użyty do uzyskania dostępu do adresu tego obiektu.
Przypuszczam, że
int i = 1;
int *p = NULL;
Zatem instrukcja p = &i;
, kopiuje adres zmiennej i
do wskaźnika p
.
Jest wyrażony jako p
punktów do i
.
printf("%d\n", *p);
drukuje 1, co jest wartością i
.
Wskaźnik arytmetyczny
Zobacz tutaj: Pointer Arithmetic
void * wskaźniki jako argumenty i zwracają wartości do standardowych funkcji
void*
to catch all type dla wskaźników do typów obiektów. Przykładem tego jest użycie funkcji malloc
, która jest zadeklarowana jako
void* malloc(size_t);
Typ zwrotu wskaźnik-do-pustki oznacza, że możliwe jest przypisanie wartości zwrotu z malloc
do wskaźnika do dowolnego innego typu obiektu:
int* vector = malloc(10 * sizeof *vector);
Powszechnie uważa się za dobrą praktykę nie rzucania wartościami w puste wskaźniki i poza nie. W konkretnym przypadku malloc()
dzieje się tak, ponieważ w przypadku jawnej rzutowania kompilator może w przeciwnym razie założyć, ale nie ostrzegać o niepoprawnym typie zwrotu dla malloc()
, jeśli zapomnisz dołączyć stdlib.h
. Jest to również przypadek zastosowania prawidłowego zachowania wskaźników pustki, aby lepiej dostosować się do zasady OSUSZANIA (nie powtarzaj się); porównaj powyższe z poniższymi, w których poniższy kod zawiera kilka niepotrzebnych dodatkowych miejsc, w których literówka może powodować problemy:
int* vector = (int*)malloc(10 * sizeof int*);
Podobnie funkcje takie jak
void* memcpy(void *restrict target, void const *restrict source, size_t size);
ich argumenty są określone jako void *
ponieważ można przekazać adres dowolnego obiektu, niezależnie od typu. Również tutaj wywołanie nie powinno używać rzutowania
unsigned char buffer[sizeof(int)];
int b = 67;
memcpy(buffer, &b, sizeof buffer);
Const Pointers
Pojedyncze wskaźniki
Wskaźnik do
int
Wskaźnik może wskazywać na różnych liczb całkowitych, a
int
„s mogą być zmieniane przez wskaźnik. Ta próbka kodu przypisuje b do punktuint b
a następnie zmienia wartośćb
na100
.int b; int* p; p = &b; /* OK */ *p = 100; /* OK */
Wskaźnik do
const int
Wskaźnik może wskazywać różne liczby całkowite, ale wartości
int
nie można zmienić za pomocą wskaźnika.int b; const int* p; p = &b; /* OK */ *p = 100; /* Compiler Error */
const
wskaźnik doint
Wskaźnik może wskazywać tylko jeden
int
ale wartośćint
można zmienić za pomocą wskaźnika.int a, b; int* const p = &b; /* OK as initialisation, no assignment */ *p = 100; /* OK */ p = &a; /* Compiler Error */
const
wskaźnik doconst int
Wskaźnik może wskazywać tylko jeden
int
aint
nie może być zmieniany za pomocą wskaźnika.int a, b; const int* const p = &b; /* OK as initialisation, no assignment */ p = &a; /* Compiler Error */ *p = 100; /* Compiler Error */
Wskaźnik do wskaźnika
Wskaźnik do wskaźnika do
int
Ten kod przypisuje adres
p1
podwójnemu wskaźnikowip
(który następnie wskazuje naint* p1
(który wskazuje naint
)).Następnie zmienia
p1
aby wskazywać naint a
. Następnie zmienia wartość a na 100.void f1(void) { int a, b; int *p1; int **p; p1 = &b; /* OK */ p = &p1; /* OK */ *p = &a; /* OK */ **p = 100; /* OK */ }
Wskaźnik do wskaźnika do
const int
void f2(void) { int b; const int *p1; const int **p; p = &p1; /* OK */ *p = &b; /* OK */ **p = 100; /* error: assignment of read-only location ‘**p’ */ }
Wskaźnik do
const
wskaźnik doint
void f3(void) { int b; int *p1; int * const *p; p = &p1; /* OK */ *p = &b; /* error: assignment of read-only location ‘*p’ */ **p = 100; /* OK */ }
const
wskaźnik do wskaźnika doint
void f4(void) { int b; int *p1; int ** const p = &p1; /* OK as initialisation, not assignment */ p = &p1; /* error: assignment of read-only variable ‘p’ */ *p = &b; /* OK */ **p = 100; /* OK */ }
Wskaźnik do
const
wskaźnik doconst int
void f5(void) { int b; const int *p1; const int * const *p; p = &p1; /* OK */ *p = &b; /* error: assignment of read-only location ‘*p’ */ **p = 100; /* error: assignment of read-only location ‘**p’ */ }
const
wskaźnik do wskaźnika doconst int
void f6(void) { int b; const int *p1; const int ** const p = &p1; /* OK as initialisation, not assignment */ p = &p1; /* error: assignment of read-only variable ‘p’ */ *p = &b; /* OK */ **p = 100; /* error: assignment of read-only location ‘**p’ */ }
const
wskaźnik doconst
wskaźnik doint
void f7(void) { int b; int *p1; int * const * const p = &p1; /* OK as initialisation, not assignment */ p = &p1; /* error: assignment of read-only variable ‘p’ */ *p = &b; /* error: assignment of read-only location ‘*p’ */ **p = 100; /* OK */ }
Ta sama gwiazdka, różne znaczenia
Przesłanka
Najbardziej mylącą rzeczą dotyczącą składni wskaźnika w C i C ++ jest to, że istnieją dwa różne znaczenia, które stosuje się, gdy symbol wskaźnika, gwiazdka ( *
), jest używany ze zmienną.
Przykład
Po pierwsze, używasz *
aby zadeklarować zmienną wskaźnika.
int i = 5;
/* 'p' is a pointer to an integer, initialized as NULL */
int *p = NULL;
/* '&i' evaluates into address of 'i', which then assigned to 'p' */
p = &i;
/* 'p' is now holding the address of 'i' */
Kiedy nie deklarujesz (ani nie powielasz), *
służy do wyłuskiwania zmiennej wskaźnikowej:
*p = 123;
/* 'p' was pointing to 'i', so this changes value of 'i' to 123 */
Jeśli chcesz, aby istniejąca zmienna wskaźnikowa przechowywała adres innej zmiennej, nie używasz *
, ale postępuj w ten sposób:
p = &another_variable;
Powszechne zamieszanie wśród początkujących programistów C pojawia się, gdy deklarują i inicjują zmienną wskaźnika w tym samym czasie.
int *p = &i;
Ponieważ int i = 5;
i int i; i = 5;
dać ten sam wynik, niektóre z nich mogą pomyśleć int *p = &i;
i int *p; *p = &i;
dać również ten sam wynik. Faktem jest, nie, int *p; *p = &i;
spróbuje uszanować niezainicjowany wskaźnik, co spowoduje UB. Nigdy nie używaj *
gdy nie deklarujesz wskaźnika ani nie odsuwasz wskaźnika.
Wniosek
Gwiazdka ( *
) ma dwa różne znaczenia w obrębie C w odniesieniu do wskaźników, w zależności od tego, gdzie jest używana. W przypadku użycia w deklaracji zmiennej wartość po prawej stronie strony równej powinna być wartością wskaźnika do adresu w pamięci. W przypadku użycia z już zadeklarowaną zmienną gwiazdka odrzuci wartość wskaźnika, podążając za wskazanym miejscem w pamięci i umożliwiając przypisanie lub odzyskanie przechowywanej tam wartości.
Na wynos
Ważne jest, aby mieć na uwadze swoje P i Q, że tak powiem, w przypadku wskaźników. Pamiętaj o tym, kiedy używasz gwiazdki i co to znaczy, kiedy go używasz. Przeoczenie tego drobnego szczegółu może spowodować błędne i / lub niezdefiniowane zachowanie, z którym tak naprawdę nie chcesz mieć do czynienia.
Wskaźnik do wskaźnika
W C wskaźnik może odnosić się do innego wskaźnika.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int A = 42;
int* pA = &A;
int** ppA = &pA;
int*** pppA = &ppA;
printf("%d", ***pppA); /* prints 42 */
return EXIT_SUCCESS;
}
Ale bezpośrednie i referencyjne nie są dozwolone.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int A = 42;
int* pA = &A;
int** ppA = &&A; /* Compilation error here! */
int*** pppA = &&&A; /* Compilation error here! */
...
Wprowadzenie
Wskaźnik jest zadeklarowany podobnie jak każda inna zmienna, z tym wyjątkiem, że między typem a nazwą zmiennej umieszczana jest gwiazdka ( *
), co oznacza, że jest wskaźnikiem.
int *pointer; /* inside a function, pointer is uninitialized and doesn't point to any valid object yet */
Aby zadeklarować dwie zmienne wskaźnikowe tego samego typu, w tej samej deklaracji użyj symbolu gwiazdki przed każdym identyfikatorem. Na przykład,
int *iptr1, *iptr2;
int *iptr3, iptr4; /* iptr3 is a pointer variable, whereas iptr4 is misnamed and is an int */
Adres lub operator odniesienia oznaczony znakiem ampersand ( &
) podaje adres danej zmiennej, którą można umieścić we wskaźniku odpowiedniego typu.
int value = 1;
pointer = &value;
Operator pośredni lub dereferencyjny oznaczony gwiazdką ( *
) pobiera zawartość obiektu wskazywanego przez wskaźnik.
printf("Value of pointed to integer: %d\n", *pointer);
/* Value of pointed to integer: 1 */
Jeśli wskaźnik wskazuje na strukturę lub typ unii, możesz ją wyrejestrować i uzyskać bezpośredni dostęp do jej członków za pomocą operatora ->
:
SomeStruct *s = &someObject;
s->someMember = 5; /* Equivalent to (*s).someMember = 5 */
W języku C wskaźnik jest odrębnym typem wartości, który można ponownie przypisać, a poza tym jest traktowany jako zmienna sama w sobie. Na przykład poniższy przykład wypisuje wartość samego wskaźnika (zmiennej).
printf("Value of the pointer itself: %p\n", (void *)pointer);
/* Value of the pointer itself: 0x7ffcd41b06e4 */
/* This address will be different each time the program is executed */
Ponieważ wskaźnik jest zmienną zmienną, możliwe jest, że nie będzie wskazywał poprawnego obiektu, albo przez ustawienie wartości null
pointer = 0; /* or alternatively */
pointer = NULL;
lub po prostu zawierając dowolny wzór bitowy, który nie jest prawidłowym adresem. Ta ostatnia sytuacja jest bardzo zła, ponieważ nie można jej przetestować przed usunięciem wskaźnika, istnieje tylko test na wypadek, gdy wskaźnik jest pusty:
if (!pointer) exit(EXIT_FAILURE);
Wskaźnik można usunąć z dereferencji tylko wtedy, gdy wskazuje na prawidłowy obiekt, w przeciwnym razie zachowanie jest niezdefiniowane. Wiele nowoczesnych implementacji może ci pomóc, podnosząc jakiś błąd, taki jak błąd segmentacji i przerywając wykonywanie, ale inne mogą po prostu pozostawić twój program w nieprawidłowym stanie.
Wartość zwracana przez operator dereferencji jest zmiennym aliasem oryginalnej zmiennej, więc można ją zmienić, modyfikując oryginalną zmienną.
*pointer += 1;
printf("Value of pointed to variable after change: %d\n", *pointer);
/* Value of pointed to variable after change: 2 */
Wskaźniki są również ponownie przypisywane. Oznacza to, że wskaźnik wskazujący obiekt może być później użyty do wskazania innego obiektu tego samego typu.
int value2 = 10;
pointer = &value2;
printf("Value from pointer: %d\n", *pointer);
/* Value from pointer: 10 */
Jak każda inna zmienna, wskaźniki mają określony typ. Nie można na przykład przypisać adresu short int
do wskaźnika do long int
. Takie zachowanie jest określane jako wykreślanie czcionek i jest zabronione w C, choć jest kilka wyjątków.
Chociaż wskaźnik musi być określonego typu, pamięć przydzielona dla każdego typu wskaźnika jest równa pamięci używanej przez środowisko do przechowywania adresów, a nie wielkości wskazanego typu.
#include <stdio.h>
int main(void) {
printf("Size of int pointer: %zu\n", sizeof (int*)); /* size 4 bytes */
printf("Size of int variable: %zu\n", sizeof (int)); /* size 4 bytes */
printf("Size of char pointer: %zu\n", sizeof (char*)); /* size 4 bytes */
printf("Size of char variable: %zu\n", sizeof (char)); /* size 1 bytes */
printf("Size of short pointer: %zu\n", sizeof (short*)); /* size 4 bytes */
printf("Size of short variable: %zu\n", sizeof (short)); /* size 2 bytes */
return 0;
}
(Uwaga: jeśli używasz Microsoft Visual Studio, który nie obsługuje standardów C99 lub C11, musisz użyć %Iu
1 zamiast %zu
w powyższej próbce.)
Zauważ, że powyższe wyniki mogą się różnić w zależności od środowiska w liczbach, ale wszystkie środowiska będą miały takie same rozmiary dla różnych typów wskaźników.
Wyciąg na podstawie informacji z Cardiff University C Pointers Wprowadzenie
Wskaźniki i tablice
Wskaźniki i tablice są ściśle połączone w C. Tablice w C są zawsze przechowywane w ciągłych miejscach w pamięci. Arytmetyka wskaźnika jest zawsze skalowana według wielkości wskazanego elementu. Więc jeśli mamy tablicę trzech podwójnych i wskaźnik do podstawy, *ptr
odnosi się do pierwszego podwójnego, *(ptr + 1)
do drugiego, *(ptr + 2)
do trzeciego. Bardziej wygodną notacją jest użycie notacji tablicowej []
.
double point[3] = {0.0, 1.0, 2.0};
double *ptr = point;
/* prints x 0.0, y 1.0 z 2.0 */
printf("x %f y %f z %f\n", ptr[0], ptr[1], ptr[2]);
Zasadniczo ptr i nazwa tablicy są wymienne. Ta reguła oznacza również, że tablica rozpada się na wskaźnik po przekazaniu do podprogramu.
double point[3] = {0.0, 1.0, 2.0};
printf("length of point is %s\n", length(point));
/* get the distance of a 3D point from the origin */
double length(double *pt)
{
return sqrt(pt[0] * pt[0] + pt[1] * pt[1] + pt[2] * pt[2])
}
Wskaźnik może wskazywać dowolny element w tablicy lub element poza ostatnim elementem. Błędem jest jednak ustawianie wskaźnika na dowolną inną wartość, w tym element przed tablicą. (Powodem jest to, że w architekturach podzielonych na segmenty adres zanim pierwszy element może przekroczyć granicę segmentu, kompilator zapewnia, że nie nastąpi to dla ostatniego elementu plus jeden).
Przypis 1: Informacje o formacie Microsoft można znaleźć za pomocą printf()
i składni specyfikacji formatu .
Zachowanie polimorficzne z pustymi wskaźnikami
Standardowa funkcja biblioteczna qsort()
jest dobrym przykładem tego, w jaki sposób można używać wskaźników pustych, aby pojedyncza funkcja działała na wielu różnych typach.
void qsort (
void *base, /* Array to be sorted */
size_t num, /* Number of elements in array */
size_t size, /* Size in bytes of each element */
int (*compar)(const void *, const void *)); /* Comparison function for two elements */
Tablica do sortowania jest przekazywana jako wskaźnik pustki, więc można operować tablicą dowolnego typu elementu. Kolejne dwa argumenty mówią qsort()
ile elementów powinien się spodziewać w tablicy i jak duży w bajtach jest każdy element.
Ostatni argument jest wskaźnikiem funkcji do funkcji porównawczej, która sama przyjmuje dwa puste wskaźniki. Dzięki temu, że program wywołujący udostępnia tę funkcję, qsort()
może skutecznie sortować elementy dowolnego typu.
Oto przykład takiej funkcji porównawczej do porównywania liczb zmiennoprzecinkowych. Zauważ, że każda funkcja porównania przekazana do qsort()
musi mieć podpis tego typu. Polimorficzny jest sposób, w jaki argumenty pustego wskaźnika są rzutowane na wskaźniki typu elementu, który chcemy porównać.
int compare_floats(const void *a, const void *b)
{
float fa = *((float *)a);
float fb = *((float *)b);
if (fa < fb)
return -1;
if (fa > fb)
return 1;
return 0;
}
Ponieważ wiemy, że qsort użyje tej funkcji do porównania liczb zmiennoprzecinkowych, przed odwołaniem do nich rzutujemy argumenty pustego wskaźnika na wskaźniki liczb zmiennoprzecinkowych.
Teraz użycie funkcji polimorficznej qsort na tablicy „array” o długości „len” jest bardzo proste:
qsort(array, len, sizeof(array[0]), compare_floats);