Czym dokładnie jest wskaźnik C, jeśli nie jest to adres pamięci?


206

W renomowanym źródle o C po omówieniu &operatora podane są następujące informacje :

... To trochę niefortunne, że terminologia [adres] pozostaje, ponieważ dezorientuje tych, którzy nie wiedzą, o co chodzi w adresach, i wprowadza w błąd tych, którzy to robią: myślenie o wskaźnikach tak, jakby były adresami, zwykle prowadzi do smutku ... .

Inne materiały, które przeczytałem (powiedziałbym, że z równie renomowanych źródeł) zawsze bezwstydnie odnosiły się do wskaźników i &operatora, podając adresy pamięci. Chciałbym nadal szukać aktualności sprawy, ale jest to trochę trudne, gdy renomowane źródła KIND OF nie zgadzają się.

Teraz jestem nieco zdezorientowany - co to właściwie jest wskaźnik, jeśli nie adres pamięci?

PS

Później autor mówi: ... Będę jednak nadal używał terminu „adres”, ponieważ wymyślenie innego [terminu] byłoby jeszcze gorsze.


118
Wskaźnik jest zmienna , która przechowuje adres. Ma też swój własny adres. Jest to podstawowa różnica między wskaźnikiem a tablicą. Tablica skutecznie jest adresem (i domyślnie jej adres jest sam ).
WhozCraig

7
Jakie jest twoje „renomowane źródło” oferty?
Cornstalks

22
Ostatecznym renomowanym źródłem jest standard językowy, a nie książki częściowo z niego pochodzące i częściowo wyciągnięte z tyłka autora. Nauczyłem się tego na własnej skórze, popełniając prawie każdy błąd, jaki mogłem, i powoli budowałem model mentalny C nieco zbliżony do opisanego przez standard, a następnie w końcu zastępowałem ten model modelem standardowym.
Alexey Frunze

9
@thang Ludzie myślą, że wskaźnik = liczba całkowita, ponieważ często tak jest (x86 Linux i Windows „uczą” nas tego), ponieważ ludzie uwielbiają uogólnianie, ponieważ ludzie nie znają dobrze standardu językowego i ponieważ mieli niewielkie doświadczenie z radykalnie różnymi platformy. Te same osoby prawdopodobnie zakładają, że wskaźnik do danych i wskaźnik do funkcji można konwertować na siebie, a dane można wykonywać jako kod, a kod można uzyskać jako dane. Chociaż może to być prawda w architekturach von Neumana (z 1 przestrzenią adresową), ale niekoniecznie jest tak w architekturach Harvarda (z kodem i przestrzeniami danych).
Alexey Frunze

6
@exebook Standardy nie są dla początkujących (szczególnie kompletnych). Nie powinny one zapewniać łagodnego wprowadzenia i mnóstwa przykładów. Formalnie coś definiują, więc mogą być poprawnie wdrożone przez profesjonalistów.
Alexey Frunze

Odpowiedzi:


148

Standard C nie określa, czym jest wewnętrzny wskaźnik i jak działa wewnętrznie. Jest to celowe, aby nie ograniczać liczby platform, na których C można zaimplementować jako język kompilowany lub interpretowany.

Wartość wskaźnika może być jakimś identyfikatorem lub uchwytem lub kombinacją kilku identyfikatorów (przywitaj się z segmentami i przesunięciami x86) i niekoniecznie rzeczywistym adresem pamięci. Ten identyfikator może być dowolny, nawet ciąg tekstowy o stałym rozmiarze. Reprezentacje bezadresowe mogą być szczególnie przydatne dla interpretera C.


34
Nie ma wiele do wyjaśnienia. Każda zmienna ma swój adres w pamięci. Ale nie musisz przechowywać ich adresów we wskaźnikach do nich. Zamiast tego możesz ponumerować zmienne od 1 do dowolnej i zapisać tę liczbę we wskaźniku. Jest to całkowicie legalne według standardu językowego, o ile implementacja wie, jak przekształcić te liczby w adresy i jak wykonywać arytmetykę wskaźników z tymi liczbami i wszystkimi innymi rzeczami wymaganymi przez standard.
Alexey Frunze

4
Chciałbym dodać, że na x86 adres pamięci składa się z selektora segmentu i przesunięcia, więc reprezentuje wskaźnik jako segment: przesunięcie nadal używa adresu pamięci.
Thang 1'13

6
@Lundin Nie mam problemów z ignorowaniem ogólnej natury standardu i nie ma zastosowania, gdy znam swoją platformę i mój kompilator. Pierwotne pytanie jest jednak ogólne, więc nie można zignorować standardu, odpowiadając na nie.
Alexey Frunze

8
@Lundin Nie musisz być rewolucjonistą ani naukowcem. Załóżmy, że chcesz emulować maszynę 32-bitową na fizycznym komputerze 16-bitowym i rozszerzysz 64 KB pamięci RAM do 4 GB za pomocą pamięci dyskowej i zaimplementujesz 32-bitowe wskaźniki jako przesunięcia w dużym pliku. Te wskaźniki nie są prawdziwymi adresami pamięci.
Alexey Frunze

6
Najlepszym przykładem, jaki kiedykolwiek widziałem, była implementacja C dla Symbolics Lisp Machines (około 1990). Każdy obiekt C został zaimplementowany jako tablica Lisp, a wskaźniki zostały zaimplementowane jako para tablicy i indeksu. Ze względu na sprawdzanie granic tablicy Lisp, nigdy nie można było przepełnić jednego obiektu do drugiego.
Barmar

62

Nie jestem pewien co do twojego źródła, ale opisywany przez ciebie język pochodzi ze standardu C:

6.5.3.2 Operatory adresu i pośrednictwa
[...]
3. Operator jednoargumentowy i operator podaje adres swojego operandu. [...]

Więc ... tak, wskaźniki wskazują adresy pamięci. Przynajmniej tak sugeruje standard C.

Mówiąc bardziej precyzyjnie, wskaźnik to zmienna przechowująca wartość jakiegoś adresu . Adres obiektu (który może być przechowywany we wskaźniku) jest zwracany przez jednoargumentowy &operator.

Mogę zapisać adres „42 Wallaby Way, Sydney” w zmiennej (i ta zmienna byłaby swego rodzaju „wskaźnikiem”, ale ponieważ nie jest to adres pamięci, nie nazwalibyśmy go właściwie „wskaźnikiem”). Twój komputer ma adresy pojemników pamięci. Wskaźniki przechowują wartość adresu (tzn. Wskaźnik przechowuje wartość „42 Wallaby Way, Sydney”, która jest adresem).

Edycja: Chcę rozwinąć komentarz Alexey Frunze.

Czym dokładnie jest wskaźnik? Spójrzmy na standard C:

6.2.5 Rodzaje
[...]
20. [...] typu wskaźnik może pochodzić od typu funkcji lub typu obiektu, zwany odwołuje typu . Typ wskaźnika opisuje obiekt, którego wartość zapewnia odwołanie do encji określonego typu. Typ wskaźnika pochodzący od typu odniesienia T jest czasem nazywany „wskaźnikiem do T”. Konstrukcja typu wskaźnika z przywoływanego typu nosi nazwę „wyprowadzenie typu wskaźnika”. Typ wskaźnika jest kompletnym typem obiektu.

Zasadniczo wskaźniki przechowują wartość, która zapewnia odniesienie do jakiegoś obiektu lub funkcji. Rodzaj. Wskaźniki są przeznaczone do przechowywania wartości, która zapewnia odwołanie do jakiegoś obiektu lub funkcji, ale nie zawsze tak jest:

6.3.2.3 Wskaźniki
[...]
5. Liczba całkowita może być przekształcona na dowolny typ wskaźnika. Z wyjątkiem przypadków określonych wcześniej, wynik jest zdefiniowany w implementacji, może nie być poprawnie wyrównany, może nie wskazywać na encję typu odniesienia i może być reprezentacją pułapki.

Powyższy cytat mówi, że możemy zamienić liczbę całkowitą na wskaźnik. Jeśli to zrobimy (to znaczy, jeśli umieścimy we wskaźniku wartość całkowitą zamiast konkretnego odwołania do obiektu lub funkcji), wówczas wskaźnik „może nie wskazywać na jednostkę typu odniesienia” (tzn. Może nie dostarczyć odwołanie do obiektu lub funkcji). Może dostarczyć nam czegoś innego. I to jest jedno miejsce, w którym możesz umieścić jakiś uchwyt lub identyfikator we wskaźniku (tzn. Wskaźnik nie wskazuje na obiekt; przechowuje wartość, która coś reprezentuje, ale ta wartość może nie być adresem).

Tak więc, jak mówi Alexey Frunze, możliwe, że wskaźnik nie przechowuje adresu obiektu lub funkcji. Możliwe, że wskaźnik przechowuje zamiast tego jakiś „uchwyt” lub identyfikator, i można to zrobić, przypisując wskaźnikowi dowolną wartość całkowitą. To, co reprezentuje ten uchwyt lub identyfikator, zależy od systemu / środowiska / kontekstu. Tak długo, jak twój system / implementacja może zrozumieć wartość, jesteś w dobrej formie (ale to zależy od konkretnej wartości i konkretnego systemu / implementacji).

