Путаница в отношении указателей и многомерных массивов

Если возможно следующее:

MyFunction(int *array, int size)
{
    for(int i=0 ; i<size ; i++)
    {
        printf("%d", array[i]);
    }
}

main()
{
    int array[6] = {0, 1, 2, 3, 4, 5};
    MyFunction(array, 6);
}

Почему следующее не?

MyFunction(int **array, int row, int col)
{
    for(int i=0 ; i<row ; i++)
    {
        for(int j=0 ; j<col ; j++)
        {
            printf("%d", array[i][j]);
        }
    }
}

main()
{
    int array[3][3] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
    MyFunction(array, 3, 3);
}

Ответы

Ответ 1

Во-первых, некоторый стандартный язык:

6.3.2.1 Lvalues, массивы и обозначения функций
...
3 За исключением случаев, когда это операнд оператора sizeof или унарный оператор & или строковый литерал, используемый для инициализации массива, выражение, которое имеет тип "массив типа", преобразуется в выражение с типом "указатель на тип", который указывает на начальный элемент объекта массива и не является значением lvalue. Если объект массива имеет класс хранения регистров, поведение undefined.

Учитывая объявление

int myarray[3][3];

тип myarray - это "3-элементный массив из 3-элементного массива int". Следуя приведенному выше правилу, когда вы пишете

MyFunction(myarray, 3, 3);

выражение myarray имеет свой тип, неявно преобразованный ( "распад" ) из "3-элементного массива из 3-элементного массива int" в "указатель на 3-элементный массив int" или int (*)[3].

Таким образом, ваш прототип функции должен быть

int MyFunction(int (*array)[3], int row, int col)

Обратите внимание, что int **array не совпадает с int (*array)[3]; арифметика указателя будет отличаться, поэтому ваши индексы не будут указывать на нужные места. Помните, что индексирование массива определяется в терминах арифметики указателя: a[i] == *(a+i), a[i][j] == *(*(a + i) + j). a+i даст другое значение в зависимости от того, является ли a int ** или int (*)[N].

В этом конкретном примере предполагается, что вы всегда передаете массив элементов Nx3 из int; не очень гибкий, если вы хотите иметь дело с любым размером NxM. Один из способов обойти это - это явно передать адрес первого элемента в массиве, поэтому вы просто передаете простой указатель, а затем вручную вычислите правильное смещение:

void MyFunction(int *arr, int row, int col)
{
  int i, j;
  for (i = 0; i < row; i++)
     for (j = 0; j < col; j++)
       printf("%d", a[i*col+j]);
}

