Wskaźnik (programowanie komputerowe) - Pointer (computer programming)

Uważam, że deklaracje przypisania i zmienne wskaźnikowe należą do „najcenniejszych skarbów informatyki”.

Donald Knuth , Programowanie strukturalne, z przejdź do Oświadczenia

Wskaźnik a wskazujący na adres pamięci związany ze zmienną b . Na tym diagramie architektura obliczeniowa wykorzystuje tę samą przestrzeń adresową i prymityw danych zarówno dla wskaźników, jak i dla wskaźników; taka potrzeba nie powinna mieć miejsca.

W informatyce , wykorzystując wskaźnik jest obiekt w wielu językach programowania , który przechowuje adres pamięci . Może to być wartość innej wartości znajdującej się w pamięci komputera lub w niektórych przypadkach wartość sprzętu komputerowego mapowanego w pamięci . Wskaźnik odwołuje się do lokalizacji w pamięci, a uzyskanie wartości przechowywanej w tej lokalizacji jest znane jako dereferencja wskaźnika. Przez analogię, numer strony w indeksie książki można uznać za wskaźnik do odpowiedniej strony; wyłuskanie takiego wskaźnika odbywałoby się poprzez przerzucenie się na stronę o podanym numerze strony i odczytanie tekstu znajdującego się na tej stronie. Rzeczywisty format i zawartość zmiennej wskaźnikowej zależy od podstawowej architektury komputera .

Korzystanie wskaźniki znacznie poprawia wydajność dla powtarzalnych operacji, jak przejeżdżające iterowalny danych struktur (np struny , lookup tabele , tabele sterujące i drzewa struktury). W szczególności kopiowanie i wyłuskiwanie wskaźników jest często znacznie tańsze w czasie i przestrzeni niż kopiowanie i uzyskiwanie dostępu do danych, na które wskazują wskaźniki.

Wskaźniki są również używane do przechowywania adresów punktów wejścia dla wywoływanych podprogramów w programowaniu proceduralnym i do łączenia w czasie wykonywania z bibliotekami dołączanymi dynamicznie (DLL) . W programowaniu obiektowym , wskaźniki do funkcji są stosowane do wiązania metod , często przy użyciu wirtualnych tabel metody .

Wskaźnik to prosta, bardziej konkretna implementacja bardziej abstrakcyjnego typu danych referencyjnych . Niektóre języki, zwłaszcza języki niskiego poziomu , obsługują pewien rodzaj wskaźnika, chociaż niektóre mają więcej ograniczeń w ich użyciu niż inne. Podczas gdy "wskaźnik" był używany w odniesieniu do odwołań w ogóle, bardziej właściwie odnosi się do struktur danych, których interfejs wyraźnie pozwala na manipulowanie wskaźnikiem (arytmetycznie poprzez arytmetyka wskaźnika ) jako adres pamięci, w przeciwieństwie domagicznego pliku cookielubmożliwości,które na to nie pozwalają. Ponieważ wskaźniki umożliwiają zarówno chroniony, jak i niechroniony dostęp do adresów pamięci, istnieje ryzyko związane z ich używaniem, szczególnie w tym drugim przypadku. Prymitywne wskaźniki są często przechowywane w formacie podobnym doliczby całkowitej; jednak próba wyłuskania lub "szukania" takiego wskaźnika, którego wartość nie jest prawidłowym adresem pamięci, może spowodowaćawarięprogramu(lub zawierać nieprawidłowe dane). Aby złagodzić ten potencjalny problem, ze względu nabezpieczeństwo typówwskaźniki są uważane za oddzielny typ sparametryzowany przez typ danych, na który wskazują, nawet jeśli podstawowa reprezentacja jest liczbą całkowitą. Można również podjąć inne środki (takie jakwalidacjaisprawdzanie granic), aby sprawdzić, czy zmienna wskaźnika zawiera wartość, która jest zarówno prawidłowym adresem pamięci, jak i zakresem liczbowym, który procesor jest w stanie zaadresować.

Historia

W 1955 roku radziecka informatyk Kateryna Juszczenko wynalazła język programowania Adres, który umożliwiał adresowanie pośrednie i adresy najwyższej rangi – analogiczne do wskaźników. Ten język był powszechnie używany na komputerach Związku Radzieckiego. Jednak był nieznany poza Związkiem Radzieckim i zwykle Haroldowi Lawsonowi przypisuje się wynalezienie wskaźnika w 1964 roku. W 2000 roku Lawson otrzymał nagrodę Computer Pioneer Award od IEEE „za wynalezienie zmiennej wskaźnikowej i wprowadzenie tej koncepcji do PL/I, zapewniając w ten sposób po raz pierwszy możliwość elastycznego traktowania połączonych list w ogólnym język wysokiego poziomu". Jego przełomowa praca na temat koncepcji ukazała się w czerwcowym numerze CACM z 1967 roku zatytułowanym: PL/I List Processing. Według Oxford English Dictionary The słowo wskaźnik pierwszy ukazał się drukiem jako wskaźnik stosu w protokole technicznym przez system rozwoju Corporation .

Opis formalny

W informatyce wskaźnik jest rodzajem odniesienia .

Dane prymitywny (lub po prostu prymitywne ) jest dowolny punkt odniesienia, które mogą być odczytywane lub zapisywane do pamięci komputera przy użyciu jednej dostępu do pamięci (na przykład, zarówno bajt i słowo są prymitywy).

Agregat danych (lub tylko agregat ) jest grupa podstaw, które są logicznie ciągły w pamięci, które są oglądane wspólnie jako jeden punkt odniesienia (na przykład, agregat może być 3 logicznie ciągłych bajtach których wartości przedstawiają 3 współrzędne punkt w przestrzeni). Gdy agregat składa się w całości z tego samego typu prymitywu, agregat może być nazwany tablicą ; w pewnym sensie wielobajtowe słowo prymitywne jest tablicą bajtów, a niektóre programy używają słów w ten sposób.

W kontekście tych definicji bajt jest najmniejszym prymitywem; każdy adres pamięci określa inny bajt. Adres pamięci początkowego bajtu układu odniesienia jest uważany za adres pamięci (lub bazowy adres pamięci ) całego układu odniesienia.

Wskaźnik pamięci (lub tylko wskaźnik ) jest prymitywny, którego wartość ma być stosowany jako adres pamięci; mówi się, że wskaźnik wskazuje na adres pamięci . Mówi się również, że wskaźnik wskazuje na datum [w pamięci], gdy wartością wskaźnika jest adres pamięci datum.

Mówiąc bardziej ogólnie, wskaźnik jest rodzajem odniesienia i mówi się, że wskaźnik odwołuje się do układu odniesienia przechowywanego gdzieś w pamięci ; aby uzyskać ten punkt odniesienia, należy wyłuskać wskaźnik . Cechą, która oddziela wskaźniki od innych rodzajów odwołań, jest to, że wartość wskaźnika ma być interpretowana jako adres pamięci, co jest pojęciem raczej niskiego poziomu.

Odwołania służą jako poziom pośredni: wartość wskaźnika określa, który adres pamięci (to znaczy, który punkt odniesienia) ma być użyty w obliczeniach. Ponieważ pośredniość jest podstawowym aspektem algorytmów, wskaźniki są często wyrażane jako podstawowy typ danych w językach programowania ; w statycznie (lub silnie ) typizowanych językach programowania typ wskaźnika określa typ odniesienia, na które wskazuje wskaźnik.

Korzenie architektoniczne

Wskaźniki są bardzo cienką abstrakcją na szczycie możliwości adresowania zapewnianych przez większość nowoczesnych architektur . W najprostszym schemacie adres lub indeks numeryczny jest przypisany do każdej jednostki pamięci w systemie, przy czym jednostka jest zazwyczaj albo bajtem, albo słowem – w zależności od tego, czy architektura jest adresowalna bajtowo czy słownie – skutecznie przekształca całą pamięć w bardzo dużą tablicę . System zapewniłby wówczas również operację odzyskania wartości przechowywanej w jednostce pamięci pod danym adresem (zwykle z wykorzystaniem rejestrów ogólnego przeznaczenia maszyny ).