Zwykle wskaźnik przechowuje adres obiektu lub funkcji. Jeśli nie jest przechowywany rzeczywisty adres (do obiektu lub funkcji), wynik jest zdefiniowany w implementacji (co oznacza, że ​​dokładnie to, co się dzieje i co reprezentuje teraz wskaźnik, zależy od systemu i implementacji, więc może to być uchwyt lub identyfikator na określonego systemu, ale użycie tego samego kodu / wartości w innym systemie może spowodować awarię programu).

To skończyło się dłużej, niż myślałem, że będzie ...


3
W interpretatorze C wskaźnik może zawierać identyfikator nieadresowy / uchwyt / itp.
Alexey Frunze

4
@exebook Standard nie jest w żaden sposób ograniczony do skompilowanego C.
Alexey Frunze

7
@Lundin Bravo! Zignorujmy standard bardziej! Jakbyśmy nie zignorowali go wystarczająco i nie stworzyliśmy z tego powodu błędnego i słabo przenośnego oprogramowania. Proszę również nie twierdzić, że pierwotne pytanie jest ogólne i jako takie wymaga ogólnej odpowiedzi.
Alexey Frunze

3
Kiedy inni mówią, że wskaźnik może być uchwytem lub czymś innym niż adres, nie oznaczają one tylko, że można zmusić dane do wskaźnika poprzez rzutowanie liczby całkowitej na wskaźnik. Oznacza to, że kompilator może wykorzystywać coś innego niż adresy pamięci do implementacji wskaźników. W procesorze Alpha z ABI DEC wskaźnik funkcji nie był adresem funkcji, ale adresem deskryptora funkcji, a deskryptor zawierał adres funkcji i niektóre dane o parametrach funkcji. Chodzi o to, że standard C jest bardzo elastyczny.
Eric Postpischil

5
@Lundin: Twierdzenie, że wskaźniki są implementowane jako adresy całkowite w 100% istniejących systemów komputerowych w świecie rzeczywistym, jest fałszywe. Istnieją komputery z adresowaniem słów i adresowaniem z przesunięciem segmentu. Kompilatory nadal istnieją z obsługą wskaźników bliskich i dalekich. Istnieją komputery PDP-11 z RSX-11 i Konstruktorem zadań i jego nakładkami, w których wskaźnik musi identyfikować informacje potrzebne do załadowania funkcji z dysku. Wskaźnik nie może mieć adresu pamięci obiektu, jeśli obiektu nie ma w pamięci!
Eric Postpischil

39

Wskaźnik vs zmienna

Na tym zdjęciu,

pointer_p to wskaźnik, który znajduje się w 0x12345 i wskazuje zmienną zmienną_v w 0x34567.


16
Nie tylko nie odnosi się to do pojęcia adresu w przeciwieństwie do wskaźnika, ale integralnie pomija punkt, w którym adres nie jest tylko liczbą całkowitą.
Gilles „SO- przestań być zły”

19
-1, to tylko wyjaśnia, czym jest wskaźnik. To nie był question-- a ty odpychając wszystkie zawiłości, że pytanie jest o.
Alexis

34

Myślenie o wskaźniku jako o adresie jest przybliżeniem . Jak wszystkie przybliżenia, czasami jest wystarczająco użyteczny, ale czasem nie jest dokładny, co oznacza, że ​​poleganie na nim powoduje problemy.

Wskaźnik jest jak adres, ponieważ wskazuje, gdzie znaleźć obiekt. Jednym z bezpośrednich ograniczeń tej analogii jest to, że nie wszystkie wskaźniki faktycznie zawierają adres. NULLjest wskaźnikiem, który nie jest adresem. Zawartość zmiennej wskaźnikowej może w rzeczywistości być jednego z trzech rodzajów:

  • adres obiektu, który może być dereferencjonowane (jeśli pzawiera adres xczym ekspresja *pma taką samą wartość, co x);
  • wskaźnik zerowa , przy czym NULLjest przykładem;
  • niepoprawna treść, która nie wskazuje na obiekt (jeśli pnie ma prawidłowej wartości, *pmoże zrobić wszystko („niezdefiniowane zachowanie”), z awarią programu dość powszechną możliwością).

Ponadto dokładniej byłoby powiedzieć, że wskaźnik (jeśli jest poprawny i nie jest pusty) zawiera adres: wskaźnik wskazuje, gdzie znaleźć obiekt, ale wiąże się z nim więcej informacji.

W szczególności wskaźnik ma typ. Na większości platform typ wskaźnika nie ma wpływu w czasie wykonywania, ale ma wpływ wykraczający poza typ w czasie kompilacji. Jeśli pjest wskaźnikiem do int( int *p;), to p + 1wskazuje na liczbę całkowitą, która jest sizeof(int)bajtem później p(zakładając, że p + 1nadal jest poprawnym wskaźnikiem). Jeśli qwskaźnik do chartego wskazuje na ten sam adres co p( char *q = p;), q + 1to nie jest to ten sam adres co p + 1. Jeśli myślisz o wskaźniku jako adresach, nie jest zbyt intuicyjne, że „następny adres” jest inny dla różnych wskaźników w tej samej lokalizacji.

W niektórych środowiskach możliwe jest posiadanie wielu wartości wskaźnika z różnymi reprezentacjami (różne wzorce bitów w pamięci), które wskazują na to samo miejsce w pamięci. Można je traktować jako różne wskaźniki posiadające ten sam adres lub jako różne adresy dla tej samej lokalizacji - w tym przypadku metafora nie jest jasna. ==Operator zawsze powie Ci, czy oba operandy są skierowane w tym samym miejscu, więc na tych warunkach można mieć p == qchociaż pi qmają różne wzory bitowe.

Istnieją nawet środowiska, w których wskaźniki przenoszą inne informacje poza adres, takie jak informacje o typie lub zezwoleniu. Możesz łatwo przejść przez życie jako programista, nie spotykając się z nimi.

Istnieją środowiska, w których różne rodzaje wskaźników mają różne reprezentacje. Możesz myśleć o tym jako o różnych rodzajach adresów posiadających różne reprezentacje. Na przykład niektóre architektury mają wskaźniki bajtów i wskaźników słów lub wskaźniki obiektów i wskaźników funkcji.

Podsumowując, myślenie o wskaźnikach jako adresach nie jest takie złe, o ile się o tym pamięta

  • to tylko poprawne, niepuste wskaźniki, które są adresami;
  • możesz mieć wiele adresów dla tej samej lokalizacji;
  • nie można wykonywać arytmetyki na adresach i nie ma na nich porządku;
  • wskaźnik przenosi również informacje o typie.

Odwrotna sytuacja jest o wiele bardziej kłopotliwa. Nie wszystko, co wygląda jak adres, może być wskaźnikiem . Gdzieś głęboko w dół dowolny wskaźnik jest reprezentowany jako wzór bitowy, który można odczytać jako liczbę całkowitą, i można powiedzieć, że ta liczba całkowita jest adresem. Ale w drugą stronę, nie każda liczba całkowita jest wskaźnikiem.

Najpierw są pewne znane ograniczenia; na przykład liczba całkowita oznaczająca lokalizację poza przestrzenią adresową programu nie może być prawidłowym wskaźnikiem. Niewłaściwie ustawiony adres nie stanowi poprawnego wskaźnika dla typu danych, który wymaga wyrównania; na przykład na platformie, która intwymaga wyrównania 4 bajtów, 0x7654321 nie może być prawidłową int*wartością.

Jednak wykracza to daleko poza to, że gdy zrobisz wskaźnik na liczbę całkowitą, czeka cię świat kłopotów. Duża część tego problemu polega na tym, że optymalizacja kompilatorów jest znacznie lepsza w mikrooptymalizacji, niż się spodziewa większość programistów, tak więc ich mentalny model działania programu jest głęboko błędny. To, że masz wskaźniki o tym samym adresie, nie oznacza, że ​​są one równoważne. Rozważmy na przykład następujący fragment kodu:

unsigned int x = 0;
unsigned short *p = (unsigned short*)&x;
p[0] = 1;
printf("%u = %u\n", x, *p);

Można się spodziewać, że na maszynie run-of-the-mill, gdzie sizeof(int)==4a sizeof(short)==2, to albo drukuje 1 = 1?(little-endian) lub 65536 = 1?(big-endian). Ale na moim 64-bitowym komputerze z systemem Linux z GCC 4.4:

$ c99 -O2 -Wall a.c && ./a.out 
a.c: In function main’:
a.c:6: warning: dereferencing pointer p does break strict-aliasing rules
a.c:5: note: initialized from here
0 = 1?

GCC jest na tyle uprzejmy, aby ostrzec nas, co dzieje się nie tak w tym prostym przykładzie - w bardziej złożonych przykładach kompilator może nie zauważyć. Ponieważ pma inny typ niż &xzmiana, które ppunkty nie mogą wpływać na które &xpunkty (poza pewnymi dobrze zdefiniowanymi wyjątkami). Dlatego kompilator może zachować wartość xrejestru i nie aktualizować tego rejestru jako *pzmian. Program usuwa dwa wskaźniki do tego samego adresu i uzyskuje dwie różne wartości!

