Що станеться, якщо я визначу масив розміром 0 у C / C ++?


127

Цікаво, що насправді відбувається, якщо я визначу масив нульової довжини int array[0];в коді? GCC взагалі не скаржиться.

Зразок програми

#include <stdio.h>

int main() {
    int arr[0];
    return 0;
}

Уточнення

Я насправді намагаюся з’ясувати, чи ініціалізовані таким чином масиви нульової довжини, замість того, щоб вказувати, як змінна довжина в коментарях Дархазера, оптимізована чи ні.

Це тому, що я повинен випустити якийсь код у дикій природі, тому я намагаюся розібратися, чи потрібно мені обробляти випадки, коли значення SIZEвизначено як 0, що відбувається в якомусь коді зі статично визначенимint array[SIZE];

Я був фактично здивований, що GCC не скаржиться, що призвело до мого питання. З отриманих відповідей, я вважаю, що відсутність попередження багато в чому пояснюється підтримкою старого коду, який не оновлювався новим синтаксисом [].

Оскільки я в основному задався питанням про помилку, я відзначаю відповідь Лундіна як правильну (перший був у Наваза, але він був не настільки повний) - інші вказували на його фактичне використання для конструкцій, накладених на хвости, хоча це актуально, чи не так ' t саме те, що я шукав.


51
@AlexanderCorwin: На жаль, у C ++, із невизначеною поведінкою, нестандартними розширеннями та іншими аномаліями, випробувати щось із себе часто - це не шлях до знань.
Бенджамін Ліндлі

5
@JustinKirk Я просто потрапив у пастку, проте тестуючи і пошивши, це працювало. І завдяки критиці, яку я отримав у своєму дописі, я дізнався, що тестування та перевірка її роботи не означає, що вона є дійсною та законною. Тому самовипробування іноді не вірно.
StormByte

2
@JustinKirk, див . Відповідь Матьє на прикладі того, де ти би його використовував. Він також може стати в нагоді в шаблоні, де розмір масиву є параметром шаблону. Приклад у питанні, очевидно, поза контекстом.
Марк Викуп

2
@JustinKirk: Яка мета []в Python чи навіть ""у C? Іноді у вас є функція або макрос, який вимагає масиву, але у вас немає даних, щоб їх вкласти.
dan04

15
Що таке "C / C ++"? Це дві окремі мови
гонки легкості на Орбіті

Відповіді:


86

Масив не може мати нульовий розмір.

ISO 9899: 2011 6.7.6.2:

Якщо вираз є постійним виразом, він повинен мати значення, що перевищує нуль.

Наведений вище текст справедливий як для простого масиву (параграф 1). Для VLA (масив змінної довжини) поведінка не визначена, якщо значення виразу менше або дорівнює нулю (параграф 5). Це нормативний текст у стандарті С. Компілятору заборонено реалізувати його по-іншому.

gcc -std=c99 -pedantic подає попередження для випадку, що не стосується VLA.


34
"воно фактично повинно давати помилку" - відмінність між "попередженнями" та "помилками" не визнається у стандарті (він зазначає лише "діагностику"), і єдина ситуація, коли компіляція повинна припинятись (тобто різниця в реальному світі між попередженням та помилкою] зустрічається з #errorдирективою.
Випадково832

12
FYI, як правило, у стандартах (C або C ++) вказано лише те, що повинні дозволяти компілятори , але не те, що вони повинні заборонити . У деяких випадках вони заявляють, що компілятор повинен видати "діагностику", але це приблизно настільки конкретно, наскільки вони потрапляють. Решта залишається постачальнику компілятора. EDIT: Що також сказав Random832.
mcmcc

8
@Lundin "Компілятору заборонено створювати двійковий файл, що містить масиви нульової довжини." Стандарт не говорить абсолютно нічого подібного. Це говорить лише про те, що він повинен генерувати щонайменше одне діагностичне повідомлення при наданні вихідного коду, що містить масив з постійним виразом нульової довжини для його розміру. Єдина обставина, за якої стандарт забороняє компілятору будувати бінарний файл, якщо він стикається з #errorдирективою препроцесора.
Випадково832

5
@Lundin Створення двійкового файлу для всіх правильних випадків задовольняє №1, і генерування або не генерування одного для неправильних випадків не вплине на нього. Друку попередження достатньо для №3. Така поведінка не має відношення до №2, оскільки стандарт не визначає поведінку цього вихідного коду.
Випадково832

13
@Lundin: Справа в тому, що ваше твердження помиляється; відповідні компілятори мають право будувати бінарний файл , який містить нульової довжини масиви, до тих пір , в якості діагностичного видається.
Кіт Томпсон

85

Відповідно до стандарту, це не дозволяється.

Однак у компіляторах C існує така практика, що трактувати ці декларації як декларацію гнучкого члена масиву ( FAM ) :

C99 6.7.2.1, §16 : Окремий випадок останній елемент структури з більш ніж одним названим членом може мати неповний тип масиву; це називається гнучким членом масиву.

Стандартний синтаксис FAM:

struct Array {
  size_t size;
  int content[];
};

Ідея полягає в тому, щоб ви потім розподілили його так:

void foo(size_t x) {
  Array* array = malloc(sizeof(size_t) + x * sizeof(int));

  array->size = x;
  for (size_t i = 0; i != x; ++i) {
    array->content[i] = 0;
  }
}

Ви також можете використовувати його статично (розширення gcc):

Array a = { 3, { 1, 2, 3 } };

Це також відоме як підкладки з хвостами (цей термін передує публікації стандарту C99) або структура злому (спасибі Джо Врешнігу за те, що він це вказав).

Однак цей синтаксис був стандартизований (і ефекти гарантовані) лише останнім часом у С99. Перш ніж був необхідний постійний розмір.

  • 1 Це був портативний шлях, хоча це було досить дивно.
  • 0 було краще вказувати намір, але не є законним, що стосується Стандарту, і підтримується як розширення деякими компіляторами (включаючи gcc).

Однак практика прокладки хвоста покладається на той факт, що зберігання доступне (обережне malloc), тому воно не підходить для використання стека в цілому.


@Lundin: Я тут не бачив жодної VLA, всі розміри відомі під час компіляції. Термін гнучких масивів походить від gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Zero-Length.html, і int content[];, наскільки я розумію, тут класифікується . Оскільки я не надто кмітливий у C-положенні мистецтва ... Ви могли б підтвердити, чи мої міркування здаються правильними?
Маттьє М.

@MatthieuM .: C99 6.7.2.1, §16: Окремий випадок останній елемент структури з більш ніж одним названим членом може мати неповний тип масиву; це називається гнучким членом масиву.
Крістоф

Ця ідіома також відома під назвою "struct hack" , і я зустрів більше людей, знайомих з цим ім'ям, ніж "структура з хвостиком" (ніколи раніше її не чула, крім, можливо, як загальної посилання на прокладку структури для майбутньої сумісності з ABI ) або "гнучкий член масиву", який я вперше почув у C99.

1
Використання масиву розміром 1 для взлома структури дозволить уникнути наявності компіляторів squawk, але було лише "портативним", оскільки автори компіляторів були досить приємні, щоб визнати таке використання як стандарт фактичного. Якби не заборона масивів нульового розміру, послідовне використання програмістами однорядних масивів як замінника крихти та історичне ставлення авторів-компіляторів до того, що вони повинні задовольняти потреби програміста, навіть коли цього не вимагає Стандарт, письменники-компілятори могли б легко і з користю оптимізовані , foo[x]щоб foo[0]кожен раз , коли fooбув масив з одним елементом.
supercat

1
@RobertSsupportsMonicaCellio: Це як явно показано у відповіді, але в кінці . Я також навантажував пояснення, щоб зробити це зрозумілішим.
Матьє М.

58

У стандартному C і C ++, масив нульового розміру НЕ допускається ..

Якщо ви використовуєте GCC, компілюйте його з -pedanticопцією. Це набере попередження , сказавши:

zero.c:3:6: warning: ISO C forbids zero-size array 'a' [-pedantic]

У випадку C ++ воно дає подібне попередження.


9
У Visual C ++ 2010:error C2466: cannot allocate an array of constant size 0
Марк Викуп

4
-Werror просто перетворює всі попередження в помилки, що не виправляє неправильну поведінку компілятора GCC.
Лундін

C ++ Builder 2009 також правильно дає помилку:[BCC32 Error] test.c(3): E2021 Array must have at least one element
Лундін

1
Замість цього -pedantic -Werrorви могли також просто зробити-pedantic-errors
Stephan Dollberg

3
Масив нульового розміру - не зовсім те саме, що розміром з нулем std::array. (Убік: я пригадую, але не можу знайти джерело про те, що VLA були розглянуті та явно відхилені від знаходження в C ++.)

27

Це абсолютно незаконно і завжди було, але багато компіляторів нехтують сигналом про помилку. Я не впевнений, чому ти хочеш це робити. Я знаю одне використання - це викликати помилку часу компіляції з булева:

char someCondition[ condition ];

Якщо conditionце помилка, то я отримую помилку часу компіляції. Оскільки компілятори це дозволяють, однак я взяв до використання:

char someCondition[ 2 * condition - 1 ];

Це дає розмір 1 або -1, і я ніколи не знайшов компілятора, який би прийняв розмір -1.


Це цікавий хак, щоб використовувати його.
Алекс Коай

10
Думаю, це звичайна хитрість у метапрограмуванні. Я не був би здивований, якби реалізація STATIC_ASSERTвикористовувала його.
Джеймс Канзе

Чому б не просто:#if condition \n #error whatever \n #endif
Jerfov2

1
@ Jerfov2, оскільки умова може бути невідома під час попередньої обробки, лише час компіляції
rmeador

9

Додам, що на цьому аргументі є ціла сторінка онлайн-документації gcc.

Деякі цитати:

Масиви нульової довжини дозволені в GNU C.

У ISO C90 вам слід надати вміст довжиною 1

і

Версії GCC до 3.0 дозволяли статично ініціалізувати масиви нульової довжини, як якщо б вони були гнучкими масивами. Окрім тих випадків, які були корисними, він також дозволяв ініціалізувати в ситуаціях, які могли б пошкодити пізніші дані

щоб ви могли

int arr[0] = { 1 };

і бум :-)


Чи можу я подобатися int a[0], тоді a[0] = 1 a[1] = 2??
Сурай Джайн

2
@SurajJain Якщо ви хочете перезаписати свій стек :-) C не перевіряє індекс та розмір масиву, який ви пишете, тож ви можете, a[100000] = 5але якщо пощастить, ви просто зламаєте додаток, якщо вам пощастить: -)
xanatos

Int a [0]; означає змінний масив (масив нульового розміру), як я можу зараз його призначити
Suraj Jain

@SurajJain Яка частина "C не перевіряє індекс та розмір масиву, який ви пишете" не зрозуміла? Немає перевірки індексу на C, ви можете записати після закінчення масиву і вийти з ладу на комп'ютері або перезаписати дорогоцінні біти пам'яті. Отже, якщо у вас є масив з 0 елементів, ви можете писати після закінчення 0 елементів.
xanatos

Дивіться цей quora.com/…
Сурай Джайн

9

Ще одне використання масивів нульової довжини призначене для створення об'єкта змінної довжини (до-C99). Нульова довжина масиви є різними з гнучких масивів , які мають [] без 0.

Цитується з gcc doc :

Масиви нульової довжини дозволені в GNU C. Вони дуже корисні як останній елемент структури, який справді є заголовком для об'єкта змінної довжини:

 struct line {
   int length;
   char contents[0];
 };
 
 struct line *thisline = (struct line *)
   malloc (sizeof (struct line) + this_length);
 thisline->length = this_length;

В ISO C99 ви б використовували гнучкий елемент масиву, який дещо відрізняється за синтаксисом та семантикою:

  • Члени гнучкого масиву записуються як вміст [] без 0.
  • Члени гнучкого масиву мають неповний тип, тому оператор sizeof може не застосовуватися.

Прикладом реального світу є масиви нульової довжини struct kdbus_itemв kdbus.h (модуль ядра Linux).


2
IMHO, Стандарт не мав вагомих причин заборонити масиви нульової довжини; він міг би мати об'єкти нульового розміру просто чудові як члени структури і розглядав їх як void*цілі арифметики (тому додавання або віднімання покажчиків на об'єкти нульового розміру було б заборонено). Хоча учасники гнучких масивів переважно кращі, ніж масиви нульового розміру, вони також можуть діяти як своєрідний "союз" для псевдонімів, не додаючи додаткового рівня "синтаксичної" непрямості до того, що випливає (наприклад, дається struct foo {unsigned char as_bytes[0]; int x,y; float z;}можливість отримати доступ до членів x. z...
supercat

... безпосередньо, не вимовляючи, наприклад myStruct.asFoo.x, тощо. Далі, IIRC, C намагається включити гнучкий член масиву в структуру, що робить неможливим структуру, яка включає в себе безліч інших членів гнучких масивів відомої довжини зміст.
supercat

@supercat вагомою причиною є збереження цілісності правила щодо доступу до зовнішніх меж масиву. Як останній член структури, гнучкий масив С99 досягає точно такого ж ефекту, як масив нульового розміру GCC, але без необхідності додавати спеціальні випадки до інших правил. IMHO - це вдосконалення, яке sizeof x->contentsє помилкою в ISO C на відміну від повернення 0 в gcc. Масиви нульового розміру, які не є членами структури, створюють купу інших проблем.
М.М.

@MM: Які проблеми вони могли б викликати, якби відняття двох рівних покажчиків на об'єкт нульового розміру було визначено як отримання нуля (як віднімання рівних покажчиків на будь-який розмір об'єкта), а віднімання нерівних покажчиків на об'єкти нульового розміру визначали як вихідні Невказане значення? Якщо Стандарт зазначив, що реалізація може дозволити структурі, що містить FAM, вбудовуватися в іншу структуру за умови, що наступний елемент в останній структурі є або масивом з тим же типом елементів, що і FAM, або структурою, що починається з такого масиву , і за умови, що ...
supercat

... він визнає FAM як псевдонім масиву (якщо правила вирівнювання призведуть до того, що масиви приземляться з різними зрушеннями, потрібна була б діагностика), це було б дуже корисно. Наразі не існує хорошого способу існувати метод, який приймає покажчики на структури загального формату struct {int n; THING dat[];}і може працювати з речами статичної або автоматичної тривалості.
supercat

6

Оголошення масиву нульового розміру в структурах були б корисні, якби вони були дозволені, і якби семантика була такою, що (1) вони примушували б вирівнювати, але в іншому випадку не виділяти жодного простору, і (2) індексація масиву вважатиметься визначеною поведінкою в випадок, коли отриманий вказівник знаходитиметься в тому ж блоці пам'яті, що і структура. Така поведінка ніколи не була дозволена жодним стандартом C, але деякі старі компілятори дозволяли це, перш ніж стало стандартним для компіляторів, допускати неповні декларації масиву з порожніми дужками.

Злом структури, як це зазвичай реалізується за допомогою масиву розміром 1, є хитрим, і я не думаю, що є жодна вимога, що компілятори утримуються від його порушення. Наприклад, я б очікувати , що якщо компілятор бачить int a[1], що це буде в межах своїх прав у відношенні a[i]як a[0]. Якщо хтось намагається вирішити проблеми з вирівнюванням зламаної структури через щось подібне

typedef structure {
  uint32_t розмір;
  дані uint8_t [4]; // Використовуйте чотири, щоб уникнути скидання накладки розміром структури
}

компілятор може стати розумним і припустити, що розмір масиву дійсно дорівнює чотирма:

; Як написано
  foo = myStruct-> дані [i];
; Як інтерпретується (якщо припустити, малоінтенсивне обладнання)
  foo = ((* (uint32_t *) myStruct-> дані) >> (i << 3)) & 0xFF;

Така оптимізація може бути розумною, особливо якщо вона myStruct->dataмогла бути завантажена в реєстр в тій же операції, що і myStruct->size. Я нічого не знаю в стандарті, який забороняв би таку оптимізацію, хоча, звичайно, він би порушив будь-який код, який, можливо, очікує доступу до матеріалів за межами четвертого елемента.


1
Гнучкий елемент масиву був доданий в C99 в якості законного варіанту структури злому
MM

Стандарт говорить, що доступ до різних членів масиву не конфліктує, що, як правило, робить цю оптимізацію неможливою.
Ben Voigt

@BenVoigt: Стандарт мови мови не визначає ефект написання байту та читання містять слова одночасно, але 99,9% процесорів вказують, що запис буде успішним і слово буде містити або нову, або стару версію байт разом із незмінним вмістом інших байтів. Якщо компілятор націлює на таких процесорів, то який би був конфлікт?
supercat

@supercat: Стандарт мови C гарантує, що одночасне записування до двох різних елементів масиву не буде конфліктувати. Тож ваш аргумент, що (читайте, поки пишіть) працює нормально, недостатній.
Бен Войгт

@BenVoigt: Якщо фрагмент коду повинен був, наприклад, записати елементи масиву 0, 1 і 2 у певній послідовності, не було б дозволено прочитати всі чотири елементи в довгий, змінити три і записати назад усі чотири, але я думаю, було б дозволено прочитати всі чотири в довгі, змінити три, записати назад 16 біт як короткий, а біти 16-23 - як байт. Чи не погоджуєтесь ви з цим? І коду, який потрібен лише для зчитування елементів масиву, було б дозволено просто прочитати їх у довгий і використовувати це.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.