Wskaźniki w C: kiedy używać znaku handlowego i gwiazdki?

298

Zaczynam od wskazówek i jestem trochę zdezorientowany. Wiem &oznacza adres zmiennej, który *może być użyty przed zmienną wskaźnika, aby uzyskać wartość obiektu wskazywanego przez wskaźnik. Ale wszystko działa inaczej, gdy pracujesz z tablicami, łańcuchami lub gdy wywołujesz funkcje z kopią wskaźnika zmiennej. Trudno w tym wszystkim dostrzec logikę.

Kiedy powinienem używać &i *?

Pieter
źródło
5
Zilustruj, jak postrzegasz rzeczy, które czasami działają inaczej. W przeciwnym razie musimy zgadnąć, co Cię dezorientuje.
Drew Dormann,
1
Zgadzam się z Neilem Butterworthem. Prawdopodobnie zdobędę znacznie więcej informacji na podstawie książki z pierwszej ręki, a wyjaśnienie K&R jest dość jasne.
Tom
144
Nie zgadzam się z wami wszystkimi, którzy twierdzą, że zadawanie tego typu pytań na SO nie jest dobrym pomysłem. SO stało się zasobem numer 1 podczas wyszukiwania w Google. Odpowiedzi te są niewystarczające. Przeczytaj odpowiedź Dana Olsona. Ta odpowiedź jest naprawdę wnikliwa i niezwykle pomocna dla początkujących. RTFMjest niepomocny i, szczerze mówiąc, bardzo niegrzeczny. Jeśli nie masz czasu na odpowiedź, szanuj tych, którzy poświęcają czas na udzielenie odpowiedzi na te pytania. Chciałbym móc to zrobić „anonowi”, ale oczywiście on / ona nie ma czasu, aby wnieść jakiś znaczący wkład.
SSH
18
To, co powiedział SSH , jest absolutnie prawdziwe. Niektórzy ludzie krzyczą „Po prostu Google”, ale obecnie jest na odwrót: „Spójrz na StackOverflow”. To pytanie jest przydatne dla wielu osób. (Stąd głosy poparcia i brak głosów negatywnych.)
MC Emperor,

Odpowiedzi:

610

Masz wskaźniki i wartości:

int* p; // variable p is pointer to integer type
int i; // integer value

Wskaźnik zmienia się w wartość za pomocą *:

int i2 = *p; // integer i2 is assigned with integer value that pointer p is pointing to

Przekształcasz wartość we wskaźnik za pomocą &:

int* p2 = &i; // pointer p2 will point to the address of integer i

Edycja: W przypadku tablic są one traktowane bardzo podobnie do wskaźników. Jeśli myślisz o nich jako o wskaźnikach, będziesz używał ich *do uzyskania wartości w nich zawartych, jak wyjaśniono powyżej, ale jest też inny, bardziej powszechny sposób używania []operatora:

int a[2];  // array of integers
int i = *a; // the value of the first element of a
int i2 = a[0]; // another way to get the first element

Aby uzyskać drugi element:

int a[2]; // array
int i = *(a + 1); // the value of the second element
int i2 = a[1]; // the value of the second element

Tak więc []operator indeksowania jest specjalną formą *operatora i działa w następujący sposób:

a[i] == *(a + i);  // these two statements are the same thing
Dan Olson
źródło
2
Dlaczego to nie działa? int aX[] = {3, 4}; int *bX = &aX;
Pieter
5
Tablice są specjalne i można je w przejrzysty sposób konwertować na wskaźniki. Podkreśla to inny sposób przejścia ze wskaźnika do wartości, dodam go do powyższego wyjaśnienia.
Dan Olson
4
Jeśli dobrze to zrozumiem ... przykład int *bX = &aX;nie działa, ponieważ aXjuż zwraca adres aX[0](tj. &aX[0]), Więc &aXotrzyma adres, który nie ma sensu. Czy to jest poprawne?
Pieter,
6
Masz rację, chociaż są przypadki, w których możesz chcieć adresu tego adresu. W takim przypadku zadeklarujesz to jako int ** bX = i aX, ale jest to bardziej zaawansowany temat.
Dan Olson
3
@Dan, podany int aX[] = {3,4};, int **bX = &aX;jest błędem. &aXjest typu „wskaźnik do tablicy [2] int”, a nie „wskaźnik do wskaźnika do int”. W szczególności nazwa tablicy nie jest traktowana jako wskaźnik do jej pierwszego elementu dla jedności &. Możesz zrobić:int (*bX)[2] = &aX;
Alok Singhal
28

W przypadku tablic i funkcji istnieje pewien wzorzec; na początku jest to trochę trudne.

