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

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

int array[NROWS][NCOLUMNS];
int **array1;
int **array2;
int *array3;
int (*array4)[NCOLUMNS];

и это функции:

f1(int a[][NCOLUMNS], int m, int n);
f2(int *aryp, int nrows, int ncolumns);
f3(int **pp, int m, int n);

На веб-сайте, который я прочитал, упоминается, что мы можем передавать следующие массивы следующим функциям таким образом:

f2(&array[0][0], NROWS, NCOLUMNS);
f2(*array2, nrows, ncolumns);
f2(array3, nrows, ncolumns);
f2(*array4, nrows, NCOLUMNS);
f3(array1, nrows, ncolumns);
f3(array2, nrows, ncolumns);

не array1 а также array2 массив указателей? Поэтому, когда вы передаете их f3все указатели пройдены? И о array2 когда перешел к f2, f2 имеет нормальный указатель в формальных аргументах, но array2 это массив указателей, так как вы будете получать доступ к отдельным строкам и столбцам при передаче массива указателей в f2? И как бы вы получили доступ к отдельным строкам и столбцам при прохождении array4 который является указателем на ряд 1D массивов для работы f2?

2 ответа

То, что вы можете воспринимать как массивы в C, не всегда являются массивами с точки зрения языковых концепций.

С является языком с 70-х годов. Это очень близко к машинной реализации по сравнению с его более абстрактными потомками. Таким образом, вы должны принять во внимание реализацию, если вы хотите понять, что лежит за пределами схожих синтаксических элементов.

Доступ к указателям и массивам можно получить через (квадратную) скобку.

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

f[i] будет "работать" и для указателей и массивов, хотя основные механизмы будут отличаться, как мы увидим.

Связь между указателями и массивами

Давайте начнем с объявления переменных.

указатели

float * f просто говорит компилятору, что символ f будет когда-нибудь ссылаться на неизвестное число поплавков.

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

Указатели арифметики и скобочные обозначения

Имейте в виду, что когда вы добавляете / вычитаете значение из указателя, единица измерения является размером указанного типа.

float * f;
float * f3 = f+3; // internal value: f + 3 * sizeof (float)

// these two statements are identical
*f3 = 1;
*(f+3) = 1;

С момента написания *(f+i) неудобно, когда вы хотите ссылаться на смежные данные из указателя, можно использовать обозначение в скобках:

f[3] = 1; // equivalent to *(f+3) = 1;

Независимо от используемой записи, адрес f[3] вычисляется так:

@f[ 3 ] = f + 3 * sizeof (float)

Вы могли бы рассмотреть f функционально как (динамический) массив, но, как видит C, это все еще указатель, на который ссылается синтаксис, который делает его похожим на массив.

Массивы

float f[10] до сих пор говорит компилятору, что f будет ссылаться на некоторые поплавки, но это также

  • выделяет запрошенное количество поплавков в соответствующем месте
    • в стеке, если f является автоматической локальной переменной
    • в статических данных (он же BSS), если f является глобальной или статической переменной
  • считает символ f как постоянный указатель на первое из этих значений с плавающей точкой

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

Например, float f[] = {2,4,8} объявляет массив длины 3, эквивалентный float f[ 3 ] = {2,4,8}, Измерение может быть опущено для удобства: длина отражает количество инициализаторов, не заставляя программиста повторить его явно.

К сожалению, [] нотация может также ссылаться на указатели в некоторых других обстоятельствах (подробнее об этом позже).

Обозначения и массивы в скобках

Обозначение в скобках является наиболее естественным способом доступа к содержимому массива.

Когда вы ссылаетесь на массив, компилятор знает, что это массив. Затем он может получить доступ к данным на основе первого элемента массива, например:

@f[ 3 ] = f + 3 * sizeof (float)

В случае одномерных массивов (но только в этом случае!) Вы можете видеть, что вычисление адреса точно такое же, как и для указателя.

Массивы как указатели

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

иллюстрация

void test (void)
{
    float* f1;
    float  f2[10];
    float  f3[];         // <-- compiler error : dimension not known
    float  f4[] = {5,7}; // creates float f4[2] with f4[0]=5 and f4[1]=7

    f1[3] = 1234; // <--- write to a random memory location. You're in trouble
    f2[3] = 5678; // write into the space reserved by the compiler

    // obtain 10 floats from the heap and set f1 to point to them
    f1 = (float *) calloc (10, sizeof(float));
    f1[3] = 1234; // write into the space reserved by you

    // make f1 an alias of f2 (f1 will point to the same data as f2)
    f1 = f2;              // f2 is a constant pointer to the array data
    printf ("%g", f1[3]); // will print "5678", as set through f2

    // f2 cannot be changed
    f2 = f1; // <-- compiler error : incompatible types ‘float[10]’ / ‘float *’
}

Идет многомерный

Давайте расширим наш пример до двумерного случая:

float    f2[3][10]; // 2d array of floats
float ** f1;        // pointer to pointer to float

f1 = f2; // <-- the compiler should not allow that, but it does!

f2[2][5] = 1234;           // set some array value
printf ("%g\n", f2[2][5]); // no problem accessing it

printf ("%g\n",f1[2][5]);  // bang you're dead

посмотрим что здесь произошло

когда вы объявляете float f2[3][10] компилятор выделяет 30 требуемых операций с плавающей точкой как непрерывный блок. Первые 10 чисел представляют f[0], следующие десять - [1] и т. Д.

