Чому масиви 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наведеному вище прикладі.
supercat

@DocBrown Згідно зі стандартом C11 6.7.6.2.5, що говорить про вираз, який використовується для визначення розміру VLA, "... щоразу, коли він оцінюється, він повинен мати значення, що перевищує нуль". Я не знаю про C99 (і здається дивним, що вони змінили б його), але це звучить так, що ви не можете мати довжину нуля.
Кевін Кокс

@KevinCox: чи доступна безкоштовна онлайн-версія стандарту C11 (або деталі, про яку йдеться)?
Doc Brown

Кінцева версія не доступна безкоштовно (яка ганьба), але ви можете завантажити чернетки. Останній доступний проект - open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf .
Кевін Кокс

Відповіді:


11

Я б заперечив, що масиви С - лише вказівки на початок виділеного фрагмента пам'яті. Якщо розмір 0 означає, що у вас є вказівник на ... нічого? Ви нічого не можете мати, тому довелося б обрати якусь довільну річ. Ви не можете використовувати null, тому що тоді ваші масиви довжиною 0 виглядатимуть як нульові покажчики. І в цей момент кожна різна реалізація збирається вибирати різні довільні поведінки, що призводить до хаосу.



8
@delnan: Ну, якщо ви хочете бути педантичним щодо цього, арифметика масиву та покажчика визначена таким чином, що вказівник можна зручно використовувати для доступу до масиву або для імітації масиву. Іншими словами, це арифметична вказівка ​​та індексація масиву, які є еквівалентними в С. Але результат все одно такий же ... якщо довжина масиву дорівнює нулю, ви все одно не вказуєте ні на що.
Роберт Харві

3
@RobertHarvey Все правда, але ваші заключні слова (і вся відповідь заднім числом) просто здаються заплутаним і заплутаним способом пояснити, що такий масив (я думаю, що саме ця відповідь називає "виділений шматок пам'яті"?) sizeof0, і як це може спричинити неприємності. Все це можна пояснити, використовуючи відповідні поняття та термінологію, не втрачаючи стислості чи ясності. Змішування масивів і покажчиків лише ризикує поширити масиви = неправильне уявлення покажчиків (що важливіше в інших контекстах) без користі.

2
" Ви не можете використовувати null, тому що тоді ваші масиви довжиною 0 виглядатимуть як нульові вказівники " - насправді саме це робить Delphi. Порожні динаррі та порожні довгі рядки технічно є нульовими покажчиками.
JensG

3
-1, я тут повно @delnan. Це нічого не пояснює, особливо в контексті того, що ОП писало про деякі основні компілятори, що підтримують концепцію масивів нульової довжини. Я впевнений, що масиви нульової довжини можуть бути надані в С незалежно від реалізації, не "ведучи до хаосу".
Док Браун

6

Давайте розглянемо, як масив зазвичай викладається в пам'ять:

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

Зауважте, що не існує окремого імені, arrякий має назву, який зберігає адресу першого елемента; коли масив з'являється у виразі, C обчислює адресу першого елемента за потребою.

