Dlaczego adres tablicy jest równy jej wartości w C?

193

W poniższym fragmencie kodu wartości wskaźników i adresy wskaźników różnią się zgodnie z oczekiwaniami.

Ale wartości tablicowe i adresy nie!

Jak to może być?

Wynik

my_array = 0022FF00
&my_array = 0022FF00
pointer_to_array = 0022FF00
&pointer_to_array = 0022FEFC
#include <stdio.h>

int main()
{
  char my_array[100] = "some cool string";
  printf("my_array = %p\n", my_array);
  printf("&my_array = %p\n", &my_array);

  char *pointer_to_array = my_array;
  printf("pointer_to_array = %p\n", pointer_to_array);
  printf("&pointer_to_array = %p\n", &pointer_to_array);

  printf("Press ENTER to continue...\n");
  getchar();
  return 0;
}
Alexandre
źródło
Z FAQ comp.lang.c: - [Więc co oznacza „równoważność wskaźników i tablic” w C? ] ( c-faq.com/aryptr/aryptrequiv.html ) - [Ponieważ odwołania do tablic rozpadają się na wskaźniki, jeśli arr jest tablicą, jaka jest różnica między arr i & arr? ] ( c-faq.com/aryptr/aryvsadr.html ) Lub przeczytaj całą sekcję Tablice i wskaźniki .
jamesdlin
3
Dwa lata temu dodałem odpowiedź ze schematem na to pytanie. Co się sizeof(&array)zwraca?
Grijesh Chauhan

Odpowiedzi:

219

Nazwa tablicy zazwyczaj ocenia się adres pierwszego elementu tablicy, tak arrayi &arraymają taką samą wartość (ale różnych typów, więc array+1i &array+1będzie nie być równe, jeśli tablica jest większa niż długość 1 elementu).

Istnieją dwa wyjątki od tej reguły: kiedy nazwa tablicy jest operandem sizeoflub jednoargumentowym &(adres-of), nazwa odnosi się do samego obiektu tablicy. W ten sposób sizeof arraypodaje rozmiar w bajtach całej tablicy, a nie rozmiar wskaźnika.

Tablica zdefiniowana jako T array[size]będzie miała typ T *. Kiedy / jeśli ją zwiększasz, przechodzisz do następnego elementu w tablicy.

&arrayzwraca się do tego samego adresu, ale przy tej samej definicji tworzy wskaźnik typu T(*)[size]- tj. jest to wskaźnik do tablicy, a nie do pojedynczego elementu. Jeśli zwiększysz ten wskaźnik, doda on rozmiar całej tablicy, a nie rozmiar pojedynczego elementu. Na przykład z takim kodem:

char array[16];
printf("%p\t%p", (void*)&array, (void*)(&array+1));

Możemy oczekiwać, że drugi wskaźnik będzie o 16 większy niż pierwszy (ponieważ jest to tablica 16 znaków). Ponieważ% p zwykle konwertuje wskaźniki na szesnastkowe, może to wyglądać mniej więcej tak:

0x12341000    0x12341010
Jerry Coffin
źródło
3
@Alexandre: &arrayjest wskaźnikiem do pierwszego elementu tablicy, gdzie as arrayodnosi się do całej tablicy. Zasadniczą różnicę można również zaobserwować, porównując sizeof(array)do sizeof(&array). Zauważ jednak, że jeśli przekazujesz arrayjako argument do funkcji, &arrayw rzeczywistości tylko jest przekazywany. Nie można przekazać tablicy według wartości, chyba że jest ona hermetyzowana za pomocą rozszerzenia struct.
Clifford
14
@Clifford: Jeśli przekazujesz tablicę do funkcji, rozpada się ona na wskaźnik do jej pierwszego elementu, tak skutecznie &array[0]jest przekazywany, a nie &arrayktóry byłby wskaźnikiem do tablicy. Może to być dobry wybór, ale myślę, że ważne jest, aby to wyjaśnić; kompilatory będą ostrzegać, jeśli funkcja ma prototyp, który pasuje do typu przekazanego wskaźnika.
CB Bailey
2
@Jerry Coffin Na przykład int * p = & a, jeśli chcę adres pamięci wskaźnika int p, mogę zrobić & p. Ponieważ & tablica konwertuje na adres całej tablicy (która zaczyna się od adresu pierwszego elementu). W takim razie jak mogę znaleźć adres pamięci wskaźnika tablicy (który przechowuje adres pierwszego elementu w tablicy)? To musi być gdzieś w pamięci, prawda?
John Lee,
2
@JohnLee: Nie, nigdzie w pamięci nie musi znajdować się wskaźnik do tablicy. W przypadku utworzenia wskaźnika można następnie jego adres: int *p = array; int **pp = &p;.
Jerry Coffin
3
@Clifford pierwszy komentarz jest zły, po co go nadal trzymać? Myślę, że może to prowadzić do nieporozumień dla tych, którzy nie przeczytają poniższej odpowiedzi (@Charles).
Rick
32