Morał tego przykładu jest taki, że myślenie o (nieważnym) wskaźniku jako adresie jest w porządku, pod warunkiem, że przestrzegasz precyzyjnych reguł języka C. Drugą stroną monety jest to, że reguły języka C są skomplikowane i trudne do uzyskania intuicyjnego wyczucia, chyba że wiesz, co dzieje się pod maską. A pod maską jest to, że więź między wskaźnikami i adresami jest nieco luźna, zarówno w celu obsługi „egzotycznych” architektur procesorów, jak i w celu optymalizacji kompilatorów.

Pomyśl więc, że wskaźniki są adresami jako pierwszym krokiem do zrozumienia, ale nie podążaj za intuicją zbyt daleko.


5
+1. Wydaje się, że brakuje innych odpowiedzi, że wskaźnik zawiera informacje o typie. Jest to o wiele ważniejsze niż adres / identyfikator / jakakolwiek dyskusja.
undur_gongor

+1 Doskonałe punkty za informację o typie. Nie jestem pewien, czy przykłady kompilatora są poprawne, chociaż ... Wydaje się, że *p = 3jest mało prawdopodobne, na przykład, że odniesie sukces, gdy p nie zostanie zainicjowane.
LarsH

@ LarS Masz rację, dzięki, jak to napisałem? Zastąpiłem go przykładem, który pokazuje nawet zaskakujące zachowanie na moim komputerze.
Gilles „SO- przestań być zły”

1
um, NULL jest ((void *) 0) ..?
Aniket Inge

1
@ gnasher729 Wskaźnik zerowy jest wskaźnikiem. NULLnie jest, ale dla wymaganego poziomu szczegółowości jest to nieistotne rozproszenie. Nawet w przypadku codziennego programowania, fakt, który NULLmoże być zaimplementowany jako coś, co nie mówi „wskaźnik”, nie pojawia się często (głównie przechodząc NULLdo funkcji variadic - ale nawet tam, jeśli jej nie używasz) , już przyjmujesz założenie, że wszystkie typy wskaźników mają tę samą reprezentację).
Gilles „SO- przestań być zły”

19

Wskaźnik to zmienna przechowująca adres pamięci, a nie sam adres. Możesz jednak wyrejestrować wskaźnik i uzyskać dostęp do lokalizacji pamięci.

Na przykład:

int q = 10; /*say q is at address 0x10203040*/
int *p = &q; /*means let p contain the address of q, which is 0x10203040*/
*p = 20; /*set whatever is at the address pointed by "p" as 20*/

Otóż ​​to. To takie proste.

wprowadź opis zdjęcia tutaj

Program do zademonstrowania tego, co mówię i jego wyników jest tutaj:

http://ideone.com/rcSUsb

Program:

#include <stdio.h>

int main(int argc, char *argv[])
{
  /* POINTER AS AN ADDRESS */
  int q = 10;
  int *p = &q;

  printf("address of q is %p\n", (void *)&q);
  printf("p contains %p\n", (void *)p);

  p = NULL;
  printf("NULL p now contains %p\n", (void *)p);
  return 0;
}

5
Może jeszcze bardziej pomylić. Alice, widzisz kota? Nie, widzę tylko uśmiech kota. Zatem powiedzenie, że wskaźnik jest adresem lub wskaźnikiem, jest zmienną, która przechowuje adres, lub powiedzenie, że wskaźnik jest nazwą pojęcia odnoszącego się do idei adresu, jak daleko pisarze książek mogą posuwać się w zamieszanie dla potrzebujących?
exebook

@exebook do tych przyprawionych wskaźnikami, jest to dość proste. Może zdjęcie pomoże?
Aniket Inge

5
Wskaźnik niekoniecznie zawiera adres. W interpretatorze C może to być coś innego, coś w rodzaju identyfikatora / uchwytu.
Alexey Frunze

„Etykieta” lub nazwa zmiennej jest kompilatorem / asemblerem i nie istnieje na poziomie komputera, więc nie sądzę, że powinna pojawić się w pamięci.
Ben

1
@Aniket Zmienna wskaźnika może zawierać wartość wskaźnika. Musisz zapisać wynik fopenw zmiennej tylko wtedy, gdy musisz użyć jej więcej niż jeden raz (bo fopentak naprawdę jest to cały czas).
Gilles „SO- przestań być zły”

16

Trudno powiedzieć dokładnie, co dokładnie znaczą autorzy tych książek. To, czy wskaźnik zawiera adres, zależy od tego, jak zdefiniujesz adres i jak zdefiniujesz wskaźnik.

Sądząc po wszystkich napisanych odpowiedziach, niektórzy zakładają, że (1) adres musi być liczbą całkowitą, a (2) wskaźnik nie musi być wirtualny, ponieważ nie podano tego w specyfikacji. Przy tych założeniach jasne wskaźniki niekoniecznie zawierają adresy.

Widzimy jednak, że chociaż (2) jest prawdopodobnie prawdziwe, (1) prawdopodobnie nie musi być prawdziwe. A co sądzić o tym, że & jest nazywany adresem operatora zgodnie z odpowiedzią @ CornStalks? Czy to oznacza, że ​​autorzy specyfikacji zamierzają, aby wskaźnik zawierał adres?

Czy możemy więc powiedzieć, że wskaźnik zawiera adres, ale adres nie musi być liczbą całkowitą? Może.

Wydaje mi się, że wszystko to jest rozmownym semantycznym rozmowem pedantycznym. Jest praktycznie całkowicie bezwartościowy. Czy możesz pomyśleć o kompilatorze, który generuje kod w taki sposób, że wartość wskaźnika nie jest adresem? Jeśli tak to co? Tak myślałem...

Myślę, że to, do czego autor książki (pierwszy fragment, który twierdzi, że wskaźniki niekoniecznie są tylko adresami) prawdopodobnie odnosi się do faktu, że wskaźnik zawiera nieodłączną informację o typie.

Na przykład,

 int x;
 int* y = &x;
 char* z = &x;

oba y i z są wskaźnikami, ale y + 1 i z + 1 są różne. jeśli są to adresy pamięci, czy te wyrażenia nie dadzą ci takiej samej wartości?

I tutaj leży myślenie o wskaźnikach tak, jakby były adresami, zwykle prowadzi do smutku . Błędy zostały napisane, ponieważ ludzie myślą o wskaźnikach jak o adresach , a to zwykle prowadzi do smutku .

55555 prawdopodobnie nie jest wskaźnikiem, chociaż może być adresem, ale (int *) 55555 jest wskaźnikiem. 55555 + 1 = 55556, ale (int *) 55555 + 1 to 55559 (różnica +/- pod względem wielkościof (int)).


1
+1 za wskazanie arytmetyki wskaźnika nie jest tym samym, co arytmetyka adresów.
kutschkem

W przypadku 16-bitowego 8086 adres pamięci jest opisany przez bazę segmentu + przesunięcie, oba 16 bitów. Istnieje wiele kombinacji podstawy segmentu + przesunięcia, które dają ten sam adres w pamięci. Ten farwskaźnik nie jest po prostu „liczbą całkowitą”.
vonbrand

@ vonbrand Nie rozumiem, dlaczego opublikowałeś ten komentarz. kwestia ta została omówiona jako komentarz pod innymi odpowiedziami. prawie każda inna odpowiedź zakłada, że ​​adres = liczba całkowita i wszystko, co nie jest liczbą całkowitą, nie jest adresem. po prostu zaznaczam to i zauważam, że może to być poprawne. cała moja odpowiedź w odpowiedzi brzmi: nie ma to znaczenia. wszystko to jest po prostu pedantyczne, a główny problem nie został poruszony w innych odpowiedziach.
dniu

@tang, pomysł „wskaźnik == adres” jest błędny . To, że wszyscy i ich ulubiona ciotka nadal mówią, nie ma racji.
vonbrand,

@vonbrand i dlaczego skomentowałeś ten komentarz pod moim postem? Nie powiedziałem, że to jest dobre czy złe. W rzeczywistości jest to właściwe w niektórych scenariuszach / założeniach, ale nie zawsze. Pozwól mi jeszcze raz podsumować punkt postu (po raz drugi). cała moja odpowiedź w odpowiedzi brzmi: nie ma to znaczenia. wszystko to jest po prostu pedantyczne, a główny problem nie został poruszony w innych odpowiedziach. właściwsze byłoby komentowanie odpowiedzi, które zawierają twierdzenie, że wskaźnik == adres lub adres == liczba całkowita. zobacz moje komentarze pod postem Alexeya w odniesieniu do segmentu: offset.
dniu

15

Wskaźnik jest abstrakcją reprezentującą lokalizację pamięci. Zauważ, że cytat nie mówi, że myślenie o wskaźnikach tak, jakby były adresami pamięci, jest błędne, po prostu mówi, że „zwykle prowadzi do smutku”. Innymi słowy, prowadzi to do niepoprawnych oczekiwań.

Najbardziej prawdopodobnym źródłem smutku jest z pewnością arytmetyka wskaźników, która w rzeczywistości jest jedną z mocnych stron C. Gdyby wskaźnik był adresem, można oczekiwać, że arytmetyka wskaźnika będzie arytmetyką adresu; ale nie jest. Na przykład dodanie 10 do adresu powinno dać adres większy o 10 jednostek adresowych; ale dodanie 10 do wskaźnika zwiększa go o 10-krotność wielkości obiektu, na który wskazuje (a nawet rzeczywistego rozmiaru, ale zaokrąglonego w górę do granicy wyrównania). Przy int *zwykłej architekturze z 32-bitowymi liczbami całkowitymi dodanie 10 do niego zwiększyłoby ją o 40 jednostek adresowych (bajtów). Doświadczeni programiści C zdają sobie z tego sprawę i żyją z tym, ale twój autor najwyraźniej nie jest fanem niechlujnych metafor.

