Правильное размещение многомерных массивов

Целью этого вопроса является предоставление справки о том, как правильно распределять многомерные массивы динамически на C. Это тема, которую часто неправильно понимают и плохо объясняют даже в некоторых книгах по программированию на C. Поэтому даже опытные программисты на C не могут понять это правильно.


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

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

Вот как меня учили размещать многомерные массивы:

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

int** arr_alloc (size_t x, size_t y)
{
  int** pp = malloc(sizeof(*pp) * x);
  assert(pp != NULL);
  for(size_t i=0; i<x; i++)
  {
    pp[i] = malloc(sizeof(**pp) * y);
    assert(pp[i] != NULL);
  }

  return pp;
}

int** arr_fill (int** pp, size_t x, size_t y)
{
  for(size_t i=0; i<x; i++)
  {
    for(size_t j=0; j<y; j++)
    {
      pp[i][j] = (int)j + 1;
    }
  }

  return pp;
}

void arr_print (int** pp, size_t x, size_t y)
{
  for(size_t i=0; i<x; i++)
  {
    for(size_t j=0; j<y; j++)
    {
      printf("%d ", pp[i][j]);
    }
    printf("\n");
  }
}

void arr_free (int** pp, size_t x, size_t y)
{
  (void) y;

  for(size_t i=0; i<x; i++)
  {
    free(pp[i]);
    pp[i] = NULL;
  }
  free(pp);
  pp = NULL;
}


int main (void)
{
  size_t x = 2;
  size_t y = 3;
  int** pp;

  pp = arr_alloc(x, y);
  pp = arr_fill(pp, x, y);
  arr_print(pp, x, y);
  arr_free(pp, x, y);

  return 0;
}

Выход

1 2 3
1 2 3

Этот код отлично работает! Как это могло быть неправильно?


person Lundin    schedule 07.02.2017    source источник


Ответы (2)


Чтобы ответить на этот вопрос, мы должны сначала прояснить некоторые концепции. Что такое массив и как его использовать? А какой в ​​вопрос код, если не массив?


Что такое массив?

Формальное определение массива можно найти в стандарте C, ISO 9899: 2011 6.2.5 / 20 типов.

Тип массива описывает непрерывно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента.

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

Например, массив из 3 целых чисел int arr[3] = {1,2,3}; будет размещен в памяти следующим образом:

+-------+-------+-------+
|       |       |       |
|   1   |   2   |   3   |
|       |       |       |
+-------+-------+-------+

Так что насчет формального определения многомерного массива? Фактически, это то же самое определение, что и приведенное выше. Применяется рекурсивно.

Если бы мы разместили 2D-массив, int arr[2][3] = { {1,2,3}, {1,2,3} }; он был бы размещен в памяти следующим образом:

+-------+-------+-------+-------+-------+-------+
|       |       |       |       |       |       |
|   1   |   2   |   3   |   1   |   2   |   3   |
|       |       |       |       |       |       |
+-------+-------+-------+-------+-------+-------+

В этом примере мы имеем дело с массивом массивов. Массив, состоящий из 2 элементов, каждый из которых представляет собой массив из 3 целых чисел.


Массив - это такой же тип, как и любой другой

Массивы в C часто следуют той же системе типов, что и обычные переменные. Как показано выше, у вас может быть массив массивов, как у вас может быть массив любого другого типа.

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

int arr[3] = {1,2,3};
int* ptr = arr; // integer pointer to the first element.

for(size_t i=0; i<3; i++)
{
  printf("%d ", *ptr); // print contents.
  ptr++; // set pointer to point at the next element.
}

Это стало возможным благодаря «распаду массива». Когда arr использовался внутри выражения, он «превращался» в указатель на первый элемент.

Точно так же мы можем использовать ту же арифметику с указателями для итерации по массиву массивов, используя указатель на массив:

int arr[2][3] = { {1,2,3}, {1,2,3} };
int (*ptr)[3] = arr; // int array pointer to the first element, which is an int[3] array.