Kiedy mamy do czynienia z tablicami, warto pamiętać, że: gdy wyrażenie tablicowe pojawia się w większości kontekstów, typ wyrażenia jest domyślnie konwertowany z „tablicy N-elementowej T” na „wskaźnik do T”, a jego wartość jest ustawiana wskazywać pierwszy element w tablicy. Wyjątki od tej reguły są, gdy ekspresja tablicy pojawia się jako operand albo &czy sizeofoperatorzy, lub gdy jest to dosłowne ciąg używany jako inicjator w deklaracji.

Zatem, gdy wywołasz funkcję z wyrażeniem tablicowym jako argumentem, funkcja otrzyma wskaźnik, a nie tablicę:

int arr[10];
...
foo(arr);
...

void foo(int *arr) { ... }

Dlatego nie używasz &operatora do argumentów odpowiadających „% s” w scanf():

char str[STRING_LENGTH];
...
scanf("%s", str);

Z powodu niejawnej konwersji scanf()otrzymuje char *wartość wskazującą na początek strtablicy. Dotyczy to każdej funkcji wywoływanej z wyrażeniem tablicowym jako argumentem (prawie dowolna z str*funkcji *scanfi *printffunkcji itp.).

W praktyce prawdopodobnie nigdy nie wywołasz funkcji z wyrażeniem tablicowym za pomocą &operatora, jak w:

int arr[N];
...
foo(&arr);

void foo(int (*p)[N]) {...}

Taki kod nie jest bardzo powszechny; musisz znać rozmiar tablicy w deklaracji funkcji, a funkcja działa tylko ze wskaźnikami do tablic o określonych rozmiarach (wskaźnik do 10-elementowej tablicy T jest innego typu niż wskaźnik do 11-elementowej tablicy z T).

Gdy wyrażenie tablicowe pojawia się jako operator dla &operatora, typem wynikowego wyrażenia jest „wskaźnik do tablicy N-elementowej T” lub T (*)[N], która różni się od tablicy wskaźników ( T *[N]) i wskaźnika do typu podstawowego ( T *).

W przypadku funkcji i wskaźników należy pamiętać o następującej zasadzie: jeśli chcesz zmienić wartość argumentu i odzwierciedlić go w kodzie wywołującym, musisz przekazać wskaźnik do rzeczy, którą chcesz zmodyfikować. Znów tablice wrzucają trochę małpiego klucza do prac, ale najpierw zajmiemy się normalnymi przypadkami.

Pamiętaj, że C przekazuje wszystkie argumenty funkcji według wartości; parametr formalny otrzymuje kopię wartości parametru rzeczywistego, a wszelkie zmiany parametru formalnego nie są odzwierciedlane w parametrze faktycznym. Typowym przykładem jest funkcja zamiany:

void swap(int x, int y) { int tmp = x; x = y; y = tmp; }
...
int a = 1, b = 2;
printf("before swap: a = %d, b = %d\n", a, b);
swap(a, b);
printf("after swap: a = %d, b = %d\n", a, b);

Otrzymasz następujące dane wyjściowe:

przed zamianą: a = 1, b = 2
po zamianie: a = 1, b = 2

Parametry formalne xi ysą odrębnymi obiektami od ai b, więc zmiany xi ynie są odzwierciedlone w ai b. Ponieważ chcemy zmodyfikować wartości ai b, musimy przekazać do nich wskaźniki do funkcji zamiany:

void swap(int *x, int *y) {int tmp = *x; *x = *y; *y = tmp; }
...
int a = 1, b = 2;
printf("before swap: a = %d, b = %d\n", a, b);
swap(&a, &b);
printf("after swap: a = %d, b = %d\n", a, b);

Teraz twój wynik będzie

przed zamianą: a = 1, b = 2
po zamianie: a = 2, b = 1

Zauważ, że w funkcji wymiany nie zmieniamy wartości xi y, ale wartości what xi y point to . Pisanie do *xróżni się od pisania do x; nie aktualizujemy samej wartości x, uzyskujemy lokalizację xi aktualizujemy wartość w tej lokalizacji.

Jest to równie prawdziwe, jeśli chcemy zmodyfikować wartość wskaźnika; jeśli napiszemy

int myFopen(FILE *stream) {stream = fopen("myfile.dat", "r"); }
...
FILE *in;
myFopen(in);

następnie modyfikujemy wartość parametru wejściowego stream, a nie to, na co stream wskazuje , więc zmiana streamnie ma wpływu na wartość in; aby to zadziałało, musimy przekazać wskaźnik do wskaźnika:

int myFopen(FILE **stream) {*stream = fopen("myFile.dat", "r"); }
...
FILE *in;
myFopen(&in);

