Матрицы как параметры функции в C89

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

Недавно нас проинформировали, что, возможно, нас попросят реализовать решения с использованием C89 во время нашего следующего экзамена.

Мой вопрос касается использования многомерных массивов переменной длины в отношении объявления и использования внутри функции.

В C99 у меня может быть такая функция:

void func(int cols, int mat[][cols], int rows);

Принимая во внимание, что в C89 VLA и подобные приспособления незаконны. Мне сказали, что вместо этого вам нужно будет использовать указатель на указатель. Итак, что-то вроде:

void func(int **mat, int cols, int rows);

Однако у меня возникают проблемы с пониманием:

  1. Как вы будете получать доступ к элементам этой матрицы внутри функции. Можно ли по-прежнему использовать обозначение mat[i][j]?
  2. Как бы вы объявили и заполнили такую ​​матрицу. Вы начинаете с чего-то вроде int **mat;? Я думаю, вам придется использовать malloc(), но мне трудно понять точное выражение объявления.
  3. Можно ли вообще использовать матрицы как указатели на такие указатели? Я читал, что они работают одинаково, но не совсем так же. Как вы решаете изложенную проблему в C89?

Побочный вопрос. Что касается матриц переменного размера, мне сказали, что при такой настройке:

int rows, cols;

// get rows and cols from stdinput

int mat[rows][cols];

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

Спасибо!


person Samuele B.    schedule 19.01.2020    source источник
comment
1: Да. 2: зависит от того, что вам нужно сделать, вам не обязательно нужен malloc(). 3: Да, при передаче в качестве аргумента матрица распадается на указатель на указатель, точно так же, как массив распадается на указатель.   -  person Marco Bonelli    schedule 19.01.2020
comment
Спасибо. Для ответа (2): допустим, мне нужно получить размеры матрицы из stdinput, затем мне нужно создать матрицу с такими размерами, а затем мне нужно заполнить ее данными, указанными пользователем. Я бы использовал for(size_t i = 0; i < rows; i++) { for(size_t j = 0; j < cols; j++) { scanf("%d", &mat[i][j]); /*using scanf as an example, could be anything*/ } } Полагаю, что в этом случае мне пришлось бы выделять новый элемент для каждого места в матрице?   -  person Samuele B.    schedule 19.01.2020
comment
и особенно ответ Лундина.   -  person Antti Haapala    schedule 19.01.2020


Ответы (4)


Однако у меня возникают проблемы с пониманием:

  1. Как вы будете получать доступ к элементам этой матрицы внутри функции. Можете ли вы по-прежнему использовать обозначение mat[i][j]?
  2. Как бы вы объявили и заполнили такую ​​матрицу. Вы начинаете с чего-то вроде int **mat;? Я думаю, вам придется использовать malloc(), но мне трудно понять точное выражение объявления.
  3. Можно ли вообще использовать матрицы как указатели на такие указатели? Я читал, что они работают одинаково, но не совсем так же. Как вы решаете изложенную проблему в C89?

<сильный>1. mat[i][j]

В C89 вы правы, у вас нет поддержки VLA, если только это не предусмотрено нестандартным расширением компилятора (gcc делает это). Однако вы можете сделать то же самое в двух разных массивах.

Если вы знаете количество столбцов, которое будет у вас во время компиляции, и можете определить константу для этого значения, вы можете объявить указатель на массив [COLS]. Например, если вы знаете, что у вас будет 32 столбца и неизвестное количество строк, вы можете сделать:

#define COLS 32
...
    int (*array)[COLS] = malloc (rows * sizeof *array);

Это выделит блок памяти в одном вызове, обеспечивая хранилище для rows количества int[32] массивов, что позволит вам получить доступ как array[i][j], как и раньше. Прелесть использования указатель-на-массив заключается в том, что у вас есть однократное выделение и однократное освобождение. Вы можете realloc количество строк по мере необходимости.

(примечание: как указывает @PaulOgilvie, есть разница в том, как вы можете передать указатель на массив в функцию. Вы не можете передать как int array[][cols] как с VLA, вы должны передать как int (*array)[cols] - что вы также можете использовать с VLA, но обратное неверно)

Другой вариант — объявить указатель на указатель на type (например, int **array;). Обратите внимание, что здесь НЕ используется массив, это просто один указатель на указатель для ввода. Здесь распределение представляет собой двухэтапный процесс. Сначала вы выделяете память для некоторого количества указателей (количество строк указателей). Например:

int **array = malloc (rows * sizeof *array);

Выше вы выделяете блок памяти, способный хранить rows количество указателей, на которые затем вы можете отдельно выделить и назначить блоки памяти для хранения любого количества целочисленных значений (нет необходимости, чтобы каждая строка указывала на блок с тем же количеством целочисленных значений — что делает возможным «зубчатый массив» из-за отсутствия лучших слов). Чтобы затем выделить хранилище для целочисленных значений (или любого другого типа, который вы используете), делать:

for (int i = 0; i < rows; i++)
    array[i] = malloc (cols * sizeof *array[i]);

