Во-первых,
Я также приблизительно понимаю, что сами массивы являются указателями и что arr [p] оценивается как (arr + p * sizeof (data_type_of_arr)), где имя arr распадается на указатель на первый элемент arr.
Это не совсем правильно. Массивы не являются указателями. В большинстве случаев выражения типа массива будут преобразованы («распад») в выражения типа указателя, и значением выражения будет адрес первого элемента массива. Это значение указателя вычисляется по мере необходимости и нигде не сохраняется.
Исключения из правила распада возникают, когда выражение массива является операндом операторов sizeof
, _Alignof
или унарных &
, или является строковым литералом, используемым для инициализации массива символов в объявлении.
С учетом всего сказанного, ptr_to_arr
имеет указатель тип, а не тип массива - он не будет "распадаться" до int **
.
Для декларации
T arr[N];
верно следующее:
Expression Type Decays to Equivalent expression
---------- ---- --------- ---------------------
arr T [N] T * &arr[0]
*arr T n/a arr[0]
arr[i] T n/a n/a
&arr T (*)[N] n/a n/a
Выражения arr
, &arr[0]
и &arr
все дают одно и то же значение (по модулю любых различий в представлении между типами). arr
и &arr[0]
имеют один и тот же тип «указатель на T
» (T *
), а &arr
имеет тип «указатель на массив N-элементов T
» (T (*)[N]
).
Если вы замените T
на тип указателя P *
, так что теперь объявление будет
P *arr[N];
, вы получите следующее:
Expression Type Decays to Equivalent expression
---------- ---- --------- ---------------------
arr P *[N] P ** &arr[0]
*arr P * n/a arr[0]
arr[i] P * n/a n/a
&arr P *(*)[N] n/a n/a
Итак, учитывая ваш объявлений, правильнее было бы написать что-то вроде этого:
int arr[x];
int *p1 = arr; // the expression arr "decays" to int *
int *arr_of_ptr[x];
int **p2 = arr_of_ptr; // the expression arr_of_ptr "decays" to int **
/**
* In the following declarations, the array expressions are operands
* of the unary & operator, so the decay rule doesn't apply.
*/
int (*ptr_to_arr)[x] = &arr;
int *(*ptr_to_arr_of_ptr)[x] = &arr_of_ptr;
Опять же, ptr_to_arr
и ptr_to_arr_of_ptr
являются указателями , а не массивами, и не распадаются на другие тип указателя.
EDIT
Из комментариев:
Могу я просто вручную объяснить это как : массив указателей имеет имя, которое может распадаться на указатель,
Да, -i sh, просто имейте в виду, что это не совсем точное имя (что показано в примере ниже). Если вы студент первого курса, ваше учебное заведение не делает вам никаких одолжений, заставляя вас заниматься C так рано. Хотя это основа, на которой построена большая часть современной компьютерной экосистемы, это ужасный язык обучения. Ужасно . Да, это небольшой язык, но его аспекты глубоко не интуитивно понятны и сбивают с толку, и взаимодействие между массивами и указателями является одним из этих аспектов.
массив указателей имеет имя, которое может распадаться на указатель, но указатель на массив, даже при разыменовании, не дает мне чего-то, что распадается на указатель?
На самом деле ...
Если ptr_to_arr
имеет тип int (*)[x]
, тогда выражение *ptr_to_arr
будет иметь тип int [x]
, а будет распадаться на int *
. Выражение *ptr_to_arr_of_ptr
будет иметь тип int *[x]
, который распадется на int **
. Вот почему я продолжаю использовать термин « выражение типа массива», когда говорю о правиле распада, а не просто имя массива.
Что-то, что я упустил из своих объяснений до сих пор - почему выражения массива распадаются на указатели? В чем причина такого невероятно запутанного поведения?
C возникло не полностью сформированным из мозга Денниса Ритча ie - оно было получено из более раннего языка с именем B (который был получен из BCPL , который был получен из CPL и др. c.) 1 . B был «безтиповым» языком, где данные представляли собой просто последовательность слов или «ячеек». Память моделировалась как линейный массив «ячеек». Когда вы объявили массив из N элементов в B, например
auto arr[N];
, компилятор выделит все ячейки, необходимые для элементов массива, плюс дополнительную ячейку, в которой будут храниться числовое смещение (в основном, указатель) на первый элемент массива, и эта ячейка будет привязана к переменной arr
:
+---+
arr: | +-+-----------+
+---+ |
... |
+---+ |
| | arr[0] <--+
+---+
| | arr[1]
+---+
...
+---+
| | arr[N-1]
+---+
Для индексации в массив вы должны сместить i
ячейки из местоположения, хранящегося в arr
, и разыменовать результат. IOW, a[i]
был в точности эквивалентен *(a + i)
.
Когда Ритч ie разрабатывал язык C, он хотел сохранить семантику массива B (a[i]
по-прежнему точно эквивалентен *(a + i)
), но по разным причинам он не хотел сохранять указатель на первый элемент. Итак, он полностью избавился от этого. Теперь, когда вы объявляете массив в C, например
int arr[N];
, единственное хранилище, выделенное для самих элементов массива:
+---+
| | arr[0]
+---+
| | arr[1]
+---+
...
+---+
| | arr[N-1]
+---+
Нет отдельного объекта arr
, в котором хранится указатель на первый элемент (что является частью того, почему выражения массива не могут быть целью присваивания - нечего присваивать ). Вместо этого это значение указателя вычисляется по мере необходимости, когда вам нужно добавить индекс в массив.
Тот же принцип действует и для многомерных массивов. Предположим следующее:
int a[2][2] = { { 1, 2 }, { 3, 4 } };
В памяти вы получаете следующее:
Viewed as int Viewed as int [2]
+---+ +---+
a: | 1 | a[0][0] a:| 1 | a[0]
+---+ + - +
| 2 | a[0][1] | 2 |
+---+ +---+
| 3 | a[1][0] | 3 | a[1]
+---+ + - +
| 4 | a[1][1] | 4 |
+---+ +---+
Слева мы рассматриваем его как последовательность int
, а справа мы рассматривайте его как последовательность int [2]
.
Каждый a[i]
имеет тип int [2]
, который распадается на int *
. Само выражение a
распадается с типа int [2][2]
на int (*)[2]
( не int **
).
Выражение a[i][j]
в точности эквивалентно *(a[i] + j)
, которое эквивалент *( *(a + i) + j )
.
Как подробно описано в Разработка C языка