Istnieje dodatkowe pytanie, w jaki sposób zawartość wskaźnika reprezentuje lokalizację pamięci: Jak wyjaśniło wiele odpowiedzi, adres nie zawsze jest liczbą całkowitą (lub długą). W niektórych architekturach adres to „segment” plus przesunięcie. Wskaźnik może zawierać nawet przesunięcie do bieżącego segmentu (wskaźnik „bliski”), który sam w sobie nie jest unikalnym adresem pamięci. Zawartość wskaźnika może mieć tylko pośredni związek z adresem pamięci, gdy sprzęt go rozumie. Ale autor cytowanego cytatu nawet nie wspomina o reprezentacji, więc myślę, że miała na myśli równoważność konceptualną, a nie reprezentację.


12

Oto jak wyjaśniłem to niektórym zdezorientowanym ludziom w przeszłości: Wskaźnik ma dwa atrybuty, które wpływają na jego zachowanie. Ma wartość , która jest (w typowych środowiskach) adresem pamięci i typem , który informuje o typie i rozmiarze obiektu, na który wskazuje.

Na przykład biorąc pod uwagę:

union {
    int i;
    char c;
} u;

Możesz mieć trzy różne wskaźniki wskazujące na ten sam obiekt:

void *v = &u;
int *i = &u.i;
char *c = &u.c;

Jeśli porównasz wartości tych wskaźników, wszystkie są równe:

v==i && i==c

Jeśli jednak zwiększysz każdy wskaźnik, zobaczysz, że typ , na który wskazują, staje się odpowiedni.

i++;
c++;
// You can't perform arithmetic on a void pointer, so no v++
i != c

Zmienne ii cbędą miały w tym momencie różne wartości, ponieważ i++powodują izawarcie adresu najbliższej liczby całkowitej i c++powoduje cwskazanie znaku następnego adresowalnego. Zazwyczaj liczby całkowite zajmują więcej pamięci niż znaków, więc ikończy się na większej wartości niż cpo zwiększeniu obu znaków .


2
+1 Dziękuję. Dzięki wskaźnikom wartość i rodzaj są tak nierozłączne, jak człowiek może oddzielić ciało od duszy.
Aki Suihkonen,

i == cjest źle sformułowany (wskaźniki można porównywać z różnymi typami tylko wtedy, gdy istnieje niejawna konwersja z jednego na drugi). Co więcej, poprawienie tego za pomocą rzutowania oznacza, że ​​zastosowałeś konwersję, a następnie można dyskutować, czy konwersja zmienia wartość, czy nie. (Możesz twierdzić, że tak nie jest, ale to tylko potwierdza to samo, co próbujesz udowodnić na tym przykładzie).
MM

8

Mark Bessey już to powiedział, ale należy to ponownie podkreślić, dopóki nie zostanie zrozumiane.

Wskaźnik ma tyle samo wspólnego ze zmienną, co literał 3.

Wskaźnik jest krotką wartości (adresu) i typu (z dodatkowymi właściwościami, takimi jak tylko do odczytu). Typ (i dodatkowe parametry, jeśli istnieją) mogą dodatkowo definiować lub ograniczać kontekst; na przykład. __far ptr, __near ptr: jaki jest kontekst adresu: stos, stos, adres liniowy, przesunięcie gdzieś, pamięć fizyczna lub co.

Jest to właściwość typu, która sprawia, że ​​arytmetyka wskaźnika nieco różni się od arytmetyki liczb całkowitych.

Przeciwnych przykładów wskaźnika braku bycia zmienną jest zbyt wiele, aby je zignorować

  • fopen zwraca wskaźnik PLIKU. (gdzie jest zmienna)

  • wskaźnik stosu lub wskaźnik ramki są zwykle rejestrami nieadresowalnymi

    *(int *)0x1231330 = 13; - rzutowanie dowolnej liczby całkowitej na typ wskaźnik_integera i zapisywanie / czytanie liczb całkowitych bez wprowadzania zmiennej

W czasie istnienia programu C będzie wiele innych wystąpień wskaźników tymczasowych, które nie mają adresów - i dlatego nie są zmiennymi, ale wyrażeniami / wartościami z typem związanym z czasem kompilacji.


8

Masz rację i rozum. Zwykle wskaźnik jest tylko adresem, więc możesz rzucić go na liczbę całkowitą i wykonać dowolną arytmetykę.

Ale czasami wskaźniki są tylko częścią adresu. W niektórych architekturach wskaźnik jest konwertowany na adres z dodatkiem bazy lub używany jest inny rejestr procesora .

Ale w dzisiejszych czasach, w architekturze PC i ARM z natywną kompilacją modelu pamięci i językiem C, można pomyśleć, że wskaźnik jest liczbą całkowitą w jednym miejscu w jednowymiarowej adresowalnej pamięci RAM.


PC ... płaski model pamięci? czym są selektory?
Thang 1'13

Riight. A kiedy nadejdzie kolejna zmiana architektury, być może z osobnym kodem i przestrzeniami danych, albo ktoś wróci do czcigodnej architektury segmentu (co ma ogromne znaczenie dla bezpieczeństwa, może nawet dodać jakiś klucz do numeru segmentu + przesunięcie, aby sprawdzić uprawnienia) piękne „wskaźniki to tylko liczby całkowite” upaść.
vonbrand

7

Wskaźnik, jak każda inna zmienna w C, jest zasadniczo zbiorem bitów, które mogą być reprezentowane przez jedną lub więcej połączonych unsigned charwartości (jak w przypadku każdego innego typu cariable, sizeof(some_variable)wskaże liczbę unsigned charwartości). Tym, co odróżnia wskaźnik od innych zmiennych, jest to, że kompilator C interpretuje bity wskaźnika jako identyfikujące w jakiś sposób miejsce, w którym zmienna może być przechowywana. W C, w przeciwieństwie do niektórych innych języków, można poprosić o miejsce dla wielu zmiennych, a następnie przekonwertować wskaźnik na dowolną wartość w tym zestawie na wskaźnik na dowolną inną zmienną w tym zestawie.

Wiele kompilatorów implementuje wskaźniki za pomocą swoich bitów przechowujących rzeczywiste adresy maszyn, ale nie jest to jedyna możliwa implementacja. Implementacja może utrzymywać jedną tablicę - niedostępną dla kodu użytkownika - wyświetlającą adres sprzętowy i przydzielony rozmiar wszystkich obiektów pamięci (zestawów zmiennych) używanych przez program, a każdy wskaźnik zawiera indeks w tablicy wzdłuż z przesunięciem od tego indeksu. Taki projekt pozwoliłby systemowi nie tylko ograniczyć kod do działania tylko na pamięci, którą posiadał, ale także zapewnić, że wskaźnik do jednego elementu pamięci nie może zostać przypadkowo przekształcony w wskaźnik do innego elementu pamięci (w systemie, który używa sprzętu adresy, jeśli fooi barsą tablicami 10 pozycji, które są kolejno przechowywane w pamięci, wskaźnik do „jedenastej” pozycjifoomoże zamiast tego wskazywać pierwszy element bar, ale w systemie, w którym każdy „wskaźnik” jest identyfikatorem obiektu i przesunięciem, system może pułapkować, jeśli kod spróbuje zindeksować wskaźnik foopoza swój przydzielony zakres). Byłoby również możliwe, aby taki system wyeliminował problemy związane z fragmentacją pamięci, ponieważ adresy fizyczne związane z dowolnymi wskaźnikami mogą być przenoszone.

Zauważ, że chociaż wskaźniki są nieco abstrakcyjne, nie są wystarczająco abstrakcyjne, aby umożliwić kompilatorowi C w pełni zgodnemu ze standardami wdrożenie modułu wyrzucania elementów bezużytecznych. Kompilator C określa, że ​​każda zmienna, w tym wskaźniki, jest reprezentowana jako sekwencja unsigned charwartości. Biorąc pod uwagę dowolną zmienną, można ją rozłożyć na sekwencję liczb, a następnie przekształcić tę sekwencję liczb z powrotem na zmienną pierwotnego typu. W związku z tym program byłby możliwycalloctrochę pamięci (otrzymanie do niej wskaźnika), zapisz coś tam, rozłóż wskaźnik na serię bajtów, wyświetl te na ekranie, a następnie usuń wszystkie odniesienia do nich. Jeśli następnie program zaakceptuje niektóre liczby z klawiatury, odtworzy je do wskaźnika, a następnie spróbuje odczytać dane z tego wskaźnika, a jeśli użytkownik wprowadzi te same liczby, które program wcześniej wyświetlał, program będzie musiał wysłać dane które zostały zapisane w callocpamięci. Ponieważ nie ma możliwego sposobu, aby komputer wiedział, czy użytkownik wykonał kopię wyświetlanych liczb, nie byłoby możliwe, aby komputer wiedział, czy wspomniana pamięć będzie kiedykolwiek dostępna w przyszłości.