Отже, давайте думати про це: масив 0-елемент не матиме не збереження відведені для нього, а це означає , що немає нічого , щоб обчислити адресу масиву з (інакше кажучи, немає ніякого відображення об'єкта для ідентифікатора). Це як сказати: "Я хочу створити intзмінну, яка не займає пам'яті". Це безглузда операція.

Редагувати

Масиви Java - це абсолютно різні тварини від масивів С і С ++; вони не примітивного типу, а похідного типу Object.

Редагувати 2

Точка, викладена в коментарях нижче - обмеження "більше 0" застосовується лише до масивів, де розмір визначається за допомогою постійного вираження ; для VLA дозволено мати довжину 0. Оголошення VLA з 0-значущим постійним виразом не є порушенням обмеження, але воно викликає невизначене поведінку.

Зрозуміло, що VLA - це різні тварини від звичайних масивів , і їх реалізація може враховувати розмір 0 . Вони не можуть бути оголошені staticабо в області файлу, оскільки розмір таких об'єктів повинен бути відомий до запуску програми.

Також нічого не варто, що, як і для C11, для підтримки VLA не потрібні реалізації.


3
Вибачте, але в ІМХО ви пропустите точку, як і Теластин. Масиви нульової довжини можуть мати багато сенсу, а існуючі реалізації, як ті, про які нам розповіла ОП, показують, що це можна зробити.
Doc Brown

@DocBrown: По-перше, я говорив про те, чому стандарт мови, швидше за все, забороняє їх. По-друге, я хотів би приклад того, де масив довжиною 0 має сенс, тому що я, чесно кажучи, не можу придумати його. Найімовірніша реалізація - це трактувати T a[0]як T *a, але чому б тоді не просто використовувати T *a?
Джон Боде

Вибачте, але я не купую "теоретичні міркування", чому стандарт забороняє це. Прочитайте мою відповідь, як можна було легко обчислити адресу. І я пропоную вам перейти за посиланням у першому коментарі під питанням Роберта Харвіса і прочитати другу відповідь, є корисний приклад.
Doc Brown

@DocBrown: Ага. structХак. Я ніколи не використовував це особисто; ніколи не працював над проблемою, яка потребувала типу змінного розміру struct.
Джон Боде

2
І не забувати AFAIK з C99, C дозволяє використовувати масиви змінної довжини. І коли розмір масиву є параметром, не потребуючи трактування значення 0 як особливого випадку, можна зробити багато програм простішими.
Doc Brown

2

Зазвичай ви хочете, щоб ваш нульовий (фактично змінний) масив знав його розмір під час виконання. Потім упакуйте це в 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, і це дуже корисно.

BTW, гнучких членів масиву не існує в 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масиву -length, тому це є причиною того, що компілятор C забороняє визначати масив нульової довжини як змінну в і поза функцією, тому що тоді вміст name? Яка адреса nameзберігається саме?

Якщо pвказівник, то вираз p[n]еквівалентний*(p + n)

Якщо зірочка * у правильному виразі - це операція з відмежуванням вказівника, що означає доступ до пам’яті, на яку вказує, p + nабо доступ до пам’яті, адреса якої зберігається p + n, де p + nвираження вказівника, вона бере адресу pта додає до цієї адреси число, nпомножуючи на розмір типу вказівника p.

Чи можна додати адресу та номер?

Так, це можливо, тому що адреса є цілим числом без підпису, зазвичай представленим у шістнадцятковій нотації.


Багато компіляторів використовували для дозволу оголошень масиву нульового розміру до того, як Стандарт заборонив це, а багато хто продовжує дозволяти такі декларації як розширення. Такі декларації не спричинить жодних проблем, якщо визнати, що об'єкт розміру Nмає N+1асоційовані адреси, перший Nз яких ідентифікує унікальні байти, а останній Nз яких вказує лише один із цих байтів. Таке визначення спрацювало б добре навіть у виродженому випадку, де Nдорівнює 0.
supercat

1

Якщо ви хочете вказівник на адресу пам'яті, оголосіть її. Масив фактично вказує на фрагмент пам'яті, який ви зарезервували. Масиви розпадаються на покажчики при переході до функцій, але якщо пам'ять, на яку вони вказують, знаходиться у купі, проблем немає. Немає підстав оголошувати масив розміром нуля.


2
Як правило, ви б цього не робили безпосередньо, але в результаті макросу або при оголошенні масиву змінної довжини з динамічними даними.
Кевін Кокс

Масив не вказує ніколи. Він може містити вказівники, і в більшості контекстів ви фактично використовуєте вказівник на перший елемент, але це вже інша історія.
Дедуплікатор

1
Ім'я масиву - це постійний вказівник на пам'ять, що міститься в масиві.
ncmathsadist

1
Ні, ім'я масиву розпадається на покажчик на перший елемент, в більшості контекстів. Різниця часто є вирішальною.
Дедуплікатор

1

З часів оригінального C89, коли стандарт C визначав, що щось не визначає поведінку, це означало: "Робіть все, що зробить реалізацію на певній цільовій платформі найбільш підходящою за призначенням". Автори Стандарту не хотіли намагатися відгадати, яка поведінка може бути найбільш підходящою для певної мети. Існуючі реалізації C89 з розширеннями VLA могли мати різну, але логічну поведінку, коли задавали розмір нуля (наприклад, деякі могли б трактувати масив як вираз адреси, що дає NULL, а інші трактують його як вираз адреси, який може дорівнювати адресі інша довільна змінна, але може безпечно до неї додати нуль без пастки). Якщо якийсь код може покладатися на таку різну поведінку, автори Стандарту не хочуть "

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.