W zwykłym przypadku wskaźnik jest wystarczająco duży, aby pomieścić więcej adresów niż jest jednostek pamięci w systemie. Wprowadza to możliwość, że program może próbować uzyskać dostęp do adresu, który nie odpowiada żadnej jednostce pamięci, ponieważ zainstalowano niewystarczającą ilość pamięci (tj. poza zakresem dostępnej pamięci) lub architektura nie obsługuje takich adresów. Pierwszy przypadek może, w niektórych platformach, takich jak architektura Intel x86 , być nazywany błędem segmentacji (segfault). Drugi przypadek jest możliwy w obecnej implementacji AMD64 , gdzie wskaźniki mają długość 64 bitów, a adresy rozciągają się tylko do 48 bitów. Wskaźniki muszą być zgodne z pewnymi regułami (adresy kanoniczne), więc jeśli wskaźnik niekanoniczny jest wyłuskiwany, procesor zgłasza ogólny błąd ochrony .

Z drugiej strony, niektóre systemy mają więcej jednostek pamięci niż jest adresów. W tym przypadku do wykorzystania różnych części pamięci w różnym czasie wykorzystywany jest bardziej złożony schemat, taki jak segmentacja pamięci lub stronicowanie . Ostatnie wcielenia architektury x86 obsługują do 36 bitów adresów pamięci fizycznej, które zostały zmapowane do 32-bitowej liniowej przestrzeni adresowej poprzez mechanizm stronicowania PAE . W ten sposób tylko 1/16 możliwej całkowitej pamięci może być dostępne na raz. Innym przykładem, w tej samej rodziny komputer był 16-bitowy tryb chroniony od 80286 procesor, który, chociaż wsparcia tylko 16 MB pamięci fizycznej może uzyskać dostęp do 1 GB pamięci wirtualnej, ale połączenie z adresem 16-bitowym i segmentu rejestry utrudniały dostęp do ponad 64 KB w jednej strukturze danych.

Aby zapewnić spójny interfejs, niektóre architektury zapewniają we/wy mapowane w pamięci , co pozwala niektórym adresom odnosić się do jednostek pamięci, podczas gdy inne odwołują się do rejestrów urządzeń innych urządzeń w komputerze. Istnieją analogiczne koncepcje, takie jak przesunięcia plików, indeksy tablic i odwołania do obiektów zdalnych, które służą niektórym z tych samych celów, co adresy dla innych typów obiektów.

Zastosowania

Wskaźniki są bezpośrednio obsługiwane bez ograniczeń w językach takich jak PL/I , C , C++ , Pascal , FreeBASIC i niejawnie w większości języków asemblerowych . Służą one przede wszystkim do konstruowania referencji , które z kolei są podstawą do konstruowania prawie wszystkich struktur danych , a także do przekazywania danych między różnymi częściami programu.

W funkcjonalnych językach programowania, które w dużej mierze opierają się na listach, odwołania do danych są zarządzane abstrakcyjnie za pomocą prymitywnych konstrukcji, takich jak cons i odpowiadające im elementy car i cdr , które można traktować jako wyspecjalizowane wskaźniki do pierwszego i drugiego składnika cons-cell. Daje to początek idiomatycznemu „smakowi” programowania funkcjonalnego. Dzięki strukturyzacji danych w takich cons-listach języki te ułatwiają rekurencyjne środki do budowania i przetwarzania danych — na przykład poprzez rekurencyjny dostęp do elementów nagłówka i końca list list; np. „zabranie samochodu z cdr z cdr”. Natomiast zarządzanie pamięcią oparte na dereferencji wskaźnika w pewnym przybliżeniu tablicy adresów pamięci ułatwia traktowanie zmiennych jako slotów, do których dane mogą być bezwzględnie przypisane .

Kiedy mamy do czynienia z tablicami, krytyczna operacja wyszukiwania zazwyczaj obejmuje etap zwany obliczaniem adresu, który obejmuje konstruowanie wskaźnika do pożądanego elementu danych w tablicy. W innych strukturach danych, takich jak połączone listy , wskaźniki są używane jako odniesienia do jawnego powiązania jednego elementu struktury z drugim.

Wskaźniki służą do przekazywania parametrów przez odwołanie. Jest to przydatne, jeśli programista chce, aby modyfikacje funkcji w parametrze były widoczne dla wywołującego funkcję. Jest to również przydatne w przypadku zwracania wielu wartości z funkcji.

Wskaźniki mogą być również używane do przydzielania i zwalniania dynamicznych zmiennych i tablic w pamięci. Ponieważ zmienna często staje się zbędna po spełnieniu swojego celu, przechowywanie jej jest marnowaniem pamięci i dlatego dobrą praktyką jest cofnięcie jej (przy użyciu oryginalnego odwołania do wskaźnika), gdy nie jest już potrzebna. Niezastosowanie się do tego może spowodować wyciek pamięci (gdy dostępna wolna pamięć stopniowo lub w ciężkich przypadkach szybko się zmniejsza z powodu nagromadzenia wielu nadmiarowych bloków pamięci).

C wskaźniki

Podstawowa składnia do zdefiniowania wskaźnika to:

int *ptr;

Deklaruje ptrjako identyfikator obiektu następującego typu:

  • wskaźnik wskazujący na obiekt typu int

Jest to zwykle określane bardziej zwięźle jako „ ptrjest wskaźnikiem do int”.

Ponieważ język C nie określa niejawnej inicjalizacji obiektów o automatycznym czasie przechowywania, często należy zadbać o to, aby adres, na który ptrwskazuje, był prawidłowy; dlatego czasami sugeruje się, aby wskaźnik był jawnie zainicjalizowany do wartości null wskaźnika , która jest tradycyjnie określana w C za pomocą standardowego makra NULL:

int *ptr = NULL;

Wyłuskiwanie wskaźnika zerowego w C powoduje niezdefiniowane zachowanie , które może być katastrofalne. Jednak większość implementacji po prostu zatrzymuje wykonywanie danego programu, zwykle z błędem segmentacji .

Jednak niepotrzebne inicjowanie wskaźników może utrudnić analizę programu, ukrywając w ten sposób błędy.

W każdym razie po zadeklarowaniu wskaźnika następnym logicznym krokiem jest wskazanie na coś:

int a = 5;
int *ptr = NULL;

ptr = &a;

To przypisuje wartość adresu ado ptr. Na przykład, jeśli ajest przechowywany w lokalizacji pamięci 0x8130, to ptrpo przypisaniu wartość będzie wynosić 0x8130. Aby wyłuskać wskaźnik, ponownie używana jest gwiazdka:

*ptr = 8;

Oznacza to, że weź zawartość ptr(czyli 0x8130), "zlokalizuj" ten adres w pamięci i ustaw jego wartość na 8. Jeśli apóźniej uzyskasz dostęp ponownie, jego nowa wartość będzie wynosić 8.

Ten przykład może być jaśniejszy, jeśli pamięć jest badana bezpośrednio. Załóżmy, że aznajduje się pod adresem 0x8130 w pamięci i ptrpod adresem 0x8134; Załóżmy również, że jest to maszyna 32-bitowa, tak że int ma szerokość 32 bitów. Oto, co znajdzie się w pamięci po wykonaniu następującego fragmentu kodu:

int a = 5;
int *ptr = NULL;
Adres Zawartość
0x8130 0x000000005
0x8134 0x00000000

(Wskaźnik NULL pokazany tutaj to 0x00000000.) Przypisując adres ado ptr:

 ptr = &a;

zwraca następujące wartości pamięci:

Adres Zawartość
0x8130 0x000000005
0x8134 0x00008130

Następnie poprzez dereferencję ptrprzez kodowanie:

 *ptr = 8;

komputer pobierze zawartość ptr(czyli 0x8130), „zlokalizuje” ten adres i przypisze 8 do tej lokalizacji, uzyskując następującą pamięć:

Adres Zawartość
0x8130 0x000000008
0x8134 0x00008130

Oczywiście, dostęp aprzyniesie wartość 8, ponieważ poprzednia instrukcja zmodyfikowała zawartość aza pomocą wskaźnika ptr.