Znów tablice wrzucają do prac trochę klucza małpiego. Gdy przekazujesz wyrażenie tablicowe do funkcji, funkcja otrzymuje wskaźnik. Ze względu na to, jak zdefiniowane jest indeksowanie tablic, można użyć operatora indeksu dolnego na wskaźniku w taki sam sposób, jak można go użyć na tablicy:

int arr[N];
init(arr, N);
...
void init(int *arr, int N) {size_t i; for (i = 0; i < N; i++) arr[i] = i*i;}

Zauważ, że obiekty tablicowe nie mogą być przypisane; tzn. nie możesz zrobić czegoś takiego

int a[10], b[10];
...
a = b;

więc chcesz zachować ostrożność, mając do czynienia ze wskaźnikami do tablic; coś jak

void (int (*foo)[N])
{
  ...
  *foo = ...;
}

nie zadziała.

John Bode
źródło
16

Mówiąc prościej

  • &oznacza adres , zobaczysz, że w symbolach zastępczych funkcji modyfikujących zmienną parametru, jak w C, zmienne parametrów są przekazywane przez wartość, przy użyciu znaku handlowego i środków, aby przejść przez odniesienie.
  • *oznacza dereferencję zmiennej wskaźnikowej, co oznacza uzyskanie wartości tej zmiennej wskaźnikowej.
int foo(int *x){
   *x++;
}

int main(int argc, char **argv){
   int y = 5;
   foo(&y);  // Now y is incremented and in scope here
   printf("value of y = %d\n", y); // output is 6
   /* ... */
}

Powyższy przykład ilustruje, jak wywołać funkcję fooza pomocą funkcji pass-by-reference, porównaj z tym

int foo(int x){
   x++;
}

int main(int argc, char **argv){
   int y = 5;
   foo(y);  // Now y is still 5
   printf("value of y = %d\n", y); // output is 5
   /* ... */
}

Oto ilustracja użycia dereferencji

int main(int argc, char **argv){
   int y = 5;
   int *p = NULL;
   p = &y;
   printf("value of *p = %d\n", *p); // output is 5
}

Powyższe ilustruje, w jaki sposób uzyskaliśmy adres y i przypisaliśmy go do zmiennej wskaźnika p. Następnie odchylamy p się, przyczepiając *przód, aby uzyskać wartość p, tj *p.

t0mm13b
źródło
10

Tak, to może być dość skomplikowane, ponieważ *jest używane do wielu różnych celów w C / C ++.

Jeśli *pojawia się przed już zadeklarowaną zmienną / funkcją, oznacza to, że:

  • a) *daje dostęp do wartości tej zmiennej (jeśli typ tej zmiennej jest wskaźnikiem lub przeciążył *operatora).
  • b) *ma znaczenie operatora mnożenia, w takim przypadku po lewej stronie musi być inna zmienna*

Jeśli *pojawia się w deklaracji zmiennej lub funkcji, oznacza to, że zmienna jest wskaźnikiem:

int int_value = 1;
int * int_ptr; //can point to another int variable
int   int_array1[10]; //can contain up to 10 int values, basically int_array1 is an pointer as well which points to the first int of the array
//int   int_array2[]; //illegal, without initializer list..
int int_array3[] = {1,2,3,4,5};  // these two
int int_array4[5] = {1,2,3,4,5}; // are identical

void func_takes_int_ptr1(int *int_ptr){} // these two are identical
void func_takes int_ptr2(int int_ptr[]){}// and legal

Jeśli &pojawia się w deklaracji zmiennej lub funkcji, oznacza to ogólnie, że zmienna jest odniesieniem do zmiennej tego typu.

Jeśli &pojawia się przed już zadeklarowaną zmienną, zwraca adres tej zmiennej

Dodatkowo powinieneś wiedzieć, że przekazując tablicę do funkcji, zawsze będziesz musiał przekazać jej tablicę, z wyjątkiem sytuacji, gdy tablica jest czymś w rodzaju cstringu zakończonego 0 (tablica char).

Smerlin
źródło
1
@akmozo s / func_takes int_ptr2 / func_takes_int_ptr2 / (nieprawidłowe miejsce)
PixnBits
4

Kiedy deklarujesz zmienną wskaźnika lub parametr funkcji, użyj *:

int *x = NULL;
int *y = malloc(sizeof(int)), *z = NULL;
int* f(int *x) {
    ...
}

Uwaga: każda deklarowana zmienna potrzebuje własnej *.

Jeśli chcesz wziąć adres wartości, użyj &. Jeśli chcesz odczytać lub zapisać wartość we wskaźniku, użyj *.

int a;
int *b;
b = f(&a);
a = *b;

a = *f(&a);