for(size_t i=0; i<2; i++)
{
  printf("%d %d %d\n", (*ptr)[0], (*ptr)[1], (*ptr)[2]); // print contents
  ptr++; // set pointer to point at the next element
}

Снова произошел распад массива. Переменная arr типа int [2][3] превратилась в указатель на первый элемент. Первым элементом был int [3], а указатель на такой элемент объявлен как int(*)[3] - указатель на массив.

Понимание указателей на массивы и распада массива необходимо для работы с многомерными массивами.


Есть и другие случаи, когда массивы ведут себя так же, как обычные переменные. Оператор sizeof работает для массивов (не VLA) точно так же, как и для обычных переменных. Примеры для 32-битной системы:

int x; printf("%zu", sizeof(x)); печатает 4.
int arr[3] = {1,2,3}; printf("%zu", sizeof(arr)); печатает 12 (3 * 4 = 12)
int arr[2][3] = { {1,2,3}, {1,2,3} }; printf("%zu", sizeof(arr)); печатает 24 (2 * 3 * 4 = 24)


Как и любой другой тип, массивы можно использовать с библиотечными функциями и универсальными API. Поскольку массивы удовлетворяют требованию непрерывного размещения, мы можем, например, безопасно скопировать их с помощью memcpy:

int arr_a[3] = {1,2,3};
int arr_b[3];
memcpy(arr_b, arr_a, sizeof(arr_a));

Непрерывное размещение также является причиной, по которой работают другие подобные стандартные библиотечные функции, такие как memset, strcpy, bsearch и qsort. Они предназначены для работы с массивами, размещенными непрерывно. Так что, если у вас есть многомерный массив, вы можете эффективно искать в нем и сортировать его с помощью bsearch и qsort, избавляя вас от хлопот, связанных с реализацией двоичного поиска и быстрой сортировкой, и тем самым изобретая колесо для каждого проекта.

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


Что такое указатель на указатель, если не массив?

Теперь вернемся к коду вопроса, в котором использовался другой синтаксис с указателем на указатель. В этом нет ничего загадочного. Это указатель на указатель на тип, не больше и не меньше. Это не массив. Это не 2D-массив. Строго говоря, его нельзя использовать ни для указания на массив, ни для указания на двумерный массив.

Однако указатель на указатель может использоваться для указания на первый элемент массива указателей, вместо того, чтобы указывать на массив в целом. И вот как он используется в вопросе - как способ «имитировать» указатель на массив. В вопросе он используется для указания на массив из 2 указателей. И затем каждый из 2 указателей используется для указания на массив из 3 целых чисел.

Это известно как справочная таблица, которая представляет собой своего рода абстрактный тип данных (ADT), который чем-то отличается от концепции нижнего уровня простых массивов. Основное отличие заключается в том, как размещается справочная таблица:

+------------+
|            |
| 0x12340000 |
|            |
+------------+
      |
      |
      v
+------------+     +-------+-------+-------+
|            |     |       |       |       |
| 0x22223333 |---->|   1   |   2   |   3   |
|            |     |       |       |       |
+------------+     +-------+-------+-------+
|            | 
| 0xAAAABBBB |--+
|            |  | 
+------------+  |  
                |
                |  +-------+-------+-------+
                |  |       |       |       |
                +->|   1   |   2   |   3   |
                   |       |       |       |
                   +-------+-------+-------+

32-битные адреса в этом примере выдуманы. Поле 0x12340000 представляет указатель на указатель. Он содержит адрес 0x12340000 первого элемента в массиве указателей. Каждый указатель в этом массиве, в свою очередь, содержит адрес, указывающий на первый элемент в массиве целых чисел.

И здесь начинаются проблемы.


Проблемы с версией справочной таблицы

