Подтвердить что ты не робот

Разница между указателем разыменования и доступом к элементам массива

Я помню пример, где была продемонстрирована разница между указателями и массивами.

Массив распадается на указатель на первый элемент в массиве при передаче в качестве параметра функции, но они не эквивалентны, как показано ниже:

//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;
}

Компилятор не проверяет тип для внешних переменных, поэтому во время компиляции не генерируются ошибки. Проблема в том, что b на самом деле является массивом, но блок компиляции file2 не знает об этом и рассматривает b как указатель, что приводит к сбою при попытке разыменовать его.

Я помню, когда это объяснялось, это имело смысл, но теперь я не могу вспомнить объяснение, и я не могу придумать это сам.

Итак, я думаю, вопрос в том, как массив обрабатывается иначе, чем указатель при доступе к элементам? (потому что я думал, что p[1] преобразуется в (эквивалент сборки) *(p + 1), независимо от того, является ли p массивом или указателем - я, очевидно, ошибаюсь).


Сборка, сгенерированная двумя разборами (VS 2013):
примечание: 1158000h и 1158008h - адреса памяти a и 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  
4b9b3361

Ответ 1

Благодаря ссылке, предоставленной @tesseract в комментариях: Программирование экспертов C: Deep C Secrets (стр. 96), я придумал простой ответ (простая глупая версия объяснения в книге, для полного академического ответа прочитайте книгу):

  • при объявлении int a[2]:
    • у компилятора есть a адрес, в котором эта переменная хранится. Этот адрес также является адресом массива, так как тип переменной является массивом.
    • Доступ к a[1] означает:
      • извлечение этого адреса,
      • добавление смещения и
      • доступ к памяти на этом вычисленном новом адресе.
  • при объявлении int *b:
    • у компилятора также есть адрес для b, но это адрес переменной указателя, а не массив.
    • Таким образом, доступ к b[1] означает:
      • извлечение этого адреса,
      • доступ к этому местоположению для получения значения b, то есть адрес массива
      • добавление смещения к этому адресу, а затем
      • доступ к конечной ячейке памяти.

Ответ 2

// 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

Компонент связывает их как с одинаковым адресом памяти, так как символ b имеет разные типы в file1.c и file2.c, то же место в памяти интерпретируется по-разному.

// in file2.c

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

b[1] оценивается сначала как *(b+1). Это означает получение значения в ячейке памяти, на которую привязано b, добавить к ней 1 (арифметику указателя), чтобы получить новый адрес, загрузить это значение в регистр CPU, сохранить это значение в месте x2 связанный с. Таким образом, значение в местоположении b связано с 100, добавляет 1 к нему, чтобы получить 104 (арифметика указателя; sizeof *b равно 4) и получить значение по адресу 104! Это неверно и поведение undefined и, скорее всего, вызовет сбой программы.

Существует разница в том, как к элементам массива обращаются и как к ним обращаются значения, на которые указывает указатель. Возьмем пример.

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

a - это массив целых чисел 2, а b - указатель на целое число, инициализированное адресом первого элемента a. Теперь, когда обращение к a[1], это означает, что все, что есть в смещении 1, находится по адресу a[0], адресу (и следующему блоку), к которому привязан символ a. Это одна инструкция сборки. Как будто некоторая информация встроена в символ массива, так что ЦП может извлекать элемент со смещением от базового адреса массива в одной инструкции. Когда вы получаете доступ к *b или b[0] или b[1], сначала получаете контент b, который является адресом, а затем выполняйте арифметику указателя, чтобы получить новый адрес, а затем получить все, что есть на этом адресе. Поэтому CPU должен сначала загрузить содержимое b, оценить b+1 (для b[1]), а затем загрузить содержимое по адресу b+1. Эти две инструкции по сборке.

Для внешнего массива вам не нужно указывать его размер. Единственным требованием является то, что он должен соответствовать его внешнему определению. Поэтому оба следующих утверждения эквивалентны:

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

Вы должны соответствовать типу переменной в своем объявлении с ее внешним определением. Компилятор не проверяет типы переменных при разрешении ссылок на символы. Только функции имеют типы функций, закодированные в имени функции. Поэтому вы не получите никаких предупреждений или ошибок, и он будет компилироваться просто отлично.

Технически, компоновщик или какой-либо компонент компилятора могут отслеживать, какой тип представляет символ, а затем выдавать ошибку или предупреждение. Но от стандарта не требуется никаких требований. Вы должны поступать правильно.

Ответ 3

Это не полностью отвечает на ваш вопрос, но дает вам подсказку о том, что происходит. Измените свой код немного, чтобы дать

//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;
}

Теперь, когда вы запустите его, вы все равно получите segfault. Но перед тем, как вы это сделаете, вы получите представление о том, почему:

allocated x1 OK
a is 0x10afa4018
c is 0xff000000ff
Segmentation fault: 11

Значение указателя c не является тем, что вы ожидаете: вместо указателя на начало массива b (который был бы разумным местоположением памяти, близким к a), он, похоже, содержит содержимое массива b... (0xff 255 в шестнадцатеричном формате, конечно).

Я не могу объяснить, почему это так - см. ссылку , которая была дана @tesseract в комментариях (действительно, вся глава 4 чрезвычайно полезно).