Przy dużym obciążeniu może być w stanie wykryć użycie wartości wskaźnika, która może „wyciec” jego wartość liczbową, i przypiąć przyporządkowanie, aby śmieciarz go nie zbierał ani nie przenosił (o ile freeoczywiście nie jest to jawnie wywoływane). To, czy wynikowa implementacja byłaby tak przydatna, to inna sprawa, ponieważ jej zdolność do kolekcjonowania może być zbyt ograniczona, ale możesz przynajmniej nazwać ją zbieraczem śmieci :-) Przypisanie wskaźnika i arytmetyka nie „wyciekłyby” wartości, ale każdy dostęp do char*nieznanego pochodzenia musiałby zostać sprawdzony.
Steve Jessop,

@ SteveJessop: Myślę, że taki projekt byłby gorszy niż bezużyteczny, ponieważ nie byłoby możliwe, aby kod wiedział, jakie wskaźniki należy uwolnić. Śmieciarze, którzy zakładają, że wszystko, co wygląda jak wskaźnik, może być zbyt konserwatywny, ale generalnie rzeczy, które wyglądają - ale nie są - wskaźniki mają możliwość zmiany, unikając w ten sposób „trwałych” wycieków pamięci. Każda czynność, która wygląda na to, że rozkłada wskaźnik na bajty trwale zamraża wskaźnik, jest gwarantowaną receptą na wycieki pamięci.
supercat

Myślę, że i tak to się nie powiedzie ze względu na wydajność - jeśli chcesz, aby Twój kod działał tak wolno, ponieważ każdy dostęp jest sprawdzany, nie zapisuj go w C ;-) Mam większe nadzieje na pomysłowość programistów C niż ty, ponieważ uważam, że chociaż jest to niewygodne, prawdopodobnie nie jest wykluczone niepotrzebne przypinanie przydziałów. W każdym razie C ++ definiuje „bezpiecznie wyprowadzone wskaźniki” właśnie po to, aby poradzić sobie z tym problemem, więc wiemy, co zrobić, jeśli kiedykolwiek chcemy zwiększyć abstrakcyjność wskaźników C do poziomu, na którym obsługują one dość skuteczne usuwanie śmieci.
Steve Jessop,

@SteveJessop: Aby system GC był użyteczny, powinien być w stanie niezawodnie zwolnić pamięć, na której freenie został wywołany, lub zapobiec, aby jakiekolwiek odwołanie do uwolnionego obiektu stało się odniesieniem do obiektu na żywo [nawet przy użyciu zasobów, które wymagają wyraźne zarządzanie czasem życia, GC może nadal użytecznie wykonywać tę ostatnią funkcję]; system GC, który czasem fałszywie traktuje obiekty jako posiadające żywe odniesienia do nich, może być użyteczny, jeśli prawdopodobieństwo niepotrzebnego przypięcia N obiektów jednocześnie zbliża się do zera, gdy N staje się duże . Chyba że ktoś chce
zgłosić

... dla kodu, który jest poprawny w C ++, ale dla którego kompilator nie byłby w stanie udowodnić, że wskaźnik nigdy nie może zostać przekształcony w nierozpoznawalną formę, nie rozumiem, jak można uniknąć ryzyka, że ​​program, który w rzeczywistości nigdy nie używa wskaźników, ponieważ liczby całkowite mogą być fałszywie uznawane za takie.
supercat

6

Wskaźnik jest typem zmiennej, który jest natywnie dostępny w C / C ++ i zawiera adres pamięci. Jak każda inna zmienna ma swój własny adres i zajmuje pamięć (ilość zależy od platformy).

Jednym z problemów, który zobaczysz w wyniku zamieszania, jest próba zmiany odnośnika w funkcji przez zwykłe przekazanie wskaźnika przez wartość. Spowoduje to skopiowanie wskaźnika w zakresie funkcji i wszelkie zmiany w miejscach, w których ten nowy wskaźnik „wskazuje” nie zmieni odniesienia do wskaźnika w zakresie, który wywołał funkcję. Aby zmodyfikować rzeczywisty wskaźnik w funkcji, zwykle przekazuje się wskaźnik do wskaźnika.


1
Ogólnie jest to uchwyt / identyfikator. Zwykle jest to zwykły adres.
Alexey Frunze

Dostosowałem swoją odpowiedź, aby być trochę bardziej PC na definicję Handle w wikipedii. Lubię odwoływać się do wskaźników jako szczególnego wystąpienia uchwytu, ponieważ uchwyt może być po prostu odniesieniem do wskaźnika.
Matthew Sanders

6

KRÓTKIE PODSUMOWANIE (które również umieszczę na górze):

(0) Myślenie o wskaźnikach jako adresach jest często dobrym narzędziem do nauki i często stanowi faktyczną implementację wskaźników do zwykłych typów danych.

(1) Ale w wielu, być może większości, kompilatorach wskaźniki do funkcji nie są adresami, ale są większe niż adres (zazwyczaj 2x, czasem więcej), lub w rzeczywistości są wskaźnikami do struktury w pamięci niż zawierają adresy funkcji i rzeczy takich jak stała pula.

(2) Wskaźniki do członków danych i wskaźniki do metod są często jeszcze dziwniejsze.

(3) Starszy kod x86 z problemami ze wskaźnikami FAR i NEAR

(4) Kilka przykładów, w szczególności IBM AS / 400, z bezpiecznymi „wskaźnikami tłuszczu”.

Jestem pewien, że możesz znaleźć więcej.

SZCZEGÓŁ:

UMMPPHHH !!!!! Wiele dotychczasowych odpowiedzi jest dość typowymi odpowiedziami na „programistów” - ale nie na kompilatory ani na sprzętowe. Ponieważ udaję, że jestem zaprzeczeniem sprzętowym i często pracuję z kompilatorami, pozwól mi wrzucić moje dwa centy:

W wielu, prawdopodobnie większości kompilatorach C, wskaźnikiem do danych typu Tjest w rzeczywistości adres T.

W porządku.

Ale nawet w wielu z tych kompilatorów pewne wskaźniki NIE są adresami. Możesz to powiedzieć, patrząc na sizeof(ThePointer).

Na przykład wskaźniki do funkcji są czasami znacznie większe niż zwykłe adresy. Lub mogą obejmować poziom pośredni. Ten artykułzawiera jeden opis dotyczący procesora Intel Itanium, ale widziałem inne. Zwykle, aby wywołać funkcję, musisz znać nie tylko adres kodu funkcji, ale także adres stałej puli funkcji - obszar pamięci, z którego stałe są ładowane za pomocą pojedynczej instrukcji ładowania, zamiast kompilatora, który musi generować 64-bitowa stała z kilku instrukcji Load Immediate oraz Shift i OR. Tak więc zamiast jednego 64-bitowego adresu potrzebujesz 2 64-bitowych adresów. Niektóre ABI (interfejsy binarne aplikacji) przenoszą to jako 128 bitów, podczas gdy inne używają poziomu pośredniego, przy czym wskaźnik funkcji faktycznie jest adresem deskryptora funkcji, który zawiera 2 właśnie wspomniane adresy. Który jest lepszy? Zależy od twojego punktu widzenia: wydajność, rozmiar kodu, oraz niektóre problemy ze zgodnością - często kod zakłada, że ​​wskaźnik może być rzutowany na długi lub długi długi, ale może również zakładać, że długi długi ma dokładnie 64 bity. Taki kod może nie być zgodny ze standardami, ale klienci mogą chcieć, aby działał.

Wielu z nas ma bolesne wspomnienia o starej architekturze segmentowej Intel x86, z BLISKIMI WSKAŹNIKAMI i DALSZYMI WSKAŹNIKAMI. Na szczęście są już prawie wymarłe, więc tylko krótkie podsumowanie: w 16-bitowym trybie rzeczywistym rzeczywisty adres liniowy był

LinearAddress = SegmentRegister[SegNum].base << 4 + Offset

W trybie chronionym może tak być

LinearAddress = SegmentRegister[SegNum].base + offset

wynikowy adres jest sprawdzany pod kątem limitu ustawionego w segmencie. Niektóre programy nie używały tak naprawdę standardowych deklaracji wskaźników FAR i NEAR C / C ++, ale wiele z nich właśnie powiedziało *T--- ale były przełączniki kompilatora i linkera, więc na przykład wskaźniki kodu mogą znajdować się w pobliżu wskaźników, tylko 32-bitowe przesunięcie w stosunku do tego, co jest w rejestr CS (segment segmentu), podczas gdy wskaźnikami danych mogą być wskaźniki FAR, określające zarówno 16-bitowy numer segmentu, jak i 32-bitowe przesunięcie dla wartości 48-bitowej. Teraz obie te wielkości są z pewnością powiązane z adresem, ale ponieważ nie są one tego samego rozmiaru, który z nich jest adresem? Co więcej, segmenty zawierały również uprawnienia - tylko do odczytu, do odczytu i zapisu, wykonywalne - oprócz rzeczy związanych z rzeczywistym adresem.