Таблица поиска разбросана по всей памяти кучи. Это не является непрерывно выделенной памятью в соседних ячейках, потому что каждый вызов malloc() дает новую область памяти, не обязательно расположенную рядом с другими. Это, в свою очередь, создает множество проблем:

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

  • Мы не можем использовать оператор sizeof. Используемый для указателя на указатель, он даст нам размер указателя на указатель. Используемый для первого указанного элемента, он даст нам размер указателя. Ни один из них не является размером массива.

  • Мы не можем использовать стандартные библиотечные функции, за исключением типа массива (memcpy, memset, strcpy, bsearch, qsort и т. Д.). Все такие функции предполагают получение массивов в качестве входных данных с непрерывно размещенными данными. Вызов их с помощью нашей таблицы поиска в качестве параметра приведет к неопределенным ошибкам поведения, таким как сбои программы.

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

  • Поскольку память разбросана, ЦП не может использовать кэш-память при итерации по таблице поиска. Для эффективного использования кеша данных требуется непрерывный фрагмент памяти, который повторяется сверху вниз. Это означает, что поисковая таблица по своей природе имеет значительно меньшее время доступа, чем реальный многомерный массив.

  • Для каждого вызова malloc() код библиотеки, управляющий кучей, должен вычислять, где есть свободное место. Точно так же для каждого вызова free() есть служебный код, который должен быть выполнен. Поэтому в целях повышения производительности зачастую предпочтительнее использовать как можно меньше вызовов этих функций.


Все ли справочные таблицы плохи?

Как мы видим, существует много проблем с таблицами поиска на основе указателей. Но не все они плохие, это такой же инструмент, как и любой другой. Его просто нужно использовать по назначению. Если вы ищете многомерный массив, который следует использовать как массив, таблицы поиска явно не подходят. Но их можно использовать и для других целей.

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

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


Как тогда правильно разместить многомерный массив динамически?

Самая простая форма в современном C - просто использовать массив переменной длины (VLA). int array[x][y];, где x и y - переменные, значения которых были заданы во время выполнения, до объявления массива. Однако VLA имеют локальную область действия и не сохраняются на протяжении всей программы - они имеют автоматическую продолжительность хранения. Таким образом, хотя VLA может быть удобным и быстрым в использовании для временных массивов, он не является универсальной заменой рассматриваемой таблицы поиска.

Чтобы действительно динамически распределить многомерный массив, чтобы он получил выделенную продолжительность хранения, мы должны использовать _42 _ / _ 43 _ / _ 44_. Ниже я приведу один пример.

В современном C вы должны использовать указатели массивов на VLA. Вы можете использовать такие указатели, даже если в программе нет фактического VLA. Преимущество их использования по сравнению с обычными type* или void* заключается в повышении безопасности типов. Использование указателя на VLA также позволяет передавать размеры массива в качестве параметров функции, использующей массив, что делает ее одновременно переменной и безопасной по типу.

К сожалению, чтобы использовать преимущества указателя на VLA, мы не можем вернуть этот указатель в качестве результата функции. Поэтому, если нам нужно вернуть указатель на массив вызывающей стороне, он должен быть передан как параметр (по причинам, описанным в Динамический доступ к памяти работает только внутри функции). Это прекрасная практика для C, но делает код немного трудным для чтения. Это выглядело бы примерно так:

void arr_alloc (size_t x, size_t y, int(**aptr)[x][y])
{
  *aptr = malloc( sizeof(int[x][y]) ); // allocate a true 2D array
  assert(*aptr != NULL);
}

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

void arr_alloc (size_t x, size_t y, size_t z, int(**aptr)[x][y][z])
{
  *aptr = malloc( sizeof(int[x][y][z]) ); // allocate a true 3D array
  assert(*aptr != NULL);
}

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

/* Bad. Don't write code like this! */
int*** arr_alloc (size_t x, size_t y, size_t z)
{
  int*** ppp = malloc(sizeof(*ppp) * x);
  assert(ppp != NULL);
  for(size_t i=0; i<x; i++)
  {
    ppp[i] = malloc(sizeof(**ppp) * y);
    assert(ppp[i] != NULL);
    for(size_t j=0; j<y; j++)
    {
      ppp[i][j] = malloc(sizeof(***ppp) * z);
      assert(ppp[i][j] != NULL);
    }
  }

  return ppp;
}