int main(void)
{
  int myarray[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
  ...
  MyFunction(&myarray[0][0], 3, 3);

Поскольку мы передаем простой указатель на int, мы не можем использовать двойной индекс в MyFunc; результат arr[i] является целым числом, а не указателем, поэтому мы должны вычислить полное смещение в массив в одной операции индекса. Обратите внимание, что этот трюк будет работать только для действительно многомерных массивов.

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

void AnotherFunc(int **arr, int row, int col)
{
  int i, j;
  for (i = 0; i < row; i++)
    for (j = 0; j < col; j++)
      printf("%d", arr[i][j]);
}

int main(void)
{
  int d0[3] = {1, 2, 3};
  int d1[3] = {4, 5, 6};
  int d2[3] = {7, 8, 9};

  int *a[3] = {d0, d1, d2};

  AnotherFunc(a, 3, 3);
  ...
}

Следуя приведенному выше правилу, когда в инициализаторе для a появляются выражения d0, d1 и d2, их типы все преобразуются из "3-элементного массива int" в "указатель на int". Аналогично, когда выражение a появляется при вызове AnotherFunc, его тип преобразуется из "3-элементного массива указателя в int" в "указатель на указатель на int".

Обратите внимание, что в AnotherFunc мы индексируем обе измерения вместо вычисления смещения, как это было в MyFunc. Это потому, что a - это массив значений указателя. Выражение arr[i] получает значение i-го значения указателя из местоположения arr; мы затем находим j'th целочисленное значение, смещенное от этого значения указателя.

Следующая таблица может помочь - она ​​отображает типы различных выражений массива и их распад на основе их объявлений (T (*)[N] - тип указателя, а не тип массива, поэтому он не распадается):

Declaration            Expression            Type            Implicitly Converted (Decays) to
-----------            ----------            ----            --------------------------------
     T a[N]                     a            T [N]           T *
                               &a            T (*)[N]
                               *a            T
                             a[i]            T

  T a[M][N]                     a            T [M][N]        T (*)[N]
                               &a            T (*)[M][N] 
                               *a            T [N]           T *
                             a[i]            T [N]           T *
                            &a[i]            T (*)[N] 
                            *a[i]            T
                          a[i][j]            T

T a[L][M][N]                    a            T [L][M][N]     T (*)[M][N]
                               &a            T (*)[L][M][N]
                               *a            T [M][N]        T (*)[N]
                             a[i]            T [M][N]        T (*)[N]
                            &a[i]            T (*)[M][N]
                            *a[i]            T [N]           T *
                          a[i][j]            T [N]           T *
                         &a[i][j]            T (*)[N]
                         *a[i][j]            T 
                       a[i][j][k]            T

Образец для многомерных массивов должен быть ясным.

Ответ 2

Изменить: Здесь моя попытка ответить на более точный ответ в соответствии с запросом и на основе вашего нового примера кода:

Независимо от размеров массива, то, что вы передаете, является "указателем на массив" - это только один указатель, хотя тип указателя может меняться.

В первом примере int array[6] представляет собой массив из 6 элементов int. Передача array передает указатель на первый элемент, который является int, поэтому тип параметра int *, который может быть эквивалентно записан как int [].

В вашем втором примере int array[3][3] представляет собой массив из трех строк (элементов), каждый из которых содержит 3 int s. Передача array передает указатель на первый элемент, который представляет собой массив из 3 int s. Следовательно, тип int (*)[3] - указатель на массив из 3 элементов, который может быть эквивалентно записан как int [][3].

Надеюсь, теперь вы видите разницу. Когда вы передаете int **, это на самом деле указатель на массив int * и NOT указатель на 2D-массив.

Пример для фактического int ** будет примерно таким:

int a[3] = { 1, 2, 3 };
int b[3] = { 4, 5, 6 };
int c[3] = { 7, 8, 9 };
int *array[3] = { a, b, c };

Здесь array - массив из 3 int * s, и передача этого в качестве аргумента приведет к int **.


Оригинальный ответ:

Ваш первый пример - не 2D-массив, хотя он используется аналогичным образом. Там вы создаете ROWS число указателей char *, каждый из которых указывает на другой массив символов COLS. Здесь есть два уровня косвенности.

Второй и третий примеры представляют собой 2D-массивы, где память для всех символов ROWS * COLS является смежной. Здесь существует только один уровень косвенности. Указатель на 2D-массив не char **, а char (*)[COLS], поэтому вы можете сделать:

char (*p)[SIZE] = arr;
// use p like arr, eg. p[1][2]

Ответ 3

Другие довольно много подвели итоги. int ** A означает, что A является указателем на массив, а не ссылкой на двухмерный массив. Однако это не означает, что он неприменим. Поскольку данные в C хранятся в строчном порядке, как только вы знаете длину строки, извлечение данных должно быть легким.

Ответ 5

Первый пример возможен, потому что массивы вырождаются в указатели при передаче в качестве параметров функции.

Второй пример не работает, потому что int[3][3] вырождается до int (*)[3], а не двойной указатель int **. Это так, потому что 2D-массивы смежны в памяти, и без этой информации компилятор не знал, как получить доступ к элементам, находящимся за первой строкой. Рассмотрим простую сетку чисел:

1  2  6 
0  7  9

Если бы мы сохраняли эти числа в массиве int nums[6], как бы мы индексировали в массив для доступа к элементу 7? По 1 * 3 + 1, конечно, или, вообще говоря, row * num-columns + column. Чтобы получить доступ к любому элементу за первой строкой, вам нужно знать, сколько столбцов имеет сетка.

Когда вы сохраняете числа как nums[2][3], компилятор использует точно такую ​​же арифметику row * num-columns + column, как и вручную, с массивом 1D, он просто скрыт от программиста. Поэтому вам необходимо передать количество столбцов при передаче 2D-массива, чтобы компилятор мог выполнить эту арифметику.

Во многих других языках массивы несут информацию об их размере, исключая необходимость вручную указывать размеры при передаче многомерных массивов в функции.

Ответ 6

Возможно, мы сможем ожидать более "точного" вопроса, если вы хотите получить более точный ответ. У вашей идеи две проблемы:

  • 2D-массив int A[3][3] при использовании в выражении распадается на адрес его первого элемента таким образом, чтобы указатель типа int (*)[3]. Чтобы передать массив, вы должны использовать &A[0][0], чтобы получить указатель на первый "внутренний" элемент.
  • внутри вашей функции операция A[i][j] не может быть выполнено с тех пор ваш компилятор не имеет информации о длина строки, там.

Ответ 7

В этом коде есть две основные проблемы.

MyFunction(int **array, int row, int col);

Во-первых, используется int **array неправильный тип. Это указатель на указатель, а

int array[3][3] = {0, 1, 2, 3, 4, 5, 6, 7, 8};

- многомерный массив. Память, которая составляет этот многомерный массив, - это один кусок, а смещение от начала этого к любому элементу этого массива вычисляется на основе знания размера строки в этом массиве.

int *A[99];

Это массив указателей на целые числа. Целые числа, на которые указывают, могут быть первыми из нескольких целых чисел в памяти, что означает, что они фактически указывают на массивы целых чисел.

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

int array[3][3] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
printf("%p %p %p\n", array, array[0], &(array[0][0]) );

Вы должны получить тот же адрес, который был напечатан 3 раза, потому что все они относятся к одному и тому же адресу, но их типы не совпадают. Тип данных последних двух аналогичен и совместим для многих целей, поскольку array[0] будет рассматриваться как указатель на первый элемент первой строки array, и эта строка является массивом сама по себе.

Если вы скажете:

int **A;

Вы говорите, что есть указатель на указатель на int. Хотя A[2][4] является допустимым выражением, это не многомерный массив таким же образом, как:

int B[3][3];

Если вы скажете A[1], это оценит int *, аналогичный B[1], за исключением того, что вы можете сказать A[1] = (int *)0x4444;, но если вы сказали B[1] = (int *)0x4444;, вы получите ошибку компилятора, потому что B[1] фактически вычисленное значение, а не переменная. В B нет массива переменных int * - только некоторые вычисления, основанные на размере строки и адресе самого первого элемента массива.

Этот код должен делать что-то похожее на то, что вам нужно (некоторые изменения форматирования вывода для удобства чтения). Обратите внимание, как изменяется значение индекса в инструкции печати. ​​

MyFunction(int *array, int row, int col)
{
    int x = 0;
    for(int i=0 ; i<row ; i++ )
    {
        for(int j=0 ; j<col ; j++)
        {
            printf("%d ", array[x++]);
        }
        printf("\n");
    }
}

main()
{
    int array[3][3] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
    MyFunction(array, 3, 3);
}