Wykorzystanie w strukturach danych

Podczas konfigurowania struktur danych, takich jak listy , kolejki i drzewa, konieczne jest posiadanie wskaźników, które pomogą zarządzać implementacją i kontrolą struktury. Typowymi przykładami wskaźników są wskaźniki początkowe, wskaźniki końcowe i wskaźniki stosu . Wskaźniki te mogą być bezwzględne (rzeczywisty adres fizyczny lub wirtualny adres w pamięci wirtualnej ) lub względne ( przesunięcie względem bezwzględnego adresu początkowego ("bazy"), który zwykle używa mniej bitów niż pełny adres, ale zwykle wymaga jednego dodatkowego operacja arytmetyczna do rozwiązania).

Adresy względne są formą ręcznej segmentacji pamięci i mają wiele wspólnych zalet i wad. Dwubajtowy offset, zawierający 16-bitową liczbę całkowitą bez znaku, może być użyty do zapewnienia adresowania względnego do 64 KiB (2 16 bajtów) struktury danych. Można to łatwo rozszerzyć do 128, 256 lub 512 KiB, jeśli wskazywany adres jest zmuszony do wyrównania do granicy półsłowa, słowa lub podwójnego słowa (ale wymaga to dodatkowej operacji bitowej „przesunięcie w lewo” — o 1, 2 lub 3 bity — w celu skorygowania offsetu o współczynnik 2, 4 lub 8 przed jego dodaniem do adresu bazowego). Generalnie jednak takie schematy są bardzo kłopotliwe i dla wygody programisty preferowane są adresy bezwzględne (i leżące u ich podstaw płaska przestrzeń adresowa ).

Przesunięcie jednobajtowe, takie jak szesnastkowa wartość ASCII znaku (np. X'29') może być użyte do wskazania alternatywnej wartości całkowitej (lub indeksu) w tablicy (np. X'01'). W ten sposób znaki mogą być bardzo wydajnie tłumaczone z „ surowych danych ” na użyteczny indeks sekwencyjny, a następnie na adres bezwzględny bez tablicy przeglądowej .

Tablice C

W C indeksowanie tablic jest formalnie zdefiniowane w kategoriach arytmetyki wskaźników; oznacza to, że specyfikacja języka wymaga, aby array[i]był równoważny z *(array + i). Tak więc w C tablice można traktować jako wskaźniki do kolejnych obszarów pamięci (bez przerw), a składnia dostępu do tablic jest identyczna ze składnią, która może być użyta do wyłuskania wskaźników. Na przykład tablicę arraymożna zadeklarować i używać w następujący sposób:

int array[5];      /* Declares 5 contiguous integers */
int *ptr = array;  /* Arrays can be used as pointers */
ptr[0] = 1;        /* Pointers can be indexed with array syntax */
*(array + 1) = 2;  /* Arrays can be dereferenced with pointer syntax */
*(1 + array) = 2;  /* Pointer addition is commutative */
array[2] = 4;      /* Subscript operator is commutative */

To przydziela blok pięciu liczb całkowitych i nazywa blok array, który działa jako wskaźnik do bloku. Innym powszechnym zastosowaniem wskaźników jest wskazywanie dynamicznie przydzielonej pamięci z malloc, która zwraca kolejny blok pamięci o rozmiarze nie mniejszym niż żądany rozmiar, który może być użyty jako tablica.

Chociaż większość operatorów w tablicach i wskaźnikach jest równoważnych, wynik sizeofoperatora jest inny. W tym przykładzie sizeof(array)zwróci do 5*sizeof(int)(rozmiar tablicy), podczas gdy sizeof(ptr)zwróci do sizeof(int*), rozmiar samego wskaźnika.

Domyślne wartości tablicy można zadeklarować w następujący sposób:

int array[5] = {2, 4, 3, 1, 5};

Jeśli arrayznajduje się w pamięci, począwszy od adresu 0x1000 na 32-bitowej maszynie little-endian , to pamięć będzie zawierać następujące elementy (wartości są szesnastkowe , tak jak adresy):

0 1 2 3
1000 2 0 0 0
1004 4 0 0 0
1008 3 0 0 0
100C 1 0 0 0
1010 5 0 0 0

Reprezentowanych tutaj jest pięć liczb całkowitych: 2, 4, 3, 1 i 5. Te pięć liczb całkowitych zajmuje 32 bity (4 bajty), każda z najmniej znaczącym bajtem przechowywanym jako pierwszy (jest to architektura procesora little-endian ) i są przechowywane kolejno zaczynając od adresu 0x1000.

Składnia języka C ze wskaźnikami to:

  • array oznacza 0x1000;
  • array + 1oznacza 0x1004: „+1” oznacza dodanie rozmiaru 1 int, czyli 4 bajty;
  • *arrayoznacza wyłuskanie zawartości array. Biorąc pod uwagę zawartość jako adres pamięci (0x1000), wyszukaj wartość w tej lokalizacji (0x0002);
  • array[i]oznacza numer elementu i, liczony od 0, arrayktóry jest tłumaczony na *(array + i).

Ostatni przykład to dostęp do zawartości array. Rozbijając to:

  • array + ijest lokalizacją w pamięci (i) -tego elementu array, zaczynając od i=0;
  • *(array + i) pobiera ten adres pamięci i wyłuskuje go, aby uzyskać dostęp do wartości.

C połączona lista

Poniżej znajduje się przykładowa definicja połączonej listy w C.

/* the empty linked list is represented by NULL
 * or some other sentinel value */
#define EMPTY_LIST  NULL

struct link {
    void        *data;  /* data of this link */
    struct link *next;  /* next link; EMPTY_LIST if there is none */
};

Ta definicja rekurencyjna wskaźnikowa jest zasadniczo taka sama jak definicja rekurencyjna rekurencyjna z języka programowania Haskell :

 data Link a = Nil
             | Cons a (Link a)

Niljest pustą listą i Cons a (Link a)jest komórką przeciw typu az innym linkiem również typu a.

Definicja z referencjami jest jednak sprawdzona pod względem typu i nie używa potencjalnie mylących wartości sygnału. Z tego powodu struktury danych w C są zwykle obsługiwane przez funkcje opakowujące , które są dokładnie sprawdzane pod kątem poprawności.

Podaj adres za pomocą wskaźników

Wskaźniki mogą służyć do przekazywania zmiennych według ich adresu, co pozwala na zmianę ich wartości. Rozważmy na przykład następujący kod C :

/* a copy of the int n can be changed within the function without affecting the calling code */
void passByValue(int n) {
    n = 12;
}

/* a pointer m is passed instead. No copy of the value pointed to by m is created */
void passByAddress(int *m) {
    *m = 14;
}

int main(void) {
    int x = 3;

    /* pass a copy of x's value as the argument */
    passByValue(x);
    // the value was changed inside the function, but x is still 3 from here on

    /* pass x's address as the argument */
    passByAddress(&x);
    // x was actually changed by the function and is now equal to 14 here

    return 0;
}

Dynamiczna alokacja pamięci

W niektórych programach wymagana pamięć zależy od tego, co użytkownik może wprowadzić. W takich przypadkach programista musi dynamicznie alokować pamięć. Odbywa się to poprzez alokację pamięci na stercie, a nie na stosie , gdzie zwykle przechowywane są zmienne (zmienne mogą być również przechowywane w rejestrach procesora, ale to inna sprawa). Dynamiczna alokacja pamięci może być dokonana tylko za pomocą wskaźników, a nazwy (jak w przypadku wspólnych zmiennych) nie mogą być podane.

Wskaźniki służą do przechowywania i zarządzania adresami dynamicznie przydzielanych bloków pamięci. Takie bloki służą do przechowywania obiektów danych lub tablic obiektów. Większość ustrukturyzowanych i zorientowanych obiektowo języków zapewnia obszar pamięci zwany stertą lub wolnym magazynem , z którego obiekty są dynamicznie przydzielane.