Dzieje się tak, ponieważ nazwa tablicy ( my_array) różni się od wskaźnika do tablicy. Jest to alias adresu tablicy, a jej adres jest zdefiniowany jako adres samej tablicy.

Wskaźnik jest jednak zwykłą zmienną C na stosie. W ten sposób możesz wziąć jego adres i uzyskać inną wartość niż adres, który zawiera.

Pisałem o tym tutaj - zapraszam.

Eli Bendersky
źródło
Czy & my_array nie powinno być nieprawidłową operacją, ponieważ wartość my_array nie znajduje się na stosie, tylko my_array [0 ... length] są? Wtedy to wszystko miałoby sens ...
Alexandre
@Alexandre: Właściwie nie jestem pewien, dlaczego jest to dozwolone.
Eli Bendersky
Możesz wziąć adres dowolnej zmiennej (jeśli nie jest zaznaczona register) niezależnie od czasu jej przechowywania: statyczna, dynamiczna lub automatyczna.
CB Bailey
my_arraysam jest na stosie, ponieważ my_array jest całą tablicą.
kawiarnia
3
my_array, gdy nie jest przedmiotem operatorów &lub sizeof, jest oceniana jako wskaźnik do swojego pierwszego elementu (tj. &my_array[0]) - ale my_arraysama nie jest tym wskaźnikiem ( my_arraynadal jest tablicą). Ten wskaźnik to tylko efemeryczna wartość r (np. Podana int a;, jest podobna a + 1) - przynajmniej koncepcyjnie jest „obliczana w razie potrzeby”. Prawdziwą „wartością” my_arrayjest zawartość całej tablicy - po prostu ustalenie tej wartości w C jest jak próba złapania mgły w słoiku.
kawiarnia
28

W C, kiedy używasz nazwy tablicy w wyrażeniu (włączając w to przekazanie jej do funkcji), chyba że jest to operand operatora address-of ( &) lub sizeofoperator, rozpada się na wskaźnik do jej pierwszego elementu.

Oznacza to, że w większości kontekstów arrayjest odpowiednikiem &array[0]zarówno typu, jak i wartości.

W twoim przykładzie my_arrayma typ, char[100]który rozpada się na a, char*gdy przekazujesz go do printf.

&my_arrayma typ char (*)[100](wskaźnik do tablicy 100 char). Ponieważ jest to operand do &, jest to jeden z przypadków, które my_arraynie rozpadają się natychmiast na wskaźnik do pierwszego elementu.

Wskaźnik do tablicy ma taką samą wartość adresową jak wskaźnik do pierwszego elementu tablicy, ponieważ obiekt tablicy jest po prostu ciągłą sekwencją jej elementów, ale wskaźnik do tablicy ma inny typ niż wskaźnik do elementu ta tablica. Jest to ważne, gdy wykonujesz arytmetykę wskaźników na dwóch typach wskaźników.

pointer_to_arrayma typ char *- zainicjowany, aby wskazywać na pierwszy element tablicy, ponieważ to właśnie my_arrayrozpada się w wyrażeniu inicjatora - i &pointer_to_array ma typ char **(wskaźnik do wskaźnika do a char).

