Różnica między wskaźnikiem dereferencyjnym A dostępem do elementów tablicy

Pamiętam przykład, w którym pokazano różnicę między wskaźnikami a tablicami.

Tablica rozpada się na wskaźnik do pierwszego elementu tablicy, gdy jest przekazywana jako parametr funkcji, ale nie są one równoważne, jak pokazano poniżej:

//file file1.c

int a[2] = {800, 801};
int b[2] = {100, 101};
//file file2.c

extern int a[2];

// here b is declared as pointer,
// although the external unit defines it as an array
extern int *b; 

int main() {

  int x1, x2;

  x1 = a[1]; // ok
  x2 = b[1]; // crash at runtime

  return 0;
}

Linker nie sprawdza zewnętrznych zmiennych, więc nie generuje błędów podczas kompilacji. Problem polega na tym, że b jest w rzeczywistości tablicą, ale jednostka kompilacji file2 nie jest tego świadoma i traktuje b jako wskaźnik, powodujący awarię podczas próby dereferencji.

Pamiętam, kiedy to zostało wyjaśnione, to miało sens, ale teraz nie pamiętam wyjaśnienia ani nie mogę do niego przyjść na własną rękę.

Więc pytanie brzmi, jak tablica jest traktowana inaczej niż wskaźnik podczas dostępu do elementów? (ponieważ myślałem, że {[6] } jest konwertowane na (odpowiednik asemblera) *(p + 1) niezależnie od tego, czy p jest tablicą czy wskaźnikiem-jestem oczywiście źle).


Zgromadzenie wygenerowane przez dwa dereferencje (VS 2013):
Uwaga: 1158000h i 1158008h są adresami pamięci odpowiednio a i b

    12:   x1 = a[1];
0115139E  mov         eax,4  
011513A3  shl         eax,0  
011513A6  mov         ecx,dword ptr [eax+1158000h]  
011513AC  mov         dword ptr [x1],ecx  
    13:   x2 = b[1];
011513AF  mov         eax,4  
011513B4  shl         eax,0  
011513B7  mov         ecx,dword ptr ds:[1158008h]  
011513BD  mov         edx,dword ptr [ecx+eax]  
011513C0  mov         dword ptr [x2],edx  
Author: bolov, 2014-02-23

3 answers

Dzięki linkowi podanemu przez @tesseract w komentarzach: Expert C Programming: Deep C Secrets (strona 96), wymyśliłem prostą odpowiedź (prosta, głupia wersja wyjaśnienia w książce; aby uzyskać pełną akademicką odpowiedź przeczytaj książkę):

  • gdy zadeklarowane int a[2]:
    • kompilator ma dla a adres, w którym ta zmienna jest przechowywana. Adres ten jest również adresem tablicy, ponieważ typem zmiennej jest array.
    • Dostęp a[1] środki:
      • odzyskiwanie tego adresu,
      • dodanie offsetu i
      • dostęp do pamięci pod tym obliczonym nowym adresem.
  • gdy zadeklarowane int *b:
    • kompilator ma również adres dla b ale jest to adres zmiennej wskaźnika, a nie tablicy.
    • więc dostęp b[1] oznacza:
      • odzyskiwanie tego adresu,
      • dostęp do tej lokalizacji, aby uzyskać wartość b, tj. adres array
      • dodanie offsetu do tego adresu, a następnie
      • dostęp do ostatecznej lokalizacji pamięci.
 15
Author: bolov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-09 22:08:10
// in file2.c

extern int *b; // b is declared as a pointer to an integer

// in file1.c

int b[2] = {100, 101}; // b is defined and initialized as an array of 2 ints

Łącznik łączy je z tym samym adresem pamięci, jednak ponieważ symbol b ma różne typy w file1.c i file2.c, ta sama lokalizacja pamięci jest różnie interpretowana.

// in file2.c

int x2;  // assuming sizeof(int) == 4
x2 = b[1]; // b[1] == *(b+1) == *(100 + 1) == *(104) --> segfault