Bardziej interesującym przykładem IMHO jest (lub być może była) rodzina IBM AS / 400. Ten komputer był jednym z pierwszych, którzy wdrożyli system operacyjny w C ++. Wskaźniki na tym machime zazwyczaj były dwukrotnie większe niż rzeczywisty rozmiar adresu - np. Jako ta prezentacjamówi, 128-bitowe wskaźniki, ale rzeczywiste adresy miały 48-64 bity, i znowu kilka dodatkowych informacji, co nazywa się zdolnością, która zapewniała uprawnienia, takie jak odczyt, zapis, a także limit zapobiegający przepełnieniu bufora. Tak: możesz to zrobić kompatybilnie z C / C ++ - a gdyby było to wszechobecne, chińska PLA i słowiańska mafia nie włamaliby się do tak wielu zachodnich systemów komputerowych. Ale historycznie większość programów C / C ++ zaniedbywała bezpieczeństwo wydajności. Co najciekawsze, rodzina AS400 pozwoliła systemowi operacyjnemu na tworzenie bezpiecznych wskaźników, które mogłyby być przekazywane nieuprzywilejowanemu kodowi, ale których nieuprawniony kod nie mógł sfałszować ani manipulować. Ponownie, bezpieczeństwo, i chociaż jest zgodny ze standardami, dużo niechlujny, niezgodny ze standardami kod C / C ++ nie będzie działał w tak bezpiecznym systemie. Ponownie istnieją oficjalne standardy,

Teraz zsiadam z mydła bezpieczeństwa i wspomnę o kilku innych sposobach, w których wskaźniki (różnych typów) często nie są tak naprawdę adresami: Wskaźniki do elementów danych, wskaźniki do metod funkcji składowych, a ich statyczne wersje są większe niż zwykły adres. Jak mówi ten post :

Istnieje wiele sposobów rozwiązania tego [problemy związane z dziedziczeniem pojedynczym i wielokrotnym oraz dziedziczeniem wirtualnym]. Oto, w jaki sposób kompilator Visual Studio decyduje się go obsłużyć: Wskaźnik do funkcji składowej wielokrotnie odziedziczonej klasy jest naprawdę strukturą. ”Dalej mówią:„ Rzutowanie wskaźnika funkcji może zmienić jego rozmiar! ”.

Jak zapewne możecie zgadnąć z mojego pontyfikatu dotyczącego (nie) bezpieczeństwa, brałem udział w projektach sprzętowych / programowych C / C ++, w których wskaźnik był traktowany bardziej jak zdolność niż surowy adres.

Mógłbym kontynuować, ale mam nadzieję, że wpadłeś na pomysł.

KRÓTKIE PODSUMOWANIE (które również umieszczę na górze):

(0) myślenie o wskaźnikach jako adresach jest często dobrym narzędziem do nauki i często stanowi faktyczną implementację wskaźników do zwykłych typów danych.

(1) Ale w wielu, być może większości, kompilatorach wskaźniki do funkcji nie są adresami, ale są większe niż adres (zazwyczaj 2X, czasem więcej), lub w rzeczywistości są wskaźnikami do struktury w pamięci niż zawierają adresy funkcji i rzeczy takich jak stała pula.

(2) Wskaźniki do członków danych i wskaźniki do metod są często jeszcze dziwniejsze.

(3) Starszy kod x86 z problemami ze wskaźnikami FAR i NEAR

(4) Kilka przykładów, w szczególności IBM AS / 400, z bezpiecznymi „wskaźnikami tłuszczu”.

Jestem pewien, że możesz znaleźć więcej.


W 16-bitowym trybie rzeczywistym LinearAddress = SegmentRegister.Selector * 16 + Offset(notatka 16, bez przesunięcia o 16). W trybie chronionym LinearAddress = SegmentRegister.base + offset(bez mnożenia dowolnego rodzaju, podstawa segment jest przechowywany w GDT i LDT / zapisywane w rejestrze segmentu , jak to ).
Alexey Frunze

Masz również rację co do podstawy segmentu. Źle zapamiętałem. Jest to limit segmentu opcjonalnie wielokrotny przez 4K. Baza segmentów musi być po prostu rozszyfrowana przez sprzęt, gdy ładuje deskryptor segmentu z pamięci do rejestru segmentów.
Krazy Glew

4

Wskaźnik to po prostu kolejna zmienna, która służy do przechowywania adresu miejsca w pamięci (zwykle adresu innej zmiennej).


Więc pointee to tak naprawdę adres pamięci? Nie zgadzasz się z autorem? Po prostu próbuję zrozumieć.
d0rmLife

Podstawową funkcją wskaźnika jest wskazywanie na coś. Jak dokładnie to się osiąga i czy istnieje prawdziwy adres, czy nie, nie jest zdefiniowane. Wskaźnik może być tylko identyfikatorem / uchwytem, ​​a nie prawdziwym adresem.
Alexey Frunze

4

Możesz to zobaczyć w ten sposób. Wskaźnik to wartość reprezentująca adres w adresowalnej przestrzeni pamięci.


2
Wskaźnik niekoniecznie musi zawierać w sobie prawdziwy adres pamięci. Zobacz moją odpowiedź i komentarze pod nią.
Alexey Frunze

what .... wskaźnik do pierwszej zmiennej na stosie nie drukuje 0. drukuje górną (lub dolną) ramkę stosu w zależności od sposobu jej implementacji.
Thang 1'13

@thang Dla pierwszej zmiennej góra i dół są takie same. A jaki jest adres góry lub dołu w tym przypadku stosu?
Valentin Radu

@ValentinRadu, dlaczego nie spróbujesz .. oczywiście nie próbowałeś.
Thang 1'13

2
@thang Masz rację, zrobiłem kilka naprawdę złych założeń, do mojej obrony jest tutaj 5 rano.
Valentin Radu

3

Wskaźnik to po prostu kolejna zmienna, która może zawierać adres pamięci zwykle innej zmiennej. Wskaźnik będący zmienną również ma adres pamięci.


1
Niekoniecznie adres. Przy okazji, czy przeczytałeś istniejące odpowiedzi i komentarze przed opublikowaniem odpowiedzi?
Alexey Frunze

3

Wskaźnik prądu przemiennego jest bardzo podobny do adresu pamięci, ale z oddalonymi szczegółami zależnymi od maszyny, a także niektórymi funkcjami nie znajdującymi się w zestawie instrukcji niższego poziomu.

Na przykład wskaźnik C jest stosunkowo bogato pisany. Jeśli zwiększysz wskaźnik przez szereg struktur, ładnie przeskakuje z jednej struktury do drugiej.

Wskaźniki podlegają regułom konwersji i zapewniają sprawdzanie typu czasu kompilacji.

Istnieje specjalna wartość „wskaźnika zerowego”, która jest przenośna na poziomie kodu źródłowego, ale której reprezentacja może się różnić. Jeśli przypiszesz wskaźnikowi liczbę całkowitą, której wartość wynosi zero, wskaźnik ten przyjmuje wartość wskaźnika zerowego. Podobnie jeśli zainicjujesz wskaźnik w ten sposób.

Wskaźnik może być użyty jako zmienna boolowska: sprawdza wartość true, jeśli jest inna niż null, i false, jeśli ma wartość null.

W języku maszynowym, jeśli wskaźnik zerowy jest śmiesznym adresem, takim jak 0xFFFFFFFF, może być konieczne przeprowadzenie jawnych testów dla tej wartości. C ukrywa to przed tobą. Nawet jeśli wskaźnik zerowy ma wartość 0xFFFFFFFF, możesz go przetestować za pomocą if (ptr != 0) { /* not null! */}.

Wykorzystanie wskaźników, które podważają system typów, prowadzi do nieokreślonego zachowania, podczas gdy podobny kod w języku maszynowym może być dobrze zdefiniowany. Asemblery skompilują napisane instrukcje, ale kompilatory C zoptymalizują się w oparciu o założenie, że nie zrobiłeś nic złego. Jeśli float *pwskaźnik wskazuje long nzmienną i *p = 0.0jest wykonywany, kompilator nie musi tego obsłużyć. Dalsze użycie nnie będzie konieczne odczytanie wzorca bitowego wartości zmiennoprzecinkowej, ale być może będzie to zoptymalizowany dostęp oparty na założeniu „ścisłego aliasingu”, którego nnie zmieniono! To znaczy założenie, że program jest dobrze zachowany i dlatego pnie powinien wskazywać na n.

W C wskaźniki do kodu i wskaźniki do danych są różne, ale w wielu architekturach adresy są takie same. Można opracować kompilatory C, które mają wskaźniki „gruby”, nawet jeśli architektura docelowa nie. Wskaźniki tłuszczu oznaczają, że wskaźniki nie są tylko adresami maszyny, ale zawierają inne informacje, takie jak informacje o wielkości wskazywanego obiektu, do sprawdzania granic. Przenośnie napisane programy łatwo przenoszą się na takie kompilatory.

Jak widać, istnieje wiele różnic semantycznych między adresami maszyny a wskaźnikami C.


Wskaźniki NULL nie działają tak, jak myślisz, że działają na wszystkich platformach - zobacz moją odpowiedź na CiscoIPPhone powyżej. NULL == 0 to założenie, które obowiązuje tylko na platformach x86. Konwencja mówi, że nowe platformy powinny pasować do x86, jednak szczególnie w świecie osadzonym tak nie jest. Edycja: Ponadto C nie robi nic, aby wyodrębnić wartość sposobu wskaźnika ze sprzętu - „ptr! = 0” nie będzie działać jako test NULL na platformie, gdzie NULL! = 0.
DX-MON