Spośród nich: my_array(po rozpadzie na char*), &my_arraya pointer_to_arraywszystkie wskazują bezpośrednio na tablicę lub pierwszy element tablicy, a więc mają tę samą wartość adresu.

CB Bailey
źródło
3

Powód, dla którego my_arrayi &my_arraywynik w tym samym adresie można łatwo zrozumieć, patrząc na układ pamięci tablicy.

Powiedzmy, że masz tablicę 10 znaków (zamiast 100 w kodzie).

char my_array[10];

Pamięć dla my_arraywygląda mniej więcej tak:

+---+---+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+
^
|
Address of my_array.

W C / C ++ tablica rozpada się na wskaźnik do pierwszego elementu w wyrażeniu, takim jak

printf("my_array = %p\n", my_array);

Jeśli zbadasz, gdzie znajduje się pierwszy element tablicy, zobaczysz, że jego adres jest taki sam jak adres tablicy:

my_array[0]
|
v
+---+---+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+
^
|
Address of my_array[0].
R Sahu
źródło
3

W języku programowania B, który był bezpośrednim poprzednikiem C, wskaźniki i liczby całkowite były swobodnie zamienne. System zachowywałby się tak, jakby cała pamięć była gigantyczną tablicą. Każda nazwa zmiennej miała powiązany z nią adres globalny lub względny dla stosu, dla każdej nazwy zmiennej kompilator musiał tylko śledzić, czy była to zmienna globalna, czy lokalna, oraz jej adres względem pierwszej globalnej lub lokalnej zmienna.

Biorąc pod uwagę globalne deklaracja jak i;[nie było potrzeby, aby określić typ, ponieważ wszystko było liczbą całkowitą / wskaźnik] będą przetwarzane przez kompilator jako: address_of_i = next_global++; memory[address_of_i] = 0;a oświadczeniu jak i++będą przetwarzane jako: memory[address_of_i] = memory[address_of_i]+1;.

Deklaracja taka jak arr[10]; byłaby przetwarzana jako address_of_arr = next_global; memory[next_global] = next_global; next_global += 10;. Zauważ, że gdy tylko ta deklaracja została przetworzona, kompilator mógł natychmiast zapomnieć o arrbyciu tablicą . Oświadczenie, takie jak arr[i]=6;, będzie przetwarzane jako memory[memory[address_of_a] + memory[address_of_i]] = 6;. Kompilator nie przejmowałby się tym, czy arrreprezentował tablicę i iliczbę całkowitą, czy odwrotnie. Rzeczywiście, nie obchodziło by, gdyby obie były tablicami czy obiema liczbami całkowitymi; całkowicie szczęśliwie wygenerowałoby kod zgodnie z opisem, bez względu na to, czy wynikowe zachowanie byłoby prawdopodobnie przydatne.

Jednym z celów języka programowania C było zapewnienie dużej zgodności z B. W B, nazwa tablicy [zwanej „wektorem” w terminologii B] identyfikowała zmienną zawierającą wskaźnik, który początkowo był przypisany do do pierwszego elementu alokacji o podanym rozmiarze, więc gdyby ta nazwa pojawiła się na liście argumentów funkcji, funkcja otrzymałaby wskaźnik do wektora. Mimo że C dodał "prawdziwe" typy tablic, których nazwa była sztywno powiązana z adresem alokacji, a nie zmienną wskaźnikową, która początkowo wskazywałaby na alokację, posiadanie tablic rozkładających się na wskaźniki tworzyło kod, który zadeklarował tablicę typu C zachowuje się identycznie do kodu B, który zadeklarował wektor, a następnie nigdy nie modyfikował zmiennej przechowującej jego adres.

supercat
źródło
1

Właściwie &myarrayimyarray oba są adresem bazowym.

Jeśli chcesz zobaczyć różnicę, zamiast używać

printf("my_array = %p\n", my_array);
printf("my_array = %p\n", &my_array);

posługiwać się

printf("my_array = %s\n", my_array);
printf("my_array = %p\n", my_array);
Ravi Bisla
źródło