Почему массивы C не могут иметь длину 0?

13

Стандарт C11 гласит, что массивы как размера, так и переменной длины «должны иметь значение больше нуля». Каково оправдание для того, чтобы не допустить длину 0?

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

Интересно, что GCC (и clang) предоставляют расширения, которые допускают массивы нулевой длины. Java также допускает массивы нулевой длины.

Кевин Кокс
источник
7
stackoverflow.com/q/8625572 ... "Массив нулевой длины будет сложно и сбить с толку, чтобы согласовать с требованием, чтобы каждый объект имел уникальный адрес."
Роберт Харви
3
@RobertHarvey: Учитывая struct { int p[1],q[1]; } foo; int *pp = p+1;, ppбудет законным указателем, но *ppне будет иметь уникальный адрес. Почему та же логика не может выполняться с массивом нулевой длины? Говорят , что дано int q[0]; в структуре , qбудет ссылаться на адрес которого действительность будет , как , что в p+1приведенном выше примере.
суперкат
@DocBrown Из стандарта C11 6.7.6.2.5 говорится о выражении, используемом для определения размера VLA: «… каждый раз, когда он оценивается, он должен иметь значение больше нуля». Я не знаю о C99 (и кажется странным, что они меняют его), но похоже, что вы не можете иметь длину ноль.
Кевин Кокс,
@KevinCox: есть ли бесплатная онлайн версия стандарта C11 (или рассматриваемая часть)?
Док Браун
Финальная версия не доступна бесплатно (какой позор), но вы можете скачать черновики. Последний доступный черновик - open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf .
Кевин Кокс

Ответы:

11

Я хотел бы поспорить, что массивы C - это всего лишь указатели на начало выделенного фрагмента памяти. Наличие размера 0 будет означать, что у вас есть указатель на ... ничего? Вы не можете иметь ничего, поэтому пришлось бы выбирать какую-то произвольную вещь. Вы не можете использовать null, потому что тогда ваши массивы 0 будут выглядеть как нулевые указатели. И в этот момент каждая другая реализация выберет различное произвольное поведение, что приведет к хаосу.

Telastyn
источник
8
@delnan: Хорошо, если вы хотите быть педантичным, арифметика массива и указателя определяется так, чтобы указатель можно было удобно использовать для доступа к массиву или для имитации массива. Другими словами, это арифметика указателей и индексация массива эквивалентны в C. Но в любом случае результат одинаков ... если длина массива равна нулю, вы все равно указываете на ничто.
Роберт Харви
3
@RobertHarvey Все верно, но ваши заключительные слова (и весь ответ в ретроспективе) просто кажутся запутанным и сбивающим с толку способом объяснить, что такой массив (я думаю , это то, что этот ответ называет «выделенным куском памяти»?) sizeof0, и как это вызовет проблемы. Все это можно объяснить, используя правильные понятия и терминологию без потери краткости или ясности. Смешивание массивов и указателей только рискует распространить массивы = неправильное представление указателей (что более важно в других контекстах) без пользы.
2
« Вы не можете использовать нуль, потому что тогда ваши массивы длины 0 будут выглядеть как нулевые указатели » - на самом деле это именно то, что делает Delphi. Пустые dynarrays и пустые длинные строки - технически нулевые указатели.
JensG
3
-1, я полон @delnan здесь. Это ничего не объясняет, особенно в контексте того, что ОП написал о некоторых основных компиляторах, поддерживающих концепцию массивов нулевой длины. Я почти уверен, что массивы нулевой длины могут быть предоставлены в C независимым от реализации способом, не "приводящим к хаосу".
Док Браун
6

Давайте посмотрим, как массив обычно располагается в памяти:

         +----+
arr[0] : |    |
         +----+
arr[1] : |    |
         +----+
arr[2] : |    |
         +----+
          ...
         +----+
arr[n] : |    |
         +----+

Обратите внимание, что не существует отдельного объекта с именем, arrкоторый хранит адрес первого элемента; когда в выражении появляется массив, C вычисляет адрес первого элемента по мере необходимости.

Итак, давайте думать об этом: массив 0-элемент не будет иметь не хранения отведенные для него, а это означает , что нет ничего , чтобы вычислить адрес массива из (иначе говоря, нет никакого отображения объекта для идентификатора). Это все равно что сказать: «Я хочу создать intпеременную, которая не занимает памяти». Это бессмысленная операция.

редактировать

Java-массивы полностью отличаются от массивов C и C ++; это не примитивный тип, а ссылочный тип, полученный из Object.

Редактировать 2

Точка, затронутая в комментариях ниже - ограничение «больше 0» применяется только к массивам, размер которых указан через константное выражение ; VLA может иметь длину 0. Объявление VLA с 0-значным непостоянным выражением не является нарушением ограничения, но оно вызывает неопределенное поведение.

Понятно, что VLA - это разные животные от обычных массивов , и их реализация может учитывать размер 0 . Они не могут быть объявлены staticили находятся в области видимости файла, потому что размер таких объектов должен быть известен до запуска программы.

Также ничего не стоит, что начиная с C11 реализации не обязаны поддерживать VLA.