Теперь это - одна непонятная неразбериха "трехзвездочного программирования". И даже не говоря о четырех измерениях ...


Полный код версии, использующей настоящие 2D-массивы

#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

void arr_alloc (size_t x, size_t y, int(**aptr)[x][y])
{
  *aptr = malloc( sizeof(int[x][y]) ); // allocate a true 2D array
  assert(*aptr != NULL);
}

void arr_fill (size_t x, size_t y, int array[x][y])
{
  for(size_t i=0; i<x; i++)
  {
    for(size_t j=0; j<y; j++)
    {
      array[i][j] = (int)j + 1;
    }
  }
}

void arr_print (size_t x, size_t y, int array[x][y])
{
  for(size_t i=0; i<x; i++)
  {
    for(size_t j=0; j<y; j++)
    {
      printf("%d ", array[i][j]);
    }
    printf("\n");
  }
}

int main (void)
{
  size_t x = 2;
  size_t y = 3;
  int (*aptr)[x][y];

  arr_alloc(x, y, &aptr);
  arr_fill(x, y, *aptr);
  arr_print(x, y, *aptr);
  free(aptr); // free the whole 2D array

  return 0;
}
person Lundin    schedule 07.02.2017
comment
для ясности, ваше окончательное решение здесь полагается на поддержку VLA, которая появилась в C99? - person yano; 07.02.2017
comment
@yano Да, VLA были введены 18 лет назад. - person Lundin; 08.02.2017
comment
@DavidBowling Это было написано специально для вопросов о том, как правильно динамически распределять многомерные массивы. Распад массива и массив против указателя и т. д. уже были охвачены довольно хорошими каноническими дубликатами. - person Lundin; 08.02.2017
comment
Хорошо написано и нужен был ответ. Но меня беспокоит одно: зачем упоминать bsearch/qsort? Они предназначены для работы в одном измерении. Если вы используете их для сортировки указателей в первом измерении массива p2p, он работает так же, как сортировка строк в 2D-массиве, при условии, что пользователь определяет соответствующую функцию сравнения и дает допустимые аргументы. - person user694733; 15.02.2017
comment
@ user694733 Ну, конечно, вы можете избежать проблемы, обработав доступ в переданной функции, bsearch / qsort довольно гибкие. Но предположим, что вы, например, хотите найти / отсортировать общий кусок необработанных данных. Вы можете сделать это нормально, передав n-мерный массив в bsearch / qsort. Или предположим, что у вас есть n-мерный массив uint32_t и вы хотите искать отдельные байтовые значения. Сортировка пикселей в графическом растровом изображении по цвету и т. Д. - person Lundin; 15.02.2017
comment
Другой момент здесь заключается в том, что если вы передадите истинный n-мерный массив этим функциям, он все равно будет работать, потому что int array[x][y][z] на самом деле является одномерным массивом. Это одномерный массив из x элементов типа int [y][z]. - person Lundin; 15.02.2017
comment
Хорошо, я понимаю, что ты имеешь в виду. Я собирался предложить вам что-то добавить к ответу, но, возможно, в этом нет необходимости. Это уже длинный ответ, и главное - важнее. - person user694733; 16.02.2017
comment
избавит вас от хлопот по реализации двоичного поиска и быстрой сортировки самостоятельно - придирка, но _ 1_ не обязательно реализовывать как быструю сортировку. Это предложение может ввести людей в заблуждение, заставив их задуматься о том, сколько памяти / времени занимает функция, которой они не должны заниматься. - person StoryTeller - Unslander Monica; 19.02.2017
comment
@ RestlessC0bra 1) Правильно, хотя определение, какие строки и какие столбцы лежит в приложении. Стандарт C требует только наличия y смежных сегментов x смежных переменных данного типа. 2) Правильно. 3) Действительно - указатель на VLA не обязательно должен указывать на объект с автоматической продолжительностью хранения или даже на VLA. Указатель в форме type (*name)[n], где n - значение времени выполнения, может быть установлен так, чтобы указывать на любой массив того же типа и размера, независимо от того, где он размещен. - person Lundin; 17.03.2017
comment
@ RestlessC0bra Если он не размещен в непрерывной памяти, это не массив. Суть всего этого поста заключалась в том, чтобы развеять наши неверные убеждения. - person Lundin; 17.03.2017
comment
@Lundin Отличная статья! Всегда впечатлен полнотой ваших знаний Си и вашей способностью так хорошо объяснять! - person Toby; 06.04.2017
comment
@Lundin, с каких пор ты понял, что делаешь это неправильно, и приобрел все это глубокое понимание.? Хорошо написанный ответ, стоит следить за вашей страницей, я многому научился. У меня всегда было неправильное представление, я думал, что указатель на указатель работает быстрее и эффективнее. - person Seek Addo; 11.04.2017
comment
Я должен согласиться с @ user694733 и добавить, что в целом мы не можем использовать стандартные библиотечные функции, за исключением типа массива (memcpy, memset, strcpy, bsearch, qsort и т. Д.). звучит слишком широко. memcpy, qsort и т. Д. Прекрасно работают для массива указателя на T, или другими словами указателя на указатель на T, указывающего на первый элемент такого. Хотя вы можете делать только неглубокие копии, это очень полезно, например, для строк. В некоторых ситуациях сортировка массива указателей может быть полезной по сравнению с перемещением массивов. Тем не менее, отличное описание. - person Ilja Everilä; 03.05.2017
comment
Вместо *aptr = malloc( sizeof(int[x][y]) ); используйте *aptr = malloc(sizeof **aptr); для соответствия идиоматически правильному pointer = malloc(sizeof *pointer);. - person chux - Reinstate Monica; 05.05.2017
comment
Вы говорите, что формальное определение массива найдено ... но затем вы цитируете формальное определение типа массива. Фактически стандарт нигде формально не определяет array. - person M.M; 13.12.2017
comment
Вместо указателя на int[x][y], не лучше ли иметь указатель на int[y]? Сохраняет лишнее разыменование при использовании (в основном позволяет избежать путаницы). Я согласен, что он удаляет самую внешнюю информацию о размере. - person Ajay Brahmakshatriya; 13.12.2017
comment
@AjayBrahmakshatriya Да, это распространенный трюк, но я не использовал его здесь специально, так как читатель, возможно, вообще не знаком с указателями на массивы. - person Lundin; 13.12.2017
comment
Повторные вызовы malloc для выделения нескольких сегментов приводят к фрагментации кучи, что, в свою очередь, приводит к неэффективному использованию оперативной памяти Практически тривиально динамически выделять N-мерный массив с помощью только N + 1 вызовов malloc(), и возможно, хотя и нетривиально, выделить один с помощью одного вызова. - person Andrew Henle; 14.12.2017
comment
Спасибо за столь подробный ответ. Устранены все мои заблуждения. - person Varad Bhatnagar; 22.03.2018
comment
@Lundin int (*arr)[5]; printf("%d\n", &arr[0]); printf("%d\n", &arr[1]); Он печатает 0 and 20 Возможно ли, когда вы делаете type (*ptr)[x][y], вы устанавливаете размер шага (++ptr) как y * sizeof(type)? Когда я пробовал то же самое в 2D-массиве, я наблюдал то же самое. - person bca; 18.10.2018
comment
Очень подробный и компромиссный ответ, заслуживающий УФ. - person anastaciu; 20.02.2020