Poniższy przykładowy kod C ilustruje sposób, w jaki obiekty struktury są dynamicznie przydzielane i przywoływane. Standard C biblioteka udostępnia funkcję malloc()przydzielania bloków pamięci ze sterty. Przyjmuje rozmiar obiektu do przydzielenia jako parametr i zwraca wskaźnik do nowo przydzielonego bloku pamięci odpowiedniego do przechowywania obiektu lub zwraca wskaźnik null, jeśli alokacja nie powiodła się.

/* Parts inventory item */
struct Item {
    int         id;     /* Part number */
    char *      name;   /* Part name   */
    float       cost;   /* Cost        */
};

/* Allocate and initialize a new Item object */
struct Item * make_item(const char *name) {
    struct Item * item;

    /* Allocate a block of memory for a new Item object */
    item = malloc(sizeof(struct Item));
    if (item == NULL)
        return NULL;

    /* Initialize the members of the new Item */
    memset(item, 0, sizeof(struct Item));
    item->id =   -1;
    item->name = NULL;
    item->cost = 0.0;

    /* Save a copy of the name in the new Item */
    item->name = malloc(strlen(name) + 1);
    if (item->name == NULL) {
        free(item);
        return NULL;
    }
    strcpy(item->name, name);

    /* Return the newly created Item object */
    return item;
}

Poniższy kod ilustruje, w jaki sposób obiekty pamięci są dynamicznie zwalniane, tj. zwracane do sterty lub wolnego magazynu. Standardowa biblioteka C udostępnia funkcję free()zwalniania alokacji wcześniej przydzielonego bloku pamięci i zwracania go z powrotem na stertę.

/* Deallocate an Item object */
void destroy_item(struct Item *item) {
    /* Check for a null object pointer */
    if (item == NULL)
        return;

    /* Deallocate the name string saved within the Item */
    if (item->name != NULL) {
        free(item->name);
        item->name = NULL;
    }

    /* Deallocate the Item object itself */
    free(item);
}

Sprzęt mapowany w pamięci

W niektórych architekturach obliczeniowych wskaźniki mogą służyć do bezpośredniego manipulowania pamięcią lub urządzeniami mapowanymi w pamięci.

Przypisywanie adresów do wskaźników jest nieocenionym narzędziem podczas programowania mikrokontrolerów . Poniżej znajduje się prosty przykład deklarowania wskaźnika typu int i inicjowania go pod adresem szesnastkowym, w tym przykładzie stałą 0x7FFF:

int *hardware_address = (int *)0x7FFF;

W połowie lat 80. korzystanie z BIOS - u w celu uzyskania dostępu do funkcji wideo komputerów PC było powolne. Aplikacje, które intensywnie wyświetlały obraz, zwykle służyły do ​​uzyskiwania bezpośredniego dostępu do pamięci wideo CGA przez rzutowanie stałej szesnastkowej 0xB8000 na wskaźnik do tablicy 80 16-bitowych wartości int bez znaku. Każda wartość składała się z kodu ASCII w młodszym bajcie i koloru w starszym bajcie. Tak więc, aby umieścić literę „A” w wierszu 5, kolumnie 2 na biało na niebiesko, należy napisać następujący kod:

#define VID ((unsigned short (*)[80])0xB8000)

void foo(void) {
    VID[4][1] = 0x1F00 | 'A';
}

Użyj w tabelach kontrolnych

Tabele sterujące używane do sterowania przepływem programu zwykle wykorzystują w dużym stopniu wskaźniki. Wskaźniki, zwykle osadzone we wpisie tablicy, mogą, na przykład, być używane do utrzymywania punktów wejścia do podprogramów, które mają być wykonane, w oparciu o pewne warunki zdefiniowane w tym samym wpisie tablicy. Wskaźniki mogą jednak być po prostu indeksami do innych oddzielnych, ale powiązanych, tablic zawierających tablicę rzeczywistych adresów lub samych adresów (w zależności od dostępnych konstrukcji języka programowania). Mogą być również używane do wskazywania wcześniejszych wpisów w tabeli (jak w przetwarzaniu w pętli) lub do przodu w celu pominięcia niektórych wpisów w tabeli (jak w przełączniku lub „wczesnym” wyjściu z pętli). W tym ostatnim celu „wskaźnik” może być po prostu numerem wpisu w tabeli i może być przekształcony w rzeczywisty adres za pomocą prostej arytmetyki.

Wpisane wskaźniki i rzutowanie

W wielu językach wskaźniki mają dodatkowe ograniczenie, że obiekt, na który wskazują, ma określony typ . Na przykład wskaźnik może być zadeklarowany, aby wskazywał na liczbę całkowitą ; język będzie następnie próbował uniemożliwić programiście wskazywanie obiektów, które nie są liczbami całkowitymi, takich jak liczby zmiennoprzecinkowe , eliminując niektóre błędy.

Na przykład w C

int *money;
char *bags;

moneybyłby wskaźnikiem całkowitym i bagsbyłby wskaźnikiem char. Poniższe spowoduje ostrzeżenie kompilatora „przypisanie z niezgodnego typu wskaźnika” w GCC

bags = money;

ponieważ moneyi bagszostały zadeklarowane z różnymi typami. Aby stłumić ostrzeżenia kompilatora, należy wyraźnie zaznaczyć, że rzeczywiście chcą dokonać cesji przez rzutowania go

bags = (char *)money;

co mówi, aby rzutować wskaźnik liczby całkowitej na wskaźnik moneychar i przypisać do bags.

Projekt normy C z 2005 r. wymaga, aby rzutowanie wskaźnika pochodzącego z jednego typu na inny typ zachowywało poprawność wyrównania dla obu typów (6.3.2.3 Wskaźniki, par. 7):

char *external_buffer = "abcdef";
int *internal_data;

internal_data = (int *)external_buffer;  // UNDEFINED BEHAVIOUR if "the resulting pointer
                                         // is not correctly aligned"

W językach, które umożliwiają arytmetykę wskaźników, arytmetyka wskaźników uwzględnia rozmiar typu. Na przykład dodanie liczby całkowitej do wskaźnika daje inny wskaźnik, który wskazuje na adres, który jest wyższy o tę liczbę razy rozmiar typu. Pozwala nam to łatwo obliczyć adres elementów tablicy danego typu, jak pokazano w powyższym przykładzie tablic C. Kiedy wskaźnik jednego typu jest rzutowany na inny typ o innym rozmiarze, programista powinien oczekiwać, że arytmetyka wskaźników zostanie obliczona inaczej. Na przykład w języku C, jeśli moneytablica zaczyna się od 0x2000 i sizeof(int)ma 4 bajty, podczas gdy sizeof(char)ma 1 bajt, to money + 1wskaże 0x2004, ale bags + 1będzie wskazywać 0x2001. Inne ryzyko rzutowania obejmuje utratę danych, gdy „szerokie” dane są zapisywane w „wąskich” lokalizacjach (np. bags[0] = 65537;), nieoczekiwane wyniki podczas przesuwania wartości bitowych oraz problemy z porównaniem, zwłaszcza w przypadku wartości ze znakiem i bez znaku.

Chociaż generalnie niemożliwe jest określenie w czasie kompilacji, które rzutowania są bezpieczne, niektóre języki przechowują informacje o typie w czasie wykonywania, które mogą być użyte do potwierdzenia, że ​​te niebezpieczne rzutowania są prawidłowe w czasie wykonywania. Inne języki akceptują jedynie konserwatywne przybliżenie bezpiecznych rzutów lub wcale.

Wartość wskaźników

W C i C++ wynik porównania wskaźników jest niezdefiniowany. W tych językach i LLVM reguła jest interpretowana w taki sposób, że „tylko dlatego, że dwa wskaźniki wskazują ten sam adres, nie oznacza, że ​​są one równe i mogą być używane zamiennie”, różnica między wskaźnikami określana jako ich pochodzenie . Chociaż rzutowanie na typ całkowity, taki jak uintptr_tporównanie ofert, samo rzutowanie jest zdefiniowane przez implementację. Ponadto dalsza konwersja na bajty i arytmetyka zniechęci optymalizatorów próbujących śledzić użycie wskaźników, problem wciąż wyjaśniany w badaniach naukowych.

Zwiększanie bezpieczeństwa wskaźników