Джон Боде
источник
3
Извините, но ИМХО, вы упускаете суть, как и Теластин. Массивы нулевой длины могут иметь большой смысл, и существующие реализации, подобные тем, о которых нам рассказывал ОП, показывают, что это можно сделать.
Док Браун
@DocBrown: Сначала я говорил о том, почему языковой стандарт, скорее всего, запрещает их. Во-вторых, я хотел бы привести пример, когда массив с 0 длинами имеет смысл, потому что я, честно говоря, не могу придумать один. Наиболее вероятная реализация состоит в том, чтобы рассматривать T a[0]как T *a, но тогда почему бы просто не использовать T *a?
Джон Боде
Извините, но я не покупаю «теоретическое обоснование» того, почему стандарт запрещает это. Прочитайте мой ответ, как адрес может быть легко вычислен. И я предлагаю вам перейти по ссылке в первом комментарии Роберта Харвиза под вопросом и прочитать второй ответ, есть полезный пример.
Док Браун
@DocBrown: Ах. structХак. Я никогда не использовал это лично; никогда не работал над проблемой, которая нуждалась в structтипе переменного размера .
Джон Боде
2
И не забывать AFAIK, так как C99, C позволяет массивы переменной длины. И когда размер массива является параметром, отсутствие необходимости рассматривать значение 0 как особый случай может упростить многие программы.
Док Браун
2

Вы обычно хотели бы, чтобы ваш массив нулевого (фактически переменного) размера знал свой размер во время выполнения. Затем упакуйте это в a structи используйте элементы гибкого массива , например, например:

struct my_st {
   unsigned len;
   double flexarray[]; // of size len
};

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

Конечно, вы бы выделить:

 unsigned len = some_length_computation();
 struct my_st*p = malloc(sizeof(struct my_st)+len*sizeof(double));
 if (!p) { perror("malloc my_st"); exit(EXIT_FAILURE); };
 p->len = len;
 for (unsigned ix=0; ix<len; ix++)
    p->flexarray[ix] = log(3.0+(double)ix);

AFAIK, это было уже возможно в C99, и это очень полезно.

Кстати, гибких элементов массива не существует в C ++ (потому что было бы трудно определить, когда и как они должны быть построены и уничтожены). Смотрите, однако, будущее std :: dynarray

Василий Старынкевич
источник
Вы знаете, они могут быть ограничены тривиальными типами, и не будет никаких трудностей.
Дедупликатор
2

Если выражение type name[count]записано в какой-то функции, то вы указываете компилятору C выделить sizeof(type)*countбайты кадра стека и вычислить адрес первого элемента в массиве.

Если выражение type name[count]написано вне всех функций и структурирует определения, то вы указываете компилятору C выделить sizeof(type)*countбайты сегмента данных и вычислить адрес первого элемента в массиве.

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

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

Это рационально, что элемента нет. count+1не существует в countмассиве с длинной длиной, так что это причина, по которой компилятор C запрещает определять массив нулевой длины как переменную внутри и снаружи функции, потому что в чем тогда содержимое name? Какой адрес nameхранит именно?

Если pуказатель, то выражение p[n]эквивалентно*(p + n)

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

Можно ли добавить адрес и номер?

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

user307542
источник
Многие компиляторы разрешали объявления массивов нулевого размера до того, как стандарт запретил это, и многие продолжают разрешать такие объявления как расширение. Такие объявления не вызовут проблем, если вы поймете, что объект размера Nимеет N+1ассоциированные адреса, первый Nиз которых идентифицирует уникальные байты, а последний Nиз которых указывает только один из этих байтов. Такое определение будет прекрасно работать даже в вырожденном случае, где N0.
суперкат
1

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

ncmathsadist
источник
2
Как правило, вы не будете делать это напрямую, но в результате макроса или при объявлении массива переменной длины с динамическими данными.
Кевин Кокс
Массив не указывает никогда. Он может содержать указатели, и в большинстве случаев вы фактически используете указатель на первый элемент, но это другая история.
Дедупликатор
1
Имя массива является постоянным указателем на память, содержащуюся в массиве.
ncmathsadist
1
Нет, имя массива исчезает до указателя на первый элемент, в большинстве случаев. Разница часто имеет решающее значение.
Дедупликатор
1

Со времен первоначального C89, когда в стандарте C указывалось, что что-то имеет неопределенное поведение, это означало: «Делайте все, что сделает реализацию на конкретной целевой платформе наиболее подходящей для ее предполагаемого назначения». Авторы Стандарта не хотели пытаться угадать, какое поведение может быть наиболее подходящим для какой-либо конкретной цели. Существующие реализации C89 с расширениями VLA могли иметь разные, но логичные, поведения при заданном размере нуля (например, некоторые могли бы трактовать массив как выражение адреса, приводящее к NULL, в то время как другие рассматривали его как выражение адреса, которое могло бы равняться адресу другая произвольная переменная, но в нее можно было бы добавить ноль без перехвата). Если какой-либо код полагается на такое различное поведение, авторы Стандарта не

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

Supercat
источник