C: Как отличить массив указателей от указателя на массив? - PullRequest
4 голосов
/ 19 июня 2020

Скажем, у меня есть следующий код:

// x = whatever number

int *arr_of_ptr[x];
int (*ptr_to_arr)[x]

int **p1 = arr_of_ptr;
int **p2 = ptr_to_arr;

Насколько я понимаю, arr_of_ptr заключается в том, что «разыменование элемента arr_of_ptr приводит к int» - поэтому элементы arr_of_ptr равны указатели на целые числа . С другой стороны, разыменование ptr_to_arr приводит к массиву, из которого я могу получить целые числа, следовательно, ptr_to_arr указывает на массив .

Я также приблизительно понимаю, что массивы сами являются указателями, и этот arr[p] оценивается как (arr + p * sizeof(data_type_of_arr)), где имя arr распадается на указатель на первый элемент arr.

Так что все хорошо, но есть ли способ определить, являются ли p1 и p2 pointers to arrays или arrays of pointers без предварительной информации?

Мое замешательство в основном связано с тем, что (я думаю) мы можем оценить int **p два способами:

  1. *(p + n * size) - это то, что дает мне int
  2. (*p + n * size) - это то, что дает мне int

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

Ответы [ 3 ]

4 голосов
/ 19 июня 2020

Основное отличие состоит в том, что это допустимо:

int **p1 = arr_of_ptr;

Хотя это не так:

int **p2 = ptr_to_arr;

Поскольку arr_of_ptr является массивом, он может (в большинстве случаев) распад на указатель на его первый элемент. Итак, поскольку элементы arr_of_ptr имеют тип int *, указатель на элемент имеет тип int **, поэтому вы можете присвоить его p1.

ptr_to_arr, однако это не массив, а указатель, так что распада не происходит. Вы пытаетесь присвоить выражение типа int (*)[x] выражению типа int **. Эти типы несовместимы, и если вы попытаетесь использовать p2, вы не получите того, что ожидаете.

1 голос
/ 19 июня 2020

Во-первых,

Я также приблизительно понимаю, что сами массивы являются указателями и что 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 языка
0 голосов
/ 19 июня 2020
#include <stdio.h>

int main(void) {
    // your code goes here
    int arr[] = {1,2,3};
    int *p1 = &arr[0];
    int *p2 = &arr[1];
    int *p3 = &arr[2];
    int* arr2[3];
    arr2[0] = p1;
    arr2[1] = p2;
    arr2[2] = p3;
    int *p4 = &arr;
    printf("%d\n", sizeof(p4));
    printf("%d\n", sizeof(arr2));
    printf("%d\n", *p4);  // not **p4
    printf("%d\n", **arr2);
    return 0;
}

В приведенном выше коде arr - это обычный целочисленный массив с 3 элементами.
p1 , p2 и p3 - обычные указатели на эти элементы.
arr2 - это массив указателей хранение p1 , p2 и p3 .
p4 - это указатель на массив , указывающий на массив arr

Согласно вашему вопросу, вам нужно различать p4 и arr2
Поскольку p4 является указателем, его размер фиксирован (8 байтов), а размер arr2 зависит от того, сколько элементов он содержит (8x3 = 24).

Кроме того, для печати значения, содержащегося в p4, используйте одинарное разыменование (* p4), а не ** p4 (недопустимо), а для печати значения, содержащегося в arr2, используйте двойное разыменование (** arr2).

Вывод приведенного выше кода:

8
24
1
1
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...