Ponieważ wskaźnik pozwala programowi na próbę uzyskania dostępu do obiektu, który może nie być zdefiniowany, wskaźniki mogą być źródłem różnych błędów programistycznych . Jednak przydatność wskaźników jest tak duża, że ​​wykonanie zadań programistycznych bez nich może być trudne. W związku z tym wiele języków stworzyło konstrukcje zaprojektowane w celu zapewnienia niektórych użytecznych funkcji wskaźników bez pewnych pułapek , czasami określanych również jako zagrożenia wskaźnikowe . W tym kontekście wskaźniki bezpośrednio adresujące pamięć (używane w tym artykule) są określane jakosurowy wskaźnik s, w przeciwieństwie dointeligentnych wskaźnikówlub innych wariantów.

Jednym z głównych problemów ze wskaźnikami jest to, że dopóki można nimi bezpośrednio manipulować jako liczbą, mogą one wskazywać na nieużywane adresy lub dane, które są wykorzystywane do innych celów. Wiele języków, w tym większość funkcjonalnych języków programowania i najnowsze języki imperatywne, takie jak Java , zastępuje wskaźniki bardziej nieprzejrzystym typem referencji, zwykle określanym jako referencja , którego można używać tylko do odwoływania się do obiektów, a nie jako liczb, co zapobiega temu rodzaj błędu. Indeksowanie tablicy jest traktowane jako przypadek szczególny.

Wskaźnik, do którego nie przypisano żadnego adresu, nazywany jest dzikim wskaźnikiem . Każda próba użycia takich niezainicjowanych wskaźników może spowodować nieoczekiwane zachowanie, ponieważ wartość początkowa nie jest prawidłowym adresem lub ponieważ użycie jej może uszkodzić inne części programu. Rezultatem jest często błąd segmentacji , naruszenie pamięci lub dzikie rozgałęzienie (jeśli jest używane jako wskaźnik funkcji lub adres gałęzi).

W systemach z jawną alokacją pamięci możliwe jest utworzenie nieaktualnego wskaźnika przez cofnięcie alokacji obszaru pamięci, na który wskazuje. Ten typ wskaźnika jest niebezpieczny i subtelny, ponieważ cofnięty alokowany obszar pamięci może zawierać te same dane, które zawierał przed cofnięciem alokacji, ale może być następnie ponownie przydzielony i nadpisany przez niepowiązany kod, nieznany we wcześniejszym kodzie. Języki z wyrzucaniem elementów bezużytecznych zapobiegają tego typu błędom, ponieważ cofanie alokacji jest wykonywane automatycznie, gdy w zasięgu nie ma więcej odwołań.

Niektóre języki, takie jak C++ , obsługują inteligentne wskaźniki , które wykorzystują prostą formę zliczania odwołań, aby pomóc śledzić alokację pamięci dynamicznej oprócz działania jako odniesienie. W przypadku braku cykli referencyjnych, w których obiekt odnosi się do siebie pośrednio poprzez sekwencję inteligentnych wskaźników, eliminuje to możliwość zawieszania się wskaźników i wycieków pamięci. Łańcuchy Delphi obsługują natywnie zliczanie odwołań.

Język programowania Rust wprowadza moduł sprawdzania wypożyczeń , okresy istnienia wskaźników i optymalizację opartą na opcjonalnych typach dla wskaźników o wartości null w celu wyeliminowania błędów wskaźnika, bez uciekania się do wyrzucania elementów bezużytecznych .

Specjalne rodzaje wskaźników

Rodzaje określone przez wartość

Wskaźnik zerowy

Zerowy wskaźnik ma wartość zarezerwowaną dla wskazania, że wskaźnik nie odnosi się do ważnego przedmiotu. Wskaźniki zerowe są rutynowo używane do reprezentowania warunków, takich jak koniec listy o nieznanej długości lub niepowodzenie wykonania jakiejś akcji; to użycie wskaźników o wartości null można porównać do typów dopuszczających wartość null i do wartości Nothing w typie opcji .

Wiszący wskaźnik

Zwisające wskaźnik jest wskaźnikiem, który nie wskazuje ważnego obiektu, aw konsekwencji może spowodować zawieszenie się programu lub zachowywać się dziwnie. W językach programowania Pascal lub C wskaźniki, które nie są specjalnie zainicjowane, mogą wskazywać na nieprzewidywalne adresy w pamięci.

Poniższy przykładowy kod przedstawia nieaktualny wskaźnik:

int func(void) {
    char *p1 = malloc(sizeof(char)); /* (undefined) value of some place on the heap */
    char *p2;       /* dangling (uninitialized) pointer */
    *p1 = 'a';      /* This is OK, assuming malloc() has not returned NULL. */
    *p2 = 'b';      /* This invokes undefined behavior */
}

Tutaj p2może wskazywać na dowolne miejsce w pamięci, więc wykonanie przypisania *p2 = 'b';może uszkodzić nieznany obszar pamięci lub wywołać błąd segmentacji .

Dzika gałąź

Tam, gdzie wskaźnik jest używany jako adres punktu wejścia do programu lub początku funkcji, która niczego nie zwraca, a także jest niezainicjowana lub uszkodzona, jeśli mimo to wywołanie lub skok zostanie wykonane na ten adres, " wild branch podobno miało miejsce. Innymi słowy, dzika gałąź to wskaźnik do funkcji, który jest dziki (wiszący).

Konsekwencje są zwykle nieprzewidywalne, a błąd może wystąpić na kilka różnych sposobów, w zależności od tego, czy wskaźnik jest „poprawnym” adresem i czy istnieje (przypadkowo) ważna instrukcja (opcode) pod tym adresem. Wykrycie dzikiej gałęzi może stanowić jedno z najtrudniejszych i najbardziej frustrujących ćwiczeń debugowania, ponieważ wiele dowodów mogło zostać wcześniej zniszczonych lub przez wykonanie jednej lub więcej niewłaściwych instrukcji w lokalizacji gałęzi. Jeśli jest dostępny, symulator zestawu instrukcji może zwykle nie tylko wykryć dzikie rozgałęzienie, zanim zacznie działać, ale także zapewnić pełny lub częściowy ślad jej historii.

Rodzaje zdefiniowane przez strukturę

Wskaźnik autorelatywny

Autorelative wskaźnikiem jest wskaźnik, którego wartość jest interpretowana jako przesunięcie od adresu samego wskaźnika; tak więc, jeśli struktura danych ma element wskaźnika autorelacyjnego, który wskazuje na pewną część samej struktury danych, wówczas struktura danych może zostać przeniesiona w pamięci bez konieczności aktualizowania wartości wskaźnika autorelatywnego.

Cytowany patent używa również terminu „ wskaźnik względny” w znaczeniu tego samego. Jednak znaczenie tego terminu zostało użyte w inny sposób:

  • oznaczać przesunięcie od adresu struktury, a nie od adresu samego wskaźnika;
  • oznacza wskaźnik zawierający własny adres, który może być przydatny do rekonstrukcji w dowolnym obszarze pamięci zbioru struktur danych, które wskazują na siebie.

Wskaźnik na podstawie

Wskaźnik oparty jest wskaźnik, którego wartość jest przesunięcie od wartości innego wskaźnika. Może być używany do przechowywania i ładowania bloków danych, przypisując adres początku bloku do wskaźnika bazowego.

Rodzaje zdefiniowane przez użycie lub typ danych

Wiele pośrednich

W niektórych językach wskaźnik może odwoływać się do innego wskaźnika, co wymaga wielu operacji wyłuskiwania, aby uzyskać oryginalną wartość. Chociaż każdy poziom pośredni może zwiększać koszty wydajności, czasami jest to konieczne w celu zapewnienia prawidłowego zachowania złożonych struktur danych . Na przykład w C typowe jest definiowanie połączonej listy w kategoriach elementu, który zawiera wskaźnik do następnego elementu listy:

struct element {
    struct element *next;
    int            value;
};

struct element *head = NULL;

Ta implementacja używa wskaźnika do pierwszego elementu na liście jako substytutu całej listy. Jeśli na początku listy zostanie dodana nowa wartość, headnależy ją zmienić tak, aby wskazywała na nowy element. Ponieważ argumenty C są zawsze przekazywane przez wartość, użycie podwójnej niebezpośredniości pozwala na poprawną implementację wstawiania i ma pożądany efekt uboczny polegający na wyeliminowaniu specjalnego kodu do obsługi wstawiania na początku listy:

// Given a sorted list at *head, insert the element item at the first
// location where all earlier elements have lesser or equal value.
void insert(struct element **head, struct element *item) {
    struct element **p;  // p points to a pointer to an element
    for (p = head; *p != NULL; p = &(*p)->next) {
        if (item->value <= (*p)->value)
            break;
    }
    item->next = *p;
    *p = item;
}

// Caller does this:
insert(&head, item);

W takim przypadku, jeśli wartość itemjest mniejsza niż wartość , adres headwywołujący headjest prawidłowo aktualizowany na adres nowej pozycji.

Podstawowym przykładem jest argument argv do funkcji main w C (i C++) , który jest podany w prototypie jako char **argv— jest tak, ponieważ argvsama zmienna jest wskaźnikiem do tablicy łańcuchów (tablicy tablic), więc *argvjest wskaźnik do zerowego napisu (umownie nazwa programu) i **argvjest zerowym znakiem zerowego napisu.

Wskaźnik funkcji

W niektórych językach wskaźnik może odwoływać się do kodu wykonywalnego, tj. może wskazywać na funkcję, metodę lub procedurę. Wskaźnik Funkcja zapamiętuje adres funkcji powoływać. Chociaż funkcja ta może być używana do dynamicznego wywoływania funkcji, często jest to ulubiona technika twórców wirusów i innych złośliwych programów.

int sum(int n1, int n2) {   // Function with two integer parameters returning an integer value
    return n1 + n2;
}

int main(void) {
    int a, b, x, y;
    int (*fp)(int, int);    // Function pointer which can point to a function like sum
    fp = &sum;              // fp now points to function sum
    x = (*fp)(a, b);        // Calls function sum with arguments a and b
    y = sum(a, b);          // Calls function sum with arguments a and b
}

Powrót wskaźnik

W podwójnie połączonych listach lub strukturach drzewiastych wskaźnik wsteczny przytrzymywany na elemencie „wskazuje wstecz” na element odnoszący się do bieżącego elementu. Są one przydatne do nawigacji i manipulacji kosztem większego wykorzystania pamięci.

Symulacja przy użyciu indeksu tablicy

Możliwe jest symulowanie zachowania wskaźnika za pomocą indeksu do (zwykle jednowymiarowej) tablicy.

Przede wszystkim dla języków, które nie obsługują wskaźniki się wyraźnie jednak zrobić tablice wsporniku, tablica może być traktowane i przetwarzane tak, jakby to była cała gama pamięci (w ramach danej tablicy) i każdy wskaźnik do niego mogą być traktowane jako równoważne do rejestru ogólnego przeznaczenia w języku asemblerowym (który wskazuje na poszczególne bajty, ale których rzeczywista wartość jest względna do początku tablicy, a nie jej bezwzględnego adresu w pamięci). Zakładając, że tablica jest, powiedzmy, przyległe 16 megabajta charakter struktury danych , poszczególne bajty (lub ciąg sąsiednich bajtów wewnątrz tablicy) mogą być bezpośrednio kierowane i manipulowane używając nazwy tablicy z 31 bitowych bez znaku liczby całkowitej jako symulowanym wskaźnikiem (jest to bardzo podobne do pokazanego powyżej przykładu tablic C ). Arytmetyka wskaźników może być symulowana przez dodawanie lub odejmowanie od indeksu, przy minimalnym dodatkowym nakładzie pracy w porównaniu do prawdziwej arytmetyki wskaźników.

Jest nawet teoretycznie możliwe, przy użyciu powyższej techniki, wraz z odpowiednim symulatorem zestawu instrukcji, symulowanie dowolnego kodu maszynowego lub pośredniego ( kodu bajtowego ) dowolnego procesora/języka w innym języku, który w ogóle nie obsługuje wskaźników (np. Java / JavaScript ). Aby to osiągnąć, kod binarny można początkowo załadować do sąsiednich bajtów tablicy, aby symulator "odczytywał", interpretował i wykonywał działania całkowicie w pamięci zawartej w tej samej tablicy. Jeśli to konieczne, aby całkowicie uniknąć problemów z przepełnieniem bufora , sprawdzanie granic może być zwykle wykonywane przez kompilator (lub, jeśli nie, ręcznie kodowane w symulatorze).

Wsparcie w różnych językach programowania

Ada

Ada jest językiem silnie typizowanym, w którym wszystkie wskaźniki są wpisywane i dozwolone są tylko bezpieczne konwersje typów. Wszystkie wskaźniki są domyślnie inicjowane do null, a każda próba uzyskania dostępu do danych za pomocą nullwskaźnika powoduje zgłoszenie wyjątku . Wskaźniki w Adzie nazywane są typami dostępu . Ada 83 nie zezwalała na arytmetykę typów dostępu (chociaż wielu producentów kompilatorów przewidziało to jako niestandardową funkcję), ale Ada 95 wspiera „bezpieczną” arytmetykę dla typów dostępu za pośrednictwem pakietu System.Storage_Elements.

PODSTAWOWY

Kilka starych wersji BASICa na platformę Windows obsługiwało STRPTR() do zwracania adresu łańcucha oraz VARPTR() do zwracania adresu zmiennej. Visual Basic 5 posiadał również obsługę OBJPTR() do zwracania adresu interfejsu obiektu oraz operatora ADDRESSOF do zwracania adresu funkcji. Typy wszystkich z nich są liczbami całkowitymi, ale ich wartości są równoważne tym przechowywanym przez typy wskaźników.