Tablice są zwykle traktowane jak wskaźniki. Kiedy deklarujesz parametr tablicy w funkcji, możesz równie łatwo zadeklarować, że jest wskaźnikiem (to znaczy to samo). Gdy przekazujesz tablicę do funkcji, faktycznie przekazujesz wskaźnik do pierwszego elementu.

Wskaźniki funkcji są jedynymi rzeczami, które nie do końca przestrzegają zasad. Możesz pobrać adres funkcji bez użycia & i możesz wywołać wskaźnik funkcji bez użycia *.

Jay Conrod
źródło
4

Przeglądałem wszystkie trudne wyjaśnienia, więc zamiast tego zwróciłem się do wideo z University of New South Wales na ratunek. Oto proste wyjaśnienie: jeśli mamy komórkę, która ma adres xi wartość 7, pośrednim sposobem na podanie adresu wartości 7jest &7a pośrednim sposobem na podanie wartości pod adresem xjest *x. Więc. (cell: x , value: 7) == (cell: &7 , value: *x)Innym sposobem, aby na to spojrzeć: Johnsiedzi w 7th seat. *7th seatBędzie wskazywać Johni &Johnda address/ lokalizację 7th seat. To proste wyjaśnienie pomogło mi i mam nadzieję, że pomoże również innym. Oto link do doskonałego wideo: kliknij tutaj.

Oto inny przykład:

#include <stdio.h>

int main()
{ 
    int x;            /* A normal integer*/
    int *p;           /* A pointer to an integer ("*p" is an integer, so p
                       must be a pointer to an integer) */

    p = &x;           /* Read it, "assign the address of x to p" */
    scanf( "%d", &x );          /* Put a value in x, we could also use p here */
    printf( "%d\n", *p ); /* Note the use of the * to get the value */
    getchar();
}

Dodatek : Zawsze inicjuj wskaźnik przed użyciem, jeśli nie, wskaźnik wskaże na cokolwiek, co może spowodować awarię programu, ponieważ system operacyjny uniemożliwi dostęp do pamięci, o której nie wiesz, ale po prostu p = &x;, przypisujemy wskaźnik do określonej lokalizacji.


źródło
3

Właściwie masz to w zanadrzu, nic więcej nie musisz wiedzieć :-)

Dodałbym tylko następujące bity:

  • dwie operacje są przeciwnymi końcami widma. &bierze zmienną i podaje adres, *pobiera adres i daje zmienną (lub treść).
  • tablice „degradują” wskaźniki po przekazaniu ich do funkcji.
  • możesz mieć wiele poziomów pośrednich ( char **poznacza to, że pjest wskaźnikiem do wskaźnika do char.

Jeśli chodzi o rzeczy działające inaczej, to nie tak naprawdę:

  • tablice, jak już wspomniano, ulegają degradacji do wskaźników (do pierwszego elementu w tablicy) po przekazaniu do funkcji; nie zachowują informacji o rozmiarze.
  • w C nie ma żadnych ciągów, tylko tablice znaków, które zgodnie z konwencją reprezentują ciąg znaków zakończony znakiem zero ( \0).
  • Gdy przekazujesz adres zmiennej do funkcji, możesz cofnąć odwołanie do wskaźnika, aby zmienić samą zmienną (zwykle zmienne są przekazywane przez wartość (z wyjątkiem tablic)).
paxdiablo
źródło
3

Myślę, że jesteś trochę zdezorientowany. Powinieneś przeczytać dobry tutorial / książkę na temat wskaźników.

Ten samouczek jest bardzo dobry na początek (jasno wyjaśnia, co &i jakie *są). I tak, nie zapomnij przeczytać książki Pointers in C autorstwa Kenneth Reek.

Różnica między &i *jest bardzo wyraźna.

Przykład:

#include <stdio.h>

int main(){
  int x, *p;

  p = &x;         /* initialise pointer(take the address of x) */
  *p = 0;         /* set x to zero */
  printf("x is %d\n", x);
  printf("*p is %d\n", *p);

  *p += 1;        /* increment what p points to i.e x */
  printf("x is %d\n", x);

  (*p)++;         /* increment what p points to i.e x */
  printf("x is %d\n", x);

  return 0;
}
Prasoon Saurav
źródło
1

Ok, wygląda na to, że Twój post został edytowany ...

double foo[4];
double *bar_1 = &foo[0];

Widzisz, jak możesz użyć, &aby uzyskać adres początku struktury tablicy? Następujące

Foo_1(double *bar, int size){ return bar[size-1]; }
Foo_2(double bar[], int size){ return bar[size-1]; }

zrobi to samo.

pszenica
źródło
Pytanie zostało oznaczone C, a nie C ++.
Prasoon Saurav,
1
I usunąłem obrażające cout <<
banatki