(примечание: вы должны проверять каждое выделение, которое было опущено для краткости. Также обратите внимание в обоих случаях над разыменованным указателем использовался для установки размера шрифта для выделения, например malloc (rows * sizeof *array), который мог бы быть malloc (rows * sizeof(int*))). Если вы всегда используете указатель разыменования для установки размера шрифта -- вы никогда не ошибетесь с размером шрифта)

На данный момент у вас есть указатель на блок памяти, хранящий rows количество указателей, а затем вы назначили блок памяти, способный хранить cols количество целочисленных значений, к которым вы можете получить доступ как array[i][j]. Кроме того, здесь вы можете realloc блок памяти предоставить rows указателей для добавления строк в любое время, когда вам нужно, но вы также должны выделить память для целочисленных значений и назначить эти выделенные блоки вашим новым указателям строк, прежде чем пытаться хранить там значения.

Когда вы закончите с смоделированным 2D-массивом на основе указатель-указатель, у вас также будет 2 шага. Вы должны освободить выделенные блоки, хранящие целые числа, прежде чем вы сможете освободить блок, содержащий указатели ваших строк, например.

for (int i = 0; i < rows; i++)
    free (array[i]);                /* free storage for integers */
free (array);                       /* free pointers */

<сильный>2. Заполнение любого объекта

В любом случае, поскольку вы можете получить доступ к смоделированному 2D-массиву с помощью нотации array[i][j], теперь вы можете заполнять и получать доступ к значениям в array так же, как вы делали это с 2D VLA в C99+.

<сильный>3. Можно ли использовать матрицы с указателями на указатели

Да, смоделированный 2D-массив обеспечивает ту же функциональность, что и описанная выше.

person David C. Rankin    schedule 19.01.2020
comment
VLA — это не то же самое, что массив указателей на строки. В C89 такой массив, переданный функции, не может быть адресован как mat[i][j] (так можно адресовать массив указателей на строки). Тем не менее очень полный ответ. - person Paul Ogilvie; 19.01.2020
comment
Спасибо за обстоятельный ответ! Я разместил вопрос, в котором я показываю программу, которую я сделал, которая использует эти принципы: заголовок stackoverflow.com/questions/59829137/ поэтому я задал этот вопрос. Если вы хотите взглянуть на это, дайте мне совет, это очень приветствуется - person Samuele B.; 20.01.2020

  1. Как вы будете получать доступ к элементам этой матрицы внутри функции. Можно ли по-прежнему использовать обозначение mat[i][j]?

да.

  1. Как бы вы объявили и заполнили такую ​​матрицу. Вы начинаете с чего-то вроде int **mat;? Я думаю, вам придется использовать malloc(), но мне трудно понять точное выражение объявления.

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

// assume nrows and ncols are dynamic
size_t nrows = /* ... */;
size_t ncols = /* ... */;
size_t i;
int **matrix;

matrix = malloc(nrows * sizeof(int*));
if (matrix == NULL) {
    perror("malloc() failed");
    exit(1);
}

for (i = 0; i < nrows; i++) {
    matrix[i] = malloc(ncols * sizeof(int));
    if (matrix[i] == NULL) {
        perror("malloc() failed");
        exit(1);
    }
}

/* fill the matrix */

/* use the matrix however you want */
func(matrix, nrows, ncols);

/* free the allocated memory once you don't need it anymore */
for (i = 0; i < nrows; i++)
    free(matrix[i]);
free(matrix);
  1. Можно ли вообще использовать матрицы как указатели на такие указатели? Я читал, что они работают одинаково, но не совсем так же. Как вы решаете изложенную проблему в C89?

Да, они могут. Массив распадается на указатель при передаче таким функциям. То же самое касается матриц, которые распадаются на указатель на указатель. См. раздел Что такое распад массива.

Дополнительный вопрос [...] - не лучший способ создать матрицу с заданными размерами из-за распределения в стеке программы. какой способ лучше?

Да, верно, это не лучший способ. Как правило, программы имеют ограниченный размер стека, поэтому размещение в стеке больших массивов не является хорошей идеей. В некоторых случаях вы можете превысить доступную память, выделенную для использования стека, и тогда ваша программа выйдет из строя. Лучше всего в этом случае использовать динамическое размещение через malloc().

person Marco Bonelli    schedule 19.01.2020

Как бы вы получили доступ к элементам этой матрицы внутри функции. Можете ли вы по-прежнему использовать обозначение mat[i][j]?

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

Является ли имя массива указателем?

Как бы вы объявили и заполнили такую ​​матрицу. Вы начинаете с чего-то вроде int **mat;? Я думаю, вам придется использовать malloc(), но мне трудно понять точное выражение объявления.

Это ваш выбор. Если вы находитесь в сценарии, где требуется статическое распределение, вы можете использовать обычное объявление массива. Если вы заранее не знаете размеры матрицы, вам следует использовать динамическое размещение. В последнем случае вы должны объявить свою матрицу в синтаксисе указателя, чтобы она могла хранить адрес, возвращаемый malloc.