C не имеет многомерных массивов (как примитивный тип данных). Но у вас могут быть массивы массивов (или других агрегатов) и массивы указателей.

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

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

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

Этот код отлично работает! Как это могло быть неправильно?

Это предложение непоследовательно (неверно, какие спецификации?) ...

Я рекомендую компилировать со всеми предупреждениями и отладочной информацией (например, с gcc -Wall -Wextra -g с GCC), чтобы улучшить свой код до тех пор, пока вы не получите никаких предупреждений, чтобы использовать отладчик gdb (чтобы понять, что происходит в вашей программе) и другие инструменты, такие как valgrind.

person Basile Starynkevitch    schedule 13.12.2017
comment
Как это отвечает на вопрос, что не так с этим методом динамического выделения 2D-массивов / массивов массивов? - person Lundin; 13.12.2017
comment
2D-массивов не существует. - person Basile Starynkevitch; 13.12.2017
comment
Это очень распространенный де-факто стандартный отраслевой термин, означающий массив массивов. Тем не менее, вопрос не содержит ли not массива массивов, и в этом весь смысл. Если вы хотите загромождать этот пост, добавьте хотя бы что-нибудь значимое. Совершенно непонятно, насколько гибкие элементы массива могут быть здесь полезным решением и в чем заключаются их преимущества. - person Lundin; 13.12.2017
comment
Решение не в гибких элементах массива, а в определении и реализации некоторого абстрактного типа данных. В этом ответе я использую гибкие элементы массива, но OP может использовать другой подход. Я даже не пытаюсь угадать, в чем заключается исходная проблема OP и какой абстрактный тип данных он должен учитывать (потому что у меня нет текста домашнего задания, данного OP). - person Basile Starynkevitch; 13.12.2017
comment
Скорее, вы используете искаженный массив C90, который не является современным C, у нас есть указатели на VLA почти 20 лет. Кроме того, решение ADT страдает от очень плохого времени доступа, поскольку вы эффективно блокируете любую возможность использования кэша данных при итерации по контейнеру. Также невозможно захватить блоки данных из матрицы. Хотя я согласен с тем, что ADT является хорошей системой решения с точки зрения дизайна, производительность будет очень низкой. - person Lundin; 13.12.2017
comment
Я не понимаю, почему решение ADT страдает от плохого времени доступа. Функции могут быть inlined, если эффективность важна. - person Basile Starynkevitch; 13.12.2017
comment
Скорее всего, он не будет встроен компилятором. Даже если бы это было так, и это не испортило доступ к кешу, у вас все равно будет несколько дополнительных ветвей на итерацию, что убивает производительность. - person Lundin; 13.12.2017
comment
Я думаю, что этот ответ можно улучшить (если вас все еще интересует), показывая минимальную реализацию и варианты использования, в которых можно использовать абстрактный тип данных (в то время как VLA не может), например, как член структуры или в массиве, или просто как возвращаемое значение. - person Bob__; 10.10.2018
comment
мы можем выполнить malloc для большой таблицы, например 301x301, и мы можем выполнить математику для логического доступа (200, 100) с индексом 100 * 300 + 200. С помощью этой логики мы можем иметь уникальный вызов malloc и с помощью этого математического трюка, который мы можно скрыть в МАКРОСАХ для доступа, как и раньше. И поскольку это контигус, мы можем использовать memset или calloc для очистки (и не создавать собственную функцию: например, arr_alloc и arr_free в первом сообщении) - person Et7f3XIV; 18.01.2019
comment
«C не имеет многомерных массивов» - это то же самое, что сказать, что C не имеет отрицательных чисел. Проверьте грамматику; нет отрицательных констант. Вы можете использовать только положительные константы и применять унарный оператор -. Конечно, в C есть отрицательные числа и многомерные массивы. Оба они просто построены из примитивов, а не сами являются примитивами. - person Eric Postpischil; 08.08.2019
comment
В C нет многомерных массивов? Я думаю, вы слишком зацикливаетесь на педантичности. Согласно 6.5.2.1 индексирование массива, параграф 3 стандарта C 11 (выделено жирным шрифтом): последовательные операторы индекса обозначают элемент объекта многомерного массива. Если E - это n-мерный массив (n ›= 2) с размерами i x j x. . . xk, то E (используемый как значение, отличное от lvalue) преобразуется в указатель на (n - 1) -мерный массив ... Если в стандарте C можно использовать термин объект многомерного массива .. . - person Andrew Henle; 22.07.2021
comment
(продолжение) сказать, что многомерные массивы не являются примитивными объектами, примерно так же полезно, как сказать, что struct или union не являются примитивными объектами. - person Andrew Henle; 22.07.2021