Nowsze dialekty języka BASIC , takie jak FreeBASIC lub BlitzMax , mają jednak wyczerpujące implementacje wskaźników. We FreeBASIC arytmetyka na ANYwskaźnikach (odpowiednik C's void*) jest traktowana tak, jakby ANYwskaźnik miał szerokość bajta. ANYwskaźników nie można wyłuskać, jak w C. Ponadto rzutowanie między ANYwskaźnikami innego typu nie spowoduje wygenerowania żadnych ostrzeżeń.

dim as integer f = 257
dim as any ptr g = @f
dim as integer ptr i = g
assert(*i = 257)
assert( (g + 4) = (@f + 1) )

C i C++

W C i C++ wskaźniki są zmiennymi, które przechowują adresy i mogą być null . Każdy wskaźnik ma typ, na który wskazuje, ale można swobodnie rzutować między typami wskaźników (ale nie między wskaźnikiem funkcji a wskaźnikiem obiektu). Specjalny typ wskaźnika zwany „pustym wskaźnikiem” umożliwia wskazanie dowolnego obiektu (nie będącego funkcją), ale jest ograniczony faktem, że nie można go wyłuskać bezpośrednio (powinien być rzutowany). Sam adres często może być bezpośrednio manipulowany przez rzutowanie wskaźnika do i z integralnego typu o wystarczającym rozmiarze, chociaż wyniki są zdefiniowane przez implementację i mogą rzeczywiście powodować niezdefiniowane zachowanie; podczas gdy wcześniejsze standardy C nie miały typu integralnego, który gwarantował, że jest wystarczająco duży, C99 określa nazwę uintptr_t typedef zdefiniowaną w <stdint.h>, ale implementacja nie musi jej dostarczać.

C++ w pełni obsługuje wskaźniki C i rzutowanie typów C. Obsługuje również nową grupę operatorów rzutowania typów, aby pomóc wyłapać niektóre niezamierzone niebezpieczne rzutowania w czasie kompilacji. Ponieważ C ++ 11 The C ++ Standardowa biblioteka zapewnia także inteligentne kursory ( unique_ptr, shared_ptri weak_ptr), które mogą być wykorzystywane w niektórych sytuacjach jako bezpieczniejszą alternatywę dla prymitywnych wskaźniki C. C++ obsługuje również inną formę referencji, zupełnie inną niż wskaźnik, zwaną po prostu referencją lub typem referencyjnym .

Arytmetyka wskaźnika , to znaczy możliwość modyfikowania adresu docelowego wskaźnika za pomocą operacji arytmetycznych (a także porównywania wielkości), jest ograniczona przez standard języka do pozostawania w granicach pojedynczego obiektu tablicy (lub tuż za nim) i będzie w przeciwnym razie wywołaj niezdefiniowane zachowanie . Dodawanie lub odejmowanie od wskaźnika przesuwa go o wielokrotność rozmiaru jego typu danych . Na przykład dodanie 1 do wskaźnika do 4-bajtowych wartości całkowitych spowoduje zwiększenie wskazywanego na bajt adresu wskaźnika o 4. Ma to efekt zwiększenia wskaźnika wskazującego na następny element w ciągłej tablicy liczb całkowitych — co jest często zamierzony wynik. Arytmetyka wskaźników nie może być wykonana na voidwskaźnikach, ponieważ typ void nie ma rozmiaru, a zatem wskazany adres nie może zostać dodany, chociaż gcc i inne kompilatory będą wykonywać arytmetykę bajtową void*jako niestandardowe rozszerzenie, traktując je tak, jakby były char *.

Arytmetyka wskaźników zapewnia programiście jeden sposób radzenia sobie z różnymi typami: dodawanie i odejmowanie wymaganej liczby elementów zamiast rzeczywistego przesunięcia w bajtach. (Arytmetyka char *wskaźników ze wskaźnikami wykorzystuje przesunięcia bajtowe, ponieważ sizeof(char)z definicji jest to 1.) W szczególności, definicja C wyraźnie deklaruje, że składnia a[n], która jest n-tym elementem tablicy a, jest równoważna *(a + n), która jest zawartością wskazanego elementu przez a + n. Oznacza n[a]to, że jest to równoważne a[n], i można na przykład pisać a[3]lub 3[a]równie dobrze, aby uzyskać dostęp do czwartego elementu tablicy a.

Choć potężna, arytmetyka wskaźników może być źródłem błędów komputerowych . Ma tendencję do mylenia początkujących programistów , zmuszając ich do różnych kontekstów: wyrażenie może być zwykłym arytmetycznym lub wskaźnikowym, a czasami łatwo jest pomylić jedno z drugim. W odpowiedzi na to wiele współczesnych języków komputerowych wysokiego poziomu (na przykład Java ) nie pozwala na bezpośredni dostęp do pamięci za pomocą adresów. Również bezpieczny dialekt C Cyclone rozwiązuje wiele problemów ze wskaźnikami. Zobacz język programowania C, aby uzyskać więcej dyskusji.

voidWskaźnik , czy void*jest obsługiwana w ANSI C i C ++ jako ogólny wskaźnik typu. Wskaźnik do voidmoże przechowywać adres dowolnego obiektu (nie funkcji) i, w języku C, jest niejawnie konwertowany na dowolny inny typ wskaźnika do obiektu przy przypisaniu, ale musi być jawnie rzutowany, jeśli jest wyłuskiwany. K&R C używany char*do celów „wskaźnika niezależnego od typu” (przed ANSI C).

int x = 4;
void* p1 = &x;
int* p2 = p1;       // void* implicitly converted to int*: valid C, but not C++
int a = *p2;
int b = *(int*)p1;  // when dereferencing inline, there is no implicit conversion

C++ nie zezwala na niejawną konwersję void*na inne typy wskaźników, nawet w przypisaniach. To była decyzja projektowa, aby uniknąć nieostrożnych, a nawet niezamierzonych rzutowań, chociaż większość kompilatorów wyświetla tylko ostrzeżenia, a nie błędy, podczas napotkania innych rzutowań.

int x = 4;
void* p1 = &x;
int* p2 = p1;                     // this fails in C++: there is no implicit conversion from void*
int* p3 = (int*)p1;               // C-style cast
int* p4 = static_cast<int*>(p1);  // C++ cast

W C++ nie ma void&(odwołanie do void) do uzupełnienia void*(wskaźnik do void), ponieważ referencje zachowują się jak aliasy do zmiennych, na które wskazują, i nigdy nie może istnieć zmienna, której typ to void.

Omówienie składni deklaracji wskaźnika

Te deklaracje wskaźnika obejmują większość wariantów deklaracji wskaźnika. Oczywiście możliwe jest posiadanie potrójnych wskaźników, ale główne zasady stojące za potrójnym wskaźnikiem już istnieją w podwójnym wskaźniku.

char cff [5][5];    /* array of arrays of chars */
char *cfp [5];      /* array of pointers to chars */
char **cpp;         /* pointer to pointer to char ("double pointer") */
char (*cpf) [5];    /* pointer to array(s) of chars */
char *cpF();        /* function which returns a pointer to char(s) */
char (*CFp)();      /* pointer to a function which returns a char */
char (*cfpF())[5];  /* function which returns pointer to an array of chars */
char (*cpFf[5])();  /* an array of pointers to functions which return a char */

() i [] mają wyższy priorytet niż *.

C#

W języku programowania C# wskaźniki są obsługiwane tylko pod pewnymi warunkami: każdy blok kodu zawierający wskaźniki musi być oznaczony unsafesłowem kluczowym. Takie bloki zwykle wymagają wyższych uprawnień bezpieczeństwa, aby można było je uruchomić. Składnia jest zasadniczo taka sama jak w C++, a wskazany adres może być pamięcią zarządzaną lub niezarządzaną . Jednak wskaźniki do pamięci zarządzanej (dowolny wskaźnik do obiektu zarządzanego) muszą być zadeklarowane przy użyciu fixedsłowa kluczowego, co zapobiega przenoszeniu wskazanego obiektu przez moduł odśmiecania pamięci w ramach zarządzania pamięcią, gdy wskaźnik znajduje się w zakresie, dzięki czemu adres wskaźnika jest prawidłowy.

Wyjątkiem jest użycie IntPtrstruktury, która jest bezpiecznym zarządzanym odpowiednikiem int*i nie wymaga niebezpiecznego kodu. Ten typ jest często zwracany podczas korzystania z metod z System.Runtime.InteropServices, na przykład:

// Get 16 bytes of memory from the process's unmanaged memory
IntPtr pointer = System.Runtime.InteropServices.Marshal.AllocHGlobal(16);

// Do something with the allocated memory

// Free the allocated memory
System.Runtime.InteropServices.Marshal.FreeHGlobal(pointer);

Platforma .NET zawiera wiele klas i metod w przestrzeniach nazw Systemi System.Runtime.InteropServices(takich jak Marshalklasa), które konwertują typy .NET (na przykład System.String) na i z wielu niezarządzanych typów i wskaźników (na przykład LPWSTRlub void*), aby umożliwić komunikację z kodem niezarządzanym . Większość takich metod ma takie same wymagania dotyczące uprawnień zabezpieczeń jak kod niezarządzany, ponieważ mogą one wpływać na dowolne miejsca w pamięci.

COBOL

Język programowania COBOL obsługuje wskaźniki do zmiennych. Pierwotne lub grupowe (rekordowe) obiekty danych zadeklarowane w LINKAGE SECTIONprogramie są z natury oparte na wskaźnikach, gdzie jedyną pamięcią przydzieloną w programie jest miejsce na adres elementu danych (zazwyczaj pojedyncze słowo pamięci). W kodzie źródłowym programu te elementy danych są używane tak jak każda inna WORKING-STORAGEzmienna, ale do ich zawartości uzyskuje się dostęp pośrednio poprzez ich LINKAGEwskaźniki.

Przestrzeń pamięci dla każdego wskazanego obiektu danych jest zwykle przydzielana dynamicznie za pomocą CALLinstrukcji zewnętrznych lub za pomocą wbudowanych konstrukcji języka rozszerzonego, takich jak instrukcje EXEC CICSlub EXEC SQL.

Rozszerzone wersje języka COBOL udostępniają również zmienne wskaźnikowe zadeklarowane za pomocą USAGE IS POINTERklauzul. Wartości takich zmiennych wskaźnikowych są ustalane i modyfikowane przy użyciu instrukcji SETi SET ADDRESS.

Niektóre rozszerzone wersje języka COBOL udostępniają również PROCEDURE-POINTERzmienne, które są w stanie przechowywać adresy kodu wykonywalnego .

PL/I

PL / I język zapewnia pełne wsparcie dla wskazówki dla wszystkich typów danych (w tym wskaźniki do struktur), rekursji , wielozadaniowość , obsługę łańcuchów, a także rozbudowane wbudowanej funkcji . PL/I był dużym krokiem naprzód w porównaniu z językami programowania swoich czasów. Wskaźniki PL/I są bez typu i dlatego rzutowanie nie jest wymagane do wyłuskiwania lub przypisywania wskaźników. Składnia deklaracji wskaźnika to DECLARE xxx POINTER;, który deklaruje wskaźnik o nazwie „xxx”. Wskaźniki są używane ze BASEDzmiennymi. Zmienna bazowa może być zadeklarowana z domyślnym lokalizatorem ( DECLARE xxx BASED(ppp);lub bez ( DECLARE xxx BASED;), gdzie xxx jest zmienną bazową, która może być zmienną elementu, strukturą lub tablicą, a ppp jest wskaźnikiem domyślnym). Taka zmienna może być adresowana bez wyraźnego odwołania do wskaźnika ( xxx=1;lub może być adresowana z wyraźnym odwołaniem do domyślnego lokalizatora (ppp) lub dowolnego innego wskaźnika ( qqq->xxx=1;).

Arytmetyka wskaźników nie jest częścią standardu PL/I, ale wiele kompilatorów zezwala na wyrażenia formularza ptr = ptr±expression. IBM PL/I ma również wbudowaną funkcję PTRADDdo wykonywania arytmetyki. Arytmetyka wskaźników jest zawsze wykonywana w bajtach.

Kompilatory IBM Enterprise PL/I mają nową formę wpisywanego wskaźnika o nazwie a HANDLE.

D

Język programowania D jest pochodną C i C++, która w pełni obsługuje wskaźniki C i rzutowanie typów C.

Eiffla

Język obiektowy Eiffla wykorzystuje semantykę wartości i referencji bez arytmetyki wskaźników. Niemniej jednak udostępniane są klasy wskaźników. Oferują arytmetykę wskaźników, rzutowanie typów, bezpośrednie zarządzanie pamięcią, interfejs z oprogramowaniem innym niż Eiffel i inne funkcje.

Fortran

Fortran-90 wprowadził możliwość silnie typizowanego wskaźnika. Wskaźniki Fortran zawierają więcej niż tylko prosty adres pamięci. Zawierają również dolną i górną granicę wymiarów tablicy, kroków (na przykład w celu obsługi dowolnych sekcji tablicy) i innych metadanych. Operatora Association , =>stosuje się do powiązania POINTERze zmienną, która ma TARGETatrybutu. Instrukcja Fortran-90 ALLOCATEmoże być również użyta do powiązania wskaźnika z blokiem pamięci. Na przykład poniższy kod może być użyty do zdefiniowania i utworzenia połączonej struktury listy:

type real_list_t
  real :: sample_data(100)
  type (real_list_t), pointer :: next => null ()
end type

type (real_list_t), target :: my_real_list
type (real_list_t), pointer :: real_list_temp

real_list_temp => my_real_list
do
  read (1,iostat=ioerr) real_list_temp%sample_data
  if (ioerr /= 0) exit
  allocate (real_list_temp%next)
  real_list_temp => real_list_temp%next
end do

Fortran-2003 dodaje obsługę wskaźników procedur. Ponadto, jako część funkcji C Interoperability , Fortran-2003 obsługuje wewnętrzne funkcje konwersji wskaźników w stylu C na wskaźniki Fortran iz powrotem.

Udać się

Idź ma wskazówki. Jego składnia deklaracji jest równoważna składni C, ale zapisywana jest na odwrót, kończąc na typie. W przeciwieństwie do C, Go ma garbage collection i nie zezwala na arytmetykę wskaźników. Typy referencyjne, takie jak w C++, nie istnieją. Niektóre typy wbudowane, takie jak mapy i kanały, są otoczone ramką (tj. wewnętrznie są wskaźnikami do struktur mutowalnych) i są inicjowane za pomocą makefunkcji. W podejściu do ujednoliconej składni między wskaźnikami i niewskaźnikami ->odrzucono operator strzałki ( ): operator kropki na wskaźniku odnosi się do pola lub metody wyłuskanego obiektu. Działa to jednak tylko z 1 poziomem niebezpośrednim.

Jawa

W Javie nie ma wyraźnej reprezentacji wskaźników . Zamiast tego bardziej złożone struktury danych, takie jak obiekty i tablice, są implementowane przy użyciu referencji . Język nie zapewnia żadnych jawnych operatorów manipulacji wskaźnikami. Kod nadal może jednak próbować wyłuskać odwołanie o wartości null (wskaźnik o wartości null), co powoduje zgłoszenie wyjątku czasu wykonywania . Miejsce zajmowane przez obiekty pamięci bez odwołań jest automatycznie odzyskiwane przez wyrzucanie elementów bezużytecznych w czasie wykonywania.

Moduł-2

Wskaźniki są zaimplementowane podobnie jak w Pascalu, podobnie jak VARparametry w wywołaniach procedur. Modula-2 jest jeszcze silniej typowana niż Pascal, z mniejszą liczbą sposobów na uniknięcie systemu typograficznego. Niektóre warianty Modula-2 (takie jak Modula-3 ) obejmują zbieranie śmieci.

Oberon

Podobnie jak w przypadku Modula-2, wskaźniki są dostępne. Wciąż jest mniej sposobów na obejście systemu typów, więc Oberon i jego warianty są nadal bezpieczniejsze pod względem wskaźników niż Modula-2 lub jego warianty. Podobnie jak w przypadku Modula-3 , garbage collection jest częścią specyfikacji języka.

Pascal

W przeciwieństwie do wielu języków, w których dostępne są wskaźniki, standard ISO Pascal umożliwia jedynie odwoływanie się do dynamicznie tworzonych zmiennych, które są anonimowe i nie pozwala im odwoływać się do standardowych zmiennych statycznych lub lokalnych. Nie ma arytmetyki wskaźników. Wskaźniki również muszą mieć skojarzony typ, a wskaźnik do jednego typu nie jest zgodny ze wskaźnikiem do innego typu (np. wskaźnik do znaku nie jest zgodny ze wskaźnikiem do liczby całkowitej). Pomaga to wyeliminować problemy z zabezpieczeniami typów związane z innymi implementacjami wskaźników, szczególnie tymi używanymi dla PL/I lub C . Usuwa również pewne ryzyko powodowane przez wiszące wskaźniki , ale możliwość dynamicznego puszczania przestrzeni odniesienia przy użyciu disposestandardowej procedury (która ma taki sam efekt jak freefunkcja biblioteczna z C ) oznacza, że ​​ryzyko zwieszonych wskaźników nie zostało całkowicie wyłączony.

Jednak w niektórych komercyjnych i open source'owych implementacjach kompilatorów Pascala (lub pochodnych) — takich jak Free Pascal , Turbo Pascal lub Object Pascal w Embarcadero Delphi — wskaźnik może odwoływać się do standardowych zmiennych statycznych lub lokalnych i może być rzutowany z jednego typu wskaźnika na inne. Ponadto, wskaźnik arytmetyka jest nieograniczony: dodawanie lub odejmowanie od wskaźnika porusza się przez to liczby bajtów w każdym kierunku, ale z wykorzystaniem Incalbo Decstandardowych procedur z przenosi wskaźnik od rozmiaru danych typu jest uznany do wskazują. Pod nazwą znajduje się również wskaźnik bez typu Pointer, który jest zgodny z innymi typami wskaźników.

Perl

Język programowania Perl obsługuje wskaźniki, choć rzadko używane, w postaci funkcji pack i unpack. Są one przeznaczone tylko do prostych interakcji ze skompilowanymi bibliotekami systemu operacyjnego. We wszystkich innych przypadkach Perl używa referencji , które są wpisywane i nie pozwalają na arytmetykę wskaźników. Służą do konstruowania złożonych struktur danych.

Zobacz też

Bibliografia

Zewnętrzne linki