Разница между указателем разыменования и доступом к элементам массива
Я помню пример, где была продемонстрирована разница между указателями и массивами.
Массив распадается на указатель на первый элемент в массиве при передаче в качестве параметра функции, но они не эквивалентны, как показано ниже:
//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
Ответы
Ответ 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 чрезвычайно полезно).