1
DX-MON, to jest całkowicie błędne dla standardu C. NULL jest przeznaczony na 0, i mogą być używane zamiennie w instrukcjach. To, czy nie reprezentacja wskaźnika NULL w sprzęcie ma wartość 0 bitów, nie ma znaczenia dla tego, jak jest reprezentowana w kodzie źródłowym.
Mark Bessey,

@ DX-MON Obawiam się, że nie pracujesz z prawidłowymi faktami. W C wyrażenie całkowe stałe służy jako stała wskaźnika zerowego, niezależnie od tego, czy wskaźnik zerowy jest adresem zerowym. Jeśli znasz kompilator C, w którym ptr != 0nie występuje test zerowy, ujawnij jego tożsamość (ale zanim to zrobisz, wyślij raport o błędzie do dostawcy).
Kaz

Rozumiem, do czego zmierzasz, ale twoje komentarze na temat zerowych wskaźników są niespójne, ponieważ mylisz wskaźniki i adresy pamięci - dokładnie tego, czego zaleca cytat cytowany w pytaniu! Prawidłowa instrukcja: C definiuje zerowy wskaźnik na zero, niezależnie od tego, czy adres pamięci przy zerowym przesunięciu jest dozwolony, czy nie.
Alexis

1
@alexis Proszę o rozdział i wiersz. C nie definiuje wskaźnika zerowego jako zero. C definiuje zero (lub dowolne wyrażenie stałej stałej, którego wartość wynosi zero) jako składnię oznaczającą stałą wskaźnika zerowego. faqs.org/faqs/C-faq/faq (sekcja 5).
Kaz

3

Przed zrozumieniem wskaźników musimy zrozumieć obiekty. Obiekty to istnienie, które istnieje i ma specyfikator lokalizacji zwany adresem. Wskaźnik jest po prostu zmienną, jak każda inna zmienna w Ctypie o nazwie, pointerktórego treść jest interpretowana jako adres obiektu, który obsługuje następną operację.

+ : A variable of type integer (usually called offset) can be added to yield a new pointer
- : A variable of type integer (usually called offset) can be subtracted to yield a new pointer
  : A variable of type pointer can be subtracted to yield an integer (usually called offset)
* : De-referencing. Retrieve the value of the variable (called address) and map to the object the address refers to.
++: It's just `+= 1`
--: It's just `-= 1`

Wskaźnik jest klasyfikowany na podstawie typu obiektu, do którego się obecnie odnosi. Jedyną istotną informacją jest rozmiar obiektu.

Każdy obiekt obsługuje operację &(adres), która pobiera specyfikator lokalizacji (adres) obiektu jako typ obiektu wskaźnika. Powinno to przezwyciężyć zamieszanie wokół nomenklatury, ponieważ rozsądnie byłoby wywoływać ją &jako operację obiektu, a nie wskaźnik, którego wynikowy typ jest wskaźnikiem typu obiektu.

Uwaga W tym objaśnieniu pominąłem pojęcie pamięci.


Podoba mi się twoje wyjaśnienie abstrakcyjnej rzeczywistości ogólnego wskaźnika w ogólnym systemie. Być może pomocne byłoby omówienie pamięci. Mówiąc sam za siebie, wiem, że to ...! Myślę, że omówienie połączenia może być bardzo pomocne w zrozumieniu ogólnego obrazu. W każdym razie +1 :)
d0rmLife

@ d0rmLife: W innych odpowiedziach masz wystarczające wyjaśnienie, które obejmuje szerszy obraz. Chciałem tylko wyjaśnić matematyczne streszczenie jako inny pogląd. Również IMHO stworzyłoby mniej zamieszania w wywoływaniu &jako „Adres”, ponieważ jest to bardziej powiązane z Obiektem niż wskaźnikiem per se ”
Abhijit

Bez obrazy, ale sam zdecyduję, jakie jest wystarczające wyjaśnienie. Jeden podręcznik nie wystarcza do pełnego wyjaśnienia struktur danych i przydziału pamięci. ;) .... w każdym razie twoja odpowiedź jest pomocna , nawet jeśli nie jest nowa.
d0rmLife

Posługiwanie się wskaźnikami bez pojęcia pamięci nie ma sensu . Jeśli obiekt istnieje bez pamięci, musi znajdować się w miejscu, w którym nie ma adresu - np. W rejestrach. Aby móc używać „&”, należy założyć pamięć.
Aki Suihkonen,

3

Adres służy do identyfikowania części pamięci o stałym rozmiarze, zwykle dla każdego bajtu, jako liczby całkowitej. Jest to dokładnie nazywane adresem bajtu , który jest również używany przez ISO C. Mogą istnieć inne metody konstruowania adresu, np. Dla każdego bitu. Jednak tak często używany jest tylko adres bajtowy, zwykle pomijamy „bajt”.

Technicznie adres nigdy nie jest wartością w C, ponieważ definicja terminu „wartość” w (ISO) C to:

dokładne znaczenie zawartości obiektu interpretowanego jako posiadający określony typ

(Podkreślone przeze mnie.) Jednak nie ma takiego „typu adresu” w C.

Wskaźnik nie jest taki sam. Wskaźnik jest rodzajem typu w języku C. Istnieje kilka różnych typów wskaźników. Oni niekoniecznie posłuszeństwa do identycznego zestawu reguł języka, np wpływu ++na wartości typu int*vs. char*.

Wartość w C może być typu wskaźnika. Nazywa się to wartością wskaźnika . Dla jasności, wartość wskaźnika nie jest wskaźnikiem w języku C. Ale jesteśmy przyzwyczajeni do mieszania ich ze sobą, ponieważ w C raczej nie będzie to dwuznaczne: jeśli nazwiemy wyrażenie pjako „wskaźnik”, będzie to tylko wartość wskaźnika, ale nie typ, ponieważ nazwany typ w C nie jest wyrażone wyrażeniem , ale nazwą typu lub nazwą typu .

Niektóre inne rzeczy są subtelne. Jako użytkownik C, po pierwsze, należy wiedzieć, co objectoznacza:

region przechowywania danych w środowisku wykonawczym, którego zawartość może reprezentować wartości

Obiekt jest bytem reprezentującym wartości, które są określonego typu. Wskaźnik jest typem obiektu . Jeśli więc zadeklarujemy int* p;, poznacza to „obiekt typu wskaźnika” lub „obiekt wskaźnika”.

Zauważ, że nie ma „zmiennej” normalnie zdefiniowanej przez normę (w rzeczywistości nigdy nie jest używana jako rzeczownik przez ISO C w tekście normatywnym). Jednak nieoficjalnie nazywamy obiekt zmienną, tak jak robi to inny język. (Ale wciąż nie tak dokładnie, np. W C ++ zmienna może być normalnie typu referencyjnego , co nie jest obiektem.) Wyrażenia „obiekt wskaźnika” lub „zmienna wskaźnika” są czasami traktowane jak „wartość wskaźnika” jak powyżej, za pomocą prawdopodobna niewielka różnica. (Kolejny zestaw przykładów to „tablica”).

Ponieważ wskaźnik jest typem, a adres w C jest faktycznie „pozbawiony typu”, wartość wskaźnika z grubsza „zawiera” adres. A wyrażenie typu wskaźnika może dać adres, np

ISO C11 6.5.2.3

3 Jednoargumentowy &operator podaje adres swojego operandu.

Uwaga: to sformułowanie zostało wprowadzone przez WG14 / N1256, tj. ISO C99: TC3. W C99 jest

3 &Operator jednoargumentowy zwraca adres swojego operandu.

Odzwierciedla opinię komisji: adres nie jest wartością wskaźnika zwróconą przez jednoargumentowego &operatora.

Pomimo powyższego sformułowania, nawet w normach nadal panuje bałagan.

ISO C11 6.6

9 Stała adresu to wskaźnik zerowy, wskaźnik do wartości oznaczającej obiekt o czasie przechowywania statycznego lub wskaźnik do oznaczenia funkcji

ISO C ++ 11 5.19

3 ... Wyrażenie stałej adresu jest wyrażeniem stałym rdzenia prvalue typu wskaźnika, które ocenia na adres obiektu o statycznym czasie przechowywania, adres funkcji lub wartość wskaźnika zerowego lub wyrażenie rdzenia prvalue typu std::nullptr_t. ...

(Ostatnia wersja robocza C ++ używa innego sformułowania, więc nie ma tego problemu).

W rzeczywistości zarówno „stała adresowa” w C, jak i „stała adresowa wyrażenia” w C ++ są stałym wyrażeniem typów wskaźników (lub przynajmniej „podobnych do wskaźników” od C ++ 11).

Wbudowany &operator jednoargumentowy nazywany jest w C i C ++ jako „adres”; podobnie std::addressofjest wprowadzony w C ++ 11.

Te nazwy mogą wprowadzać w błąd. Uzyskaną wyrażenie jest typu wskaźnik, tak że będą interpretowane jako: wynik zawiera / daje adres, niż jest adres.


2

Mówi „bo myli tych, którzy nie wiedzą, o co chodzi w adresach” - to też prawda: jeśli dowiesz się, o co chodzi, nie będziesz zdezorientowany. Teoretycznie wskaźnik jest zmienną wskazującą na inną, praktycznie posiada adres, który jest adresem zmiennej, na którą wskazuje. Nie wiem, dlaczego miałbym ukrywać ten fakt, to nie jest nauka o rakietach. Jeśli zrozumiesz wskaźniki, będziesz o krok bliżej, aby zrozumieć, jak działają komputery. Śmiało!


