Подтвердить что ты не робот

Почему массивы в C распадаются на указатели?

[Это вопрос, вдохновленный недавней дискуссией в другом месте, и я дам вам ответ на это.]

Мне было интересно, что нечетное явление C массивов, "разлагающихся" на указатели, например. при использовании в качестве аргументов функции. Это просто кажется небезопасным. Также неудобно передавать длину с ней явно. И я могу передать другой тип агрегатных структур - отлично по стоимости; структуры не распадаются.

В чем смысл этого дизайнерского решения? Как он интегрируется с языком? Почему существует различие в структурах?

4b9b3361

Ответ 1

Обоснование

Давайте рассмотрим вызовы функций, потому что здесь хорошо видны проблемы: Почему массивы не просто передаются в функции как массивы, по значению, как копия?

Во-первых, существует чисто прагматическая причина: массивы могут быть большими; не рекомендуется передавать их по значению, потому что они может превышать размер стека, особенно в 1970-х годах. Первые компиляторы были написаны на PDP-7 с объемом оперативной памяти около 9 КБ.

Существует также более техническая причина, коренящаяся в языке. Было бы сложно сгенерировать код для вызова функции с аргументами, размер которых неизвестен во время компиляции. Для всех массивов, включая массивы переменной длины в современном C, просто адреса помещаются в стек вызовов. Размер адреса, конечно, хорошо известен. Даже языки со сложными типами массивов, несущими информацию о размере во время выполнения, не передают объекты должным образом в стеке. Эти языки, как правило, проходят "ручку", что C тоже эффективно делал в течение 40 лет. См. джон Скит здесь и иллюстрированное объяснение, на которое он ссылается (sic) здесь.

Теперь язык может требовать, чтобы массив всегда имел полный тип; то есть всякий раз, когда он используется, его полное объявление, включая размер, должно быть видимым. Это, в конце концов, то, что C требует от структур (когда к ним обращаются). Следовательно, структуры могут быть переданы в функции по значению. Требование полного типа для массивов также сделало бы вызовы функций легко компилируемыми и избавило бы от необходимости передавать дополнительные аргументы длины: sizeof() все еще работал бы как ожидалось внутри вызываемого. Но представьте, что это значит. Если бы размер был действительно частью типа аргумента массива, нам нужна отдельная функция для каждого размера массива:

// for user input.
int average_ten(int arr[10]);

// for my new Hasselblad.
int average_twohundredfivemilliononehundredfourtyfivethousandsixhundred(int arr[16544*12400]);
// ...

На самом деле это было бы полностью сопоставимо с проходящими структурами, которые различаются по типу, если их элементы различаются (скажем, одна структура с 10 элементами int и одна с 16544 * 12400). Очевидно, что массивы нуждаются в большей гибкости. Например, как было показано, невозможно разумно предоставить общедоступные библиотечные функции, которые принимают аргументы массива.

Эта "головоломка со строгой типизацией" фактически является тем, что происходит в C++, когда функция получает ссылку на массив; это также причина, почему никто не делает этого, по крайней мере, явно. Это совершенно неудобно до такой степени, что бесполезно, за исключением случаев, которые предназначены для конкретных целей, и в общем коде: шаблоны C++ обеспечивают гибкость во время компиляции, которая недоступна в C.

Если в существующем C действительно массивы известных размеров должны передаваться по значению, всегда есть возможность обернуть их в структуру. Я помню, что некоторые связанные с IP заголовки в Solaris определяли структуры семейства адресов с массивами в них, что позволяло копировать их. Поскольку структура байтов структуры была фиксированной и известной, это имело смысл.

Для некоторой предыстории также интересно прочитать "Развитие языка C" Денниса Ритчи о происхождении C. C-предшественник BCPL не имел никаких массивов; память была просто однородной линейной памятью с указателями на нее.

Ответ 2

Ответ на этот вопрос можно найти в документе Dennis Ritchie "Развитие языка C" (см. раздел "Эмбриональный С" )

Согласно Деннису Ритчи, зарождающиеся версии C непосредственно унаследовали/приняли семантику массива из языков B и BCPL - предшественников C. В этих языках массивы были буквально реализованы как физические указатели. Эти указатели указывали на независимо выделенные блоки памяти, содержащие фактические элементы массива. Эти указатели были инициализированы во время выполнения. То есть назад в B и BCPL массивы дней были реализованы как "двоичные" (двудольные) объекты: независимый указатель, указывающий на независимый блок данных. Не было разницы между семантикой указателя и массива на этих языках, кроме того, что указатели массива были инициализированы автоматически. В любой момент можно было повторно назначить указатель массива в B и BCPL, чтобы он указывал в другом месте.

Изначально этот подход к семантике массивов получил наследование С. Однако его недостатки стали очевидными, когда типы struct были введены в язык (что-то не было ни B, ни BCPL). И идея заключалась в том, что структуры, естественно, могут содержать массивы. Тем не менее, продолжая придерживаться вышеупомянутой "двудольной" природы массивов B/BCPL, сразу же будет иметь место ряд очевидных осложнений со структурами. Например. структурные объекты с массивами внутри должны требовать нетривиальной "конструкции" в точке определения. Было бы невозможно копировать такие объекты структуры - вызов raw memcpy скопировал бы указатели массива без копирования фактических данных. Нельзя было бы malloc структурировать объекты, так как malloc может выделять необработанную память и не запускать никаких нетривиальных инициализаций. И так далее и т.д.

Это было сочтено неприемлемым, что привело к реорганизации массивов C. Вместо того, чтобы реализовывать массивы с помощью физических указателей, Ричи решил полностью избавиться от указателей. Новый массив был реализован как единый блок мгновенной памяти, и это именно то, что мы имеем сегодня в C. Однако для соображений обратной совместимости поведение массивов B/BCPL было сохранено (эмулировано) как можно больше на поверхностном уровне: новый массив C легко распадается на временное значение указателя, указывая на начало массива. Остальная часть функциональности массива осталась неизменной, опираясь на этот легко доступный результат распада.

Чтобы процитировать вышеупомянутую статью

Решение составляло решающий прыжок в эволюционной цепочке между бесчисленным BCPL и типизированным C. Он исключил материализацию указателя в хранилище, и вместо этого вызвали создание указатель, когда имя массива упоминается в выражении. Правило, который выживает в настоящее время C, заключается в том, что значения типа массива преобразуются, когда они появляются в выражениях, в указатели на первый объектов, составляющих массив.

Это изобретение позволило большинству существующих B-кодов продолжать работу, несмотря на основной сдвиг в семантике языка. Несколько программы, которые назначили новые значения имени массива, чтобы настроить его происхождение - возможное в B и BCPL, бессмысленное в C-легко ремонтируется. Что более важно, новый язык сохранил согласованность и выполнимость (если необычное) объяснение семантики массивов, открывая путь к более сложной структуре типов.

Итак, прямой ответ на ваш вопрос "почему" следующий: массивы в C были предназначены для распада указателей, чтобы имитировать (как можно ближе) историческое поведение массивов на языках B и BCPL.

Ответ 3

Возьмите машину времени и вернитесь в 1970 году. Начните разработку языка программирования. Вам нужен следующий код для компиляции и выполнения ожидаемой вещи:

size_t i;
int* p = (int *) malloc (10 * sizeof (int));
for (i = 0; i < 10; ++i) p [i] = i;

int a [10];
for (i = 0; i < 10; ++i) a [i] = i;

В то же время вам нужен простой язык. Достаточно просто, чтобы вы могли скомпилировать его на компьютере 1970 года. Правило, которое "а" распадается на "указатель на первый элемент", достигает этого красиво.