b[1] jest oceniana jako *(b+1). Oznacza to, że wartość w Miejscu Pamięci b jest związana, dodać 1 do niego (arytmetyka wskaźnika), aby uzyskać nowy adres, załadować tę wartość do rejestru CPU, zapisać tę wartość w miejscu x2 jest związana. Tak więc wartość w miejscu b jest przypisane do is 100, dodaj 1 do niego, aby uzyskać 104 (arytmetyka wskaźnika; sizeof *b jest 4) i uzyskaj wartość pod adresem 104! Jest to niewłaściwe i nieokreślone zachowanie i najprawdopodobniej spowoduje awarię programu.

Istnieje różnica w sposobie dostępu do elementów tablicy oraz w sposobie dostępu do wartości wskazywanych przez wskaźnik. Weźmy przykład.

int a[] = {100, 800};
int *b = a;

a jest tablicą 2 liczb całkowitych i {[4] } jest wskaźnikiem do liczby całkowitej zainicjalizowanej na adres pierwszego elementu a. Teraz, gdy a[1] jest dostępne, oznacza to, że get whatever is there at offset 1 from the address of a[0], the address (and the next block) to which the symbol a is bound. To jedna instrukcja montażu. To tak, jakby pewne informacje były osadzone w symbolu tablicy tak, że procesor może pobrać element z przesunięciem od adresu bazowego tablicy w jednej instrukcji. Kiedy uzyskasz dostęp do *b lub b[0] lub b[1], najpierw otrzymasz zawartość b, która jest adres, następnie wykonaj arytmetykę wskaźnika, aby uzyskać nowy adres, a następnie pobierz to, co jest pod tym adresem. Procesor musi więc najpierw załadować zawartość b, ocenić b+1 (dla b[1]), a następnie załadować zawartość pod adresem b+1. To dwie instrukcje montażu.

Dla tablicy extern nie trzeba określać jej rozmiaru.Jedynym wymogiem jest to, że musi odpowiadać swojej zewnętrznej definicji. Dlatego oba poniższe stwierdzenia są równoważne:

extern int a[2];  // equivalent to the below statement
extern int a[];

Musisz dopasuj typ zmiennej w jej deklaracji do jej zewnętrznej definicji. Linker nie sprawdza typów zmiennych podczas rozwiązywania odniesień do symboli. Tylko funkcje mają typy funkcji zakodowane w nazwie funkcji. W związku z tym nie otrzymasz żadnego ostrzeżenia lub błędu i skompiluje się dobrze.

Technicznie, linker lub komponent kompilatora może śledzić, jaki typ reprezentuje symbol, a następnie dać błąd lub ostrzeżenie. Ale nie ma wymogu od standard, aby to zrobić. Musisz postąpić właściwie.

 11
Author: ajay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-23 19:33:20

To nie w pełni odpowiada na twoje pytanie, ale daje ci wskazówkę, co się dzieje. Zmodyfikuj trochę swój kod, aby dać

//file1.c

int a[2] = {800, 801};
int b[2] = {255, 255};

#include <stdio.h>

extern int a[2];

// here b is declared as pointer,
// although the external unit declares it as an array

extern int *b; 
int *c;

int main() {

  int x1, x2;

  x1 = a[1]; // ok
  c = b;
  printf("allocated x1 OK\n");
  printf("a is %p\n", a);
  printf("c is %p\n", c);
  x2 = *(c+1);
  printf("%d %d\n", x1, x2);
  return 0;
}
Kiedy go uruchomisz, nadal dostaniesz segfault. Ale zanim to zrobisz, masz wgląd w to dlaczego:
allocated x1 OK
a is 0x10afa4018
c is 0xff000000ff
Segmentation fault: 11

Wartość wskaźnika c nie jest tym, czego oczekujesz: zamiast być wskaźnikiem do początku tablicy b (która byłaby sensownym miejscem pamięci blisko a), wydaje się, że zawiera zawartość tablicy b ... (0xff jest 255 W hex, oczywiście).

Nie potrafię dokładnie wyjaśnić, dlaczego tak jest-Zobacz link podany przez @tesseract w komentarzach (naprawdę cały rozdział 4 jest niezwykle przydatny).

 5
Author: Floris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-23 18:45:12