2

Pomyśl o tym, myślę, że to kwestia semantyki. Nie sądzę, aby autor miał rację, ponieważ standard C odnosi się do wskaźnika jako trzymającego adres do obiektu, do którego istnieje odwołanie, jak inni już tu wspominali. Jednak adres! = Adres pamięci. Adres może być naprawdę dowolny, jak w standardzie C, chociaż ostatecznie doprowadzi do adresu pamięci, sam wskaźnik może być identyfikatorem, przesunięciem + selektorem (x86), naprawdę wszystko, o ile może opisać (po mapowaniu) dowolną pamięć adres w przestrzeni adresowalnej.


Wskaźnik przechowuje adres (lub nie, jeśli jest pusty). Ale to jest to dalekie od bycia adres: na przykład dwa wskaźniki do tego samego adresu, ale z innego typu nie są równoważne w wielu sytuacjach.
Gilles „SO- przestań być zły”

@Gilles Jeśli widzisz „istnienie”, ponieważ w int i=5-> i wynosi 5, wówczas wskaźnikiem jest adres tak. Również null ma również adres. Zwykle niepoprawny adres zapisu (ale niekoniecznie patrz tryb rzeczywisty x86), ale jednak adres. Są tak naprawdę tylko 2 wymagania dla null: gwarantuje się, że porówna nierówny wskaźnik do rzeczywistego obiektu, a dwa dowolne wskaźniki null będą równe.
Valentin Radu

Przeciwnie, gwarantuje, że wskaźnik zerowy nie będzie równy adresowi żadnego obiektu. Dereferencja wskaźnika zerowego jest niezdefiniowanym zachowaniem. Dużym problemem przy mówieniu, że „wskaźnikiem jest adres” jest to, że działają one inaczej. Jeśli pjest wskaźnikiem, p+1nie zawsze jest to adres zwiększany o 1.
Gilles 'SO- przestań być zły'

Przeczytaj jeszcze raz komentarz proszę it's guaranteed to compare unequal to a pointer to an actual object. Jeśli chodzi o arytmetykę wskaźnika, nie widzę sensu, wartość wskaźnika jest nadal adresem, nawet jeśli operacja „+” niekoniecznie doda do niego jeden bajt.
Valentin Radu

1

Jeszcze jeden sposób, w jaki wskaźnik C lub C ++ różni się od prostego adresu pamięci z powodu różnych typów wskaźników, których nie widziałem w innych odpowiedziach (chociaż biorąc pod uwagę ich całkowity rozmiar, mogłem go przeoczyć). Ale to chyba najważniejszy, ponieważ nawet doświadczeni programiści C / C ++ mogą się o niego potknąć:

Kompilator może założyć, że wskaźniki niezgodnych typów nie wskazują tego samego adresu, nawet jeśli wyraźnie to robią, co może dać zachowanie, które nie byłoby możliwe przy prostym modelu adresu ==. Rozważ następujący kod (przy założeniu sizeof(int) = 2*sizeof(short)):

unsigned int i = 0;
unsigned short* p = (unsigned short*)&i;
p[0]=p[1]=1;

if (i == 2 + (unsigned short)(-1))
{
  // you'd expect this to execute, but it need not
}

if (i == 0)
{
  // you'd expect this not to execute, but it actually may do so
}

Należy pamiętać, że istnieje wyjątek char*, dlatego char*możliwe jest manipulowanie wartościami (choć niezbyt przenośne).


0

Szybkie podsumowanie: Adres AC to wartość, zwykle reprezentowana jako adres pamięci na poziomie komputera, z określonym typem.

Niekwalifikowane słowo „wskaźnik” jest dwuznaczne. C ma obiekty wskaźnika (zmienne), typy wskaźników, wyrażenia wskaźnika i wartości wskaźnika .

Bardzo często używa się słowa „wskaźnik”, co oznacza „obiekt wskaźnika”, co może prowadzić do pewnych nieporozumień - dlatego staram się używać słowa „wskaźnik” zamiast przymiotnika.

Standard C, przynajmniej w niektórych przypadkach, używa słowa „wskaźnik”, co oznacza „wartość wskaźnika”. Na przykład opis malloc mówi, że „zwraca albo zerowy wskaźnik, albo wskaźnik do przydzielonego miejsca”.

Więc jaki jest adres w C? Jest to wartość wskaźnika, tzn. Wartość określonego typu wskaźnika. (Z wyjątkiem tego, że wartość wskaźnika zerowego niekoniecznie jest określana jako „adres”, ponieważ nie jest to adres niczego).

Opis standardowego &operatora jednoargumentowego mówi, że „podaje adres swojego operandu”. Poza standardem C słowo „adres” jest powszechnie używane w odniesieniu do adresu pamięci (fizycznej lub wirtualnej), zwykle o rozmiarze jednego słowa (cokolwiek „słowo” znajduje się w danym systemie).

„Adres” prądu przemiennego jest zwykle implementowany jako adres maszyny - podobnie jak intwartość C jest zwykle implementowana jako słowo maszynowe. Ale adres C (wartość wskaźnika) to coś więcej niż adres maszyny. Jest to wartość zwykle reprezentowana jako adres maszyny i jest to wartość określonego typu .


0

Wartość wskaźnika to adres. Zmienna wskaźnikowa to obiekt, który może przechowywać adres. Jest to prawda, ponieważ to właśnie standard określa wskaźnik. Ważne jest, aby powiedzieć o tym nowicjuszom C, ponieważ nowicjusze C często nie są pewni różnicy między wskaźnikiem a rzeczą, na którą wskazuje (to znaczy, nie znają różnicy między kopertą a budynkiem). Pojęcie adresu (każdy obiekt ma adres i to właśnie przechowuje wskaźnik) jest ważne, ponieważ rozwiązuje to problem.

Jednak standardowe rozmowy na określonym poziomie abstrakcji. Ci ludzie, o których autor mówi, o których „wiedzą, o co chodzi”, ale którzy są nowicjuszami w C, musieli koniecznie dowiedzieć się o adresach na innym poziomie abstrakcji - być może przez programowanie w asemblerze. Nie ma gwarancji, że implementacja języka C używa tej samej reprezentacji adresów, co używane przez kodery procesorów procesorów (zwanej w tym fragmencie „adresem sklepu”), o której ci ludzie już wiedzą.

Mówi dalej o „całkowicie rozsądnej manipulacji adresem”. Jeśli chodzi o standard C, to w zasadzie nie ma czegoś takiego jak „całkowicie rozsądna manipulacja adresem”. Dodawanie jest zdefiniowane na wskaźnikach i to w zasadzie tyle. Jasne, możesz przekonwertować wskaźnik na liczbę całkowitą, wykonać operacje bitowe lub arytmetyczne, a następnie przekonwertować go z powrotem. Nie gwarantuje się, że będzie działać zgodnie ze standardem, dlatego przed napisaniem tego kodu lepiej wiedzieć, w jaki sposób konkretna implementacja C reprezentuje wskaźniki i wykonuje tę konwersję. To prawdopodobnie korzysta z reprezentacji adresów można się spodziewać, ale to nie robi to twoja wina, bo nie czytać instrukcji. To nie jest zamieszanie, to nieprawidłowa procedura programowania ;-)

Krótko mówiąc, C używa bardziej abstrakcyjnej koncepcji adresu niż autor.

Autorska koncepcja adresu nie jest oczywiście słowem najniższego poziomu w tej sprawie. Z mapami pamięci wirtualnej i adresowaniem fizycznej pamięci RAM na wiele układów scalonych, liczba, którą podajesz CPU jest „adresem sklepu”, do którego chcesz uzyskać dostęp, nie ma w zasadzie nic wspólnego z tym, gdzie dane są faktycznie zlokalizowane sprzętowo. To są wszystkie warstwy pośrednie i reprezentacyjne, ale autor wybrał jedną do uprzywilejowania. Jeśli zamierzasz to zrobić, mówiąc o C, wybierz poziom C do uprzywilejowania !

Osobiście nie sądzę, aby uwagi autora były tak pomocne, z wyjątkiem kontekstu wprowadzenia C do programistów asemblera. Z pewnością nie jest pomocne osobom pochodzącym z języków wyższego poziomu stwierdzenie, że wartości wskaźnika nie są adresami. O wiele lepiej byłoby uznać złożoność niż powiedzieć, że CPU ma monopol na powiedzenie, czym jest adres, a zatem wartości wskaźnika C „nie są” adresami. Są to adresy, ale mogą być napisane w innym języku niż adresy, które ma na myśli. Myślę, że rozróżnienie tych dwóch rzeczy w kontekście C jako „adres” i „adres sklepu” byłoby odpowiednie.


0

Mówiąc wprost, wskaźniki są w rzeczywistości przesuniętą częścią mechanizmu segmentacji, które tłumaczą się na adres liniowy po segmentacji, a następnie na adres fizyczny po stronicowaniu. Adresy fizyczne są faktycznie adresowane od ciebie pamięci RAM.

       Selector  +--------------+         +-----------+
      ---------->|              |         |           |
                 | Segmentation | ------->|  Paging   |
        Offset   |  Mechanism   |         | Mechanism |
      ---------->|              |         |           |
                 +--------------+         +-----------+
        Virtual                   Linear                Physical
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.