Как мне работать с динамическими многомерными массивами в Си?

Можно ли вообще использовать матрицы как указатели на такие указатели? Я читал, что они работают одинаково, но не совсем так же. Как вы решаете изложенную проблему в C89?

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

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

Этот вопрос является синонимом статического массива и динамического массива в C++.

person Ardent Coder    schedule 19.01.2020

В C89, если у вас есть такая функция, как void func(int **mat, int cols, int rows), вы обращаетесь к элементам следующим образом, однако это должен быть один указатель:

int main(void)
{
    int arr1[10][10];                    // automatic array, allocated on the stack
    int *arr2= malloc(100*sizeof(int));  // dynamic array, allocated on heap
    func(arr1, 10, 10);                  // pass to func. 
    func(arr2, 10, 10);                  // pass to func. Same for both arrays
    return 0;
}
void func(int *mat, int cols, int rows)
{
    // address element x, y, with x the column number and y the row number:
    int k= mat[cols*y + x];

Поскольку компилятор ничего не знает о строках и столбцах, вы сами выполняете эти вычисления. Таким образом, вы сначала пропускаете y строк из cols столбцов, а затем берете x-й элемент.

РЕДАКТИРОВАТЬ:

Это работает для массивов, которые являются непрерывными блоками памяти, что эквивалентно VLA современного стандарта C.

Другой способ - «массив указателей на строки данных», но это не эквивалентно VLA, о котором вы спрашивали. Другие ответы обсуждают этот тип решения.

person Paul Ogilvie    schedule 19.01.2020
comment
Это совершенно неправильно и работает только в том случае, если матрица непрерывна в памяти, что никак не гарантируется сигнатурой функции. Динамически распределенная матрица на 100% приведет к сбою приведенного выше кода. - person Marco Bonelli; 19.01.2020
comment
Вы действительно можете сгладить массив в одномерную непрерывную структуру, при условии, что вы также создаете массив, используя ту же логику. Ваше решение также будет работать для реального 2D-массива, объявленного как 2D-массив, поскольку элементы являются смежными. Но это не будет работать с указателем указателя, потому что указатель указателя имеет другую компоновку. Повреждение памяти гарантировано. Было бы неплохо отредактировать ваш ответ, чтобы привлечь внимание OP к случаям, когда ваше решение работает, и к различным схемам памяти. - person Christophe; 19.01.2020
comment
@MarcoBonelli, это также работает для динамически выделяемой матрицы. Это не будет работать для массива указателей на массивы данных строк, но это не эквивалентно VLA, что было сутью вопроса. VLA непрерывны в памяти по определению. - person Paul Ogilvie; 20.01.2020
comment
@ Кристоф, посмотри мою реакцию на комментарий Марко: VLA по определению непрерывен в памяти. Эквивалентность VLA была ядром вопроса. - person Paul Ogilvie; 20.01.2020
comment
@PaulOgilvie, это никогда не будет работать для динамически выделяемой матрицы. Проверьте это сами. Массив указателей на массивы — это именно то, что представляет собой динамически выделяемая матрица. - person Marco Bonelli; 20.01.2020
comment
@MarcoBonelli, мое понимание динамически выделяемого массива в любой форме - это массив, выделяемый с помощью malloc. Однако ваше определение представляет собой массив указателей на строки данных, что (опять же) не эквивалентно VLA. - person Paul Ogilvie; 20.01.2020
comment
@MarcoBonelli, массив указателей на массивы данных строк необходимо будет передавать как int **, а не int *. - person Paul Ogilvie; 20.01.2020
comment
@PaulOgilvie, вы специально сказали: это также работает для динамически выделяемой матрицы. Это не. Это то, что я имел в виду. Конечно, если работает с массивом (который не является матрицей). - person Marco Bonelli; 20.01.2020
comment
@MarcoBonelli, я полагаю, у нас разница в используемых терминах, а не в понимании. - person Paul Ogilvie; 20.01.2020
comment
@PaulOgilvie хорошо, конечно, если вы говорите матрица, когда на самом деле имеете в виду массив, я не могу читать ваши мысли :') - person Marco Bonelli; 20.01.2020
comment
@MarcoBonelli, ну, мой ответ ясен. Я никогда не использовал слово «динамический» или «матрица» в своем ответе (до редактирования) и отметил, что для вашего массива указателей на строки требуется параметр int **, а не параметр int *. Так что никакой путаницы быть не может. - person Paul Ogilvie; 20.01.2020
comment
Я думаю, Пол и Марко, что у вас есть только две разные альтернативы для решения одной и той же проблемы, и каждая из них имеет свои плюсы и минусы: Marcos int** имеет преимущество естественной работы с нативной 2D-схемой индексации (но требует более сложной инициализации). , тогда как решение Пола имеет преимущество более эффективного управления памятью (но несовместимо со встроенным 2D). Лично я видел оба варианта в кодовых базах K&R и C89. Единственная проблема здесь - это первое предложение, которое может ввести OP в заблуждение, думая, что int ** похож на int *. - person Christophe; 20.01.2020