Когда ты пишешь f2[ 2 ][ 5 ], компилятор все еще знает f является массивом, поэтому он может вычислить эффективный адрес требуемого числа с плавающей запятой следующим образом:

@f2[ 2 ][ 5 ] = f + ( 2 * 10 + 5 ) * sizeof (float)

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

При ссылке на указатель компилятор просто последовательно применяет арифметику указателей:

float h = f1[2][5];

эквивалентно:

float * g = f1[2]; // equivalent to g = *(f1+2)
float   h = g[5];  // equivalent to h = *(g +5)

f1[ 2 ][ 5 ] обрабатывается компилятором как *(*(f1+ 2 )+ 5 ), Конечный адрес будет вычислен так:

@f1[ 2 ][ 5 ] = *(f + 2 * sizeof (float *)) + 5 * sizeof (float)

Вы просили об этом, вы получили это

За одной и той же нотацией в скобках лежат две очень разные реализации.

Понятно, при попытке доступа f2 данные через f1 результаты будут катастрофическими.

Компилятор получит 3-й float от f2[2] рассмотрите его как указатель, добавьте 20 к нему и попробуйте ссылаться на полученный адрес.

Если вы пишете какое-то значение через этот тип неправильно инициализированного указателя, считайте, что вам повезло, если вы получили нарушение доступа вместо того, чтобы молча повредить случайные четыре байта памяти.

К сожалению, даже если к базовой структуре данных нельзя получить доступ должным образом, если компилятор не знает, что f2 это массив, f2 до сих пор считается константой float** указатель

В идеальном мире это не должно, но в C (увы!) Это так.

Это означает, что вы можете назначить указатель на массив без компиляции об этом, даже если результат не имеет смысла.

Вызовы функций

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

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

Здесь снова, из-за того, что компилятор рассматривает массив как постоянный указатель, вам будет позволено делать глупые вещи, такие как объявление массива и передача его функции, такой как указатель.

Пустые квадратные скобки

Что еще хуже, синтаксис объявления параметров функции позволяет использовать скобки таким образом, что еще больше увеличивает вероятность путаницы между массивами и указателями.

void f (float f1[]);

обрабатывается точно так же, как

void f (float * f1);

хотя объявление переменной

float f1[];

выдаст ошибку вместо того, чтобы рассматривать ее как альтернативный способ объявления float * f,

Можно сказать, что [] В обозначениях разрешено указывать указатели, но только в параметрах функций.

Почему его нельзя допустить, поскольку объявление переменной может быть открытым для обсуждения (среди прочего, это будет неоднозначно float f[] = { ... } Инициализированный синтаксис объявления массива), но в результате получается, что для удаления параметров функции вводится нотация, которая добавляет еще один слой путаницы.

Например, знаменитый argv Параметр может быть объявлен любым нечетным способом:

int main (int argc, char ** argv)
int main (int argc, char * argv[])
int main (int argc, char argv[][])

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

void fun (float f[][10]); // pointer to arrays of 10 floats

эквивалентный синтаксис указателя заставляет вас использовать скобки:

void fun (float (* f)[10]);

чего нельзя избежать при объявлении такой переменной:

float (* f)[10]; // pointer to array of 10 floats
float f[][10];   // <-- compiler error : array dimension not known

Заключение

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

float      ** var1;           // pointer to pointer to float
float       * var2[10];       // array of pointers to float
float      (* var3)[10];      // pointer to array of floats (mind the brackets!)
float         var4[10][10];   // array of arrays of floats (2d array of floats)

// using empty brackets notation
void fun1 (float f[  ][  ]);
void fun2 (float f[10][  ]);
void fun3 (float f[  ][10]);
void fun4 (float f[10][10]);

// using same syntax as for variables declaration
void fun1 (float ** f);
void fun2 (float * f[10]);
void fun3 (float (* f)[10]); // <-- [] notation (arguably) easier to read
void fun4 (float f[10][10]); // must always use square brackets in that case

// even more choice for multiple level pointers
void fun1 (float * f[]);

// any funI (varJ) call with I != J will end up in tears

Последнее слово совета

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

// type definition
typedef float (* tWeirdElement)[10];
typedef tWeirdElement (* tWeirdo)[10]; // pointer to arrays of 10 pointers
                                       // to arrays of 10 floats 

// variable declaration
tWeirdo weirdo;

// parameter declaration
void do_some_weird_things (tWeirdo weirdo);

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

int array[NROWS][NCOLUMNS];  
int (*array4)[NCOLUMNS];  

являются массивами. Остальные являются указателями, а не массивами.

не array1 а также array2 массив указателей?

Нет никогда. array1 а также array2 являются int ** тип, т.е. имеет указатель типа на указатель на целое число.

Поэтому, когда вы передаете их f3 все указатели пройдены?

Я объяснил это выше, что array1 а также array2 не массив указателей.

И о array2 когда перешел к f2, f2 имеет нормальный указатель в формальных аргументах, но array2 это массив указателей, так как вы будете получать доступ к отдельным строкам и столбцам при передаче массива указателей в f2?

f2 ожидает указатель на int в качестве первого аргумента. *array2 это указатель на int, Следовательно в вызове

f2(*array2, nrows, ncolumns);  

указатель на int передается f2 в качестве первого аргумента, а не массив указателей.

И как бы вы получили доступ к отдельным строкам и столбцам при прохождении array4 который является указателем на ряд 1D массивов для работы f2?

Поскольку вы передаете указатель на int введите аргумент для f2 Вы можете получить доступ только к строке.

Другие вопросы по тегам