Массив нулевой длины
Я работаю над рефакторингом какого-то старого кода и нашел несколько структур, содержащих массивы нулевой длины (ниже). Предупреждения подавлены прагмой, конечно, но я не смог создать "новые" структуры, содержащие такие структуры (ошибка 2233). Массив 'byData' используется в качестве указателя, но почему бы не использовать вместо него указатель? или массив длиной 1? И, конечно, не было добавлено никаких комментариев, чтобы я получил удовольствие от процесса... Есть ли причины использовать такую вещь? Любой совет в рефакторинге тех?
struct someData
{
int nData;
BYTE byData[0];
}
NB Это C++, Windows XP, VS 2003
5 ответов
Да, это C-Hack.
Чтобы создать массив любой длины:
struct someData* mallocSomeData(int size)
{
struct someData* result = (struct someData*)malloc(sizeof(struct someData) + size * sizeof(BYTE));
if (result)
{ result->nData = size;
}
return result;
}
Теперь у вас есть объект someData с массивом указанной длины.
К сожалению, есть несколько причин, по которым вы бы объявили массив нулевой длины в конце структуры. По сути, это дает вам возможность иметь структуру переменной длины, возвращаемую из API.
Раймонд Чен сделал отличную запись в блоге на эту тему. Я предлагаю вам взглянуть на этот пост, потому что он, вероятно, содержит ответ, который вы хотите.
Заметьте, что в его посте речь идет о массивах размера 1 вместо 0. Это так, потому что массивы нулевой длины - более свежий вход в стандарты. Его пост все равно должен относиться к вашей проблеме.
http://blogs.msdn.com/oldnewthing/archive/2004/08/26/220873.aspx
РЕДАКТИРОВАТЬ
Примечание. Несмотря на то, что в сообщении Рэймонда говорится, что массивы нулевой длины допустимы в C99, они фактически недопустимы в C99. Вместо массива длины 0 здесь вы должны использовать массив длины 1
Это старый C-хак, позволяющий использовать гибкие размеры массивов.
В стандарте C99 это не обязательно, так как он поддерживает синтаксис arr[].
Ваша интуиция о том, "почему бы не использовать массив размером 1", очень важна.
Код неправильно выполняет "взлом структуры C", поскольку объявления массивов нулевой длины являются нарушением ограничения. Это означает, что компилятор может сразу отклонить ваш хак во время компиляции с помощью диагностического сообщения, которое останавливает перевод.
Если мы хотим совершить взлом, мы должны прокрасться мимо компилятора.
Правильный способ "взлома структуры C" (который совместим с диалектами C, начиная с ANSI C 1989 года и, возможно, намного раньше), заключается в использовании совершенно корректного массива размера 1:
struct someData
{
int nData;
unsigned char byData[1];
}
Более того, вместо sizeof struct someData
Размер части перед byData
рассчитывается с использованием:
offsetof(struct someData, byData);
Чтобы выделить struct someData
с пространством для 42 байтов в byData
мы бы тогда использовали:
struct someData *psd = (struct someData *) malloc(offsetof(struct someData, byData) + 42);
Обратите внимание, что это offsetof
вычисление фактически является правильным вычислением даже в случае нулевого размера массива. Ты видишь, sizeof
вся структура может включать отступы. Например, если у нас есть что-то вроде этого:
struct hack {
unsigned long ul;
char c;
char foo[0]; /* assuming our compiler accepts this nonsense */
};
Размер struct hack
вполне возможно дополнить для выравнивания из-за ul
член. Если unsigned long
имеет ширину четыре байта, тогда вполне возможно sizeof (struct hack)
8, тогда как offsetof(struct hack, foo)
почти наверняка 5. offsetof
Метод - это способ получить точный размер предыдущей части структуры непосредственно перед массивом.
Так что это был бы способ реорганизовать код: привести его в соответствие с классическим, очень переносимым взломом структуры.
Почему бы не использовать указатель? Потому что указатель занимает дополнительное место и должен быть инициализирован.
Есть и другие веские причины не использовать указатель, а именно то, что указатель требует адресного пространства, чтобы быть значимым. Хак с структурой доступен для внешнего применения: то есть существуют ситуации, в которых такая компоновка соответствует внешнему хранилищу, например областям файлов, пакетов или разделяемой памяти, в которых вам не нужны указатели, поскольку они не имеют смысла.
Несколько лет назад я использовал struct hack в интерфейсе передачи сообщений совместно используемой памяти между ядром и пользовательским пространством. Я не хотел указателей там, потому что они были бы значимы только для исходного адресного пространства процесса, генерирующего сообщение. Часть ядра программного обеспечения имела вид на память, используя свое собственное отображение по другому адресу, и поэтому все было основано на вычислениях смещения.
Стоит указать IMO лучший способ для расчета размера, который используется в статье Рэймонда Чена, связанной выше.
struct foo
{
size_t count;
int data[1];
}
size_t foo_size_from_count(size_t count)
{
return offsetof(foo, data[count]);
}
Смещение первой записи от конца желаемого выделения также является размером желаемого выделения. ИМО - это чрезвычайно элегантный способ расчета размера. Не имеет значения, какой тип элемента массива переменного размера. Смещение (или FIELD_OFFSET или UFIELD_OFFSET в Windows) всегда записывается одинаково. Нет выражений sizeof(), чтобы случайно испортить.