Как законно ссылаться на неопределенный тип внутри структуры?
отвечая на другой вопрос, я наткнулся на такой фрагмент кода, который GCC компилирует без жалоб.
typedef struct {
struct xyz *z;
} xyz;
int main (void) {
return 0;
}
это средства, которые я всегда использовал для создания типов, которые указывают на себя (например, связанные списки), но я всегда думал, что вам нужно имя структура, чтобы вы могли использовать self-reference. Другими словами, вы не могли использовать xyz *z
внутри структуры, потому что typedef еще не завершен в этот момент.
но этот конкретный образец делает не назовите структуру, и она все еще компилируется. Я думал, что изначально в компиляторе была какая-то черная магия, которая автоматически переводила приведенный выше код, потому что структура и имена typedef были одинаковыми.
но эта маленькая красота работает также:
typedef struct {
struct NOTHING_LIKE_xyz *z;
} xyz;
что я пропустила? Это кажется явным нарушением, так как нет struct NOTHING_LIKE_xyz
тип определяется в любом месте.
когда я меняю его с указателя для фактического типа я получаю ожидаемую ошибку:
typedef struct {
struct NOTHING_LIKE_xyz z;
} xyz;
qqq.c:2: error: field `z' has incomplete type
кроме того, когда я удаляю struct
, я получаю сообщение об ошибке (parse error before "NOTHING ...
).
это разрешено в ISO C?
Обновление: A struct NOSUCHTYPE *variable;
компилирует, так что это не просто внутри структуры, где это кажется действительным. Я не могу найти ничего в стандарте c99, который допускает эту снисходительность к указателям структуры.
7 ответов
в части стандарта C99 вы после 6.7.2.3, пункт 7:
если спецификатор типа формы
struct-or-union identifier
возникает кроме как в рамках одного из вышеперечисленных формы, и никакой другой декларации идентификатор как тег виден, затем он объявляет неполную структуру или тип объединения и объявляет идентификатор как тег этого типа.
...и пункт 6.2.5 22:
тип структуры или объединения неизвестный содержание (как описано в 6.7.2.3) является неполный тип. Он завершен, для всех объявлений этого типа, объявив ту же структуру или объединение тег c его определяющим содержимым позже в тот же размах.
как говорится в предупреждении во втором случае,struct NOTHING_LIKE_xyz
Это неполного типа, как void
или массивов неизвестного размера. Неполный тип может отображаться только как тип, на который указывают, за исключением массивов неизвестного размера, которые разрешены как последний член структуры, что делает саму структуру неполным типом в этом случае. Следующий код не может разыменовать указатель на неполный тип (по уважительной причине).
неполные типы могут предложить некоторые инкапсуляция данных в с... Соответствующий абзац в http://www.ibm.com/developerworks/library/pa-ctypes1/ кажется хорошим объяснением.
1-й и 2-й случаи четко определены, потому что размер и выравнивание указателя известен. Компилятору C нужна только информация о размере и выравнивании для определения структуры.
третий случай недопустим, потому что размер этой фактической структуры неизвестен.
но будьте осторожны, чтобы 1-й случай был логичным, вам нужно дать имя структуре:
// vvv
typedef struct xyz {
struct xyz *z;
} xyz;
в противном случае внешняя структура и *z
будут рассмотрены два различных структуры.
2-й случай имеет популярный случай использования, известный как "непрозрачный указатель" (pimpl). Например, вы можете определить структуру-оболочку как
typedef struct {
struct X_impl* impl;
} X;
// usually just: typedef struct X_impl* X;
int baz(X x);
в заголовке, а затем в одном из .c
,
#include "header.h"
struct X_impl {
int foo;
int bar[123];
...
};
int baz(X x) {
return x.impl->foo;
}
преимущество этого .c
, вы не можете возиться с внутренностями объекта. Это своего рода заключение.
вы должны назвать его. В этом:
typedef struct {
struct xyz *z;
} xyz;
не сможет указать на себя как z
относится к некоторому полному другому типу, а не к безымянной структуре, которую вы только что определили. Попробуйте это:
int main()
{
xyz me1;
xyz me2;
me1.z = &me2; // this will not compile
}
вы получите сообщение об ошибке о несовместимых типах.
хорошо... Все что я могу сказать, что ваше предыдущее предположение было неверным. Каждый раз, когда вы используете struct X
construct (сам по себе или как часть большего объявления), он интерпретируется как объявление типа struct с тегом struct X
. Это может быть повторное объявление ранее объявленного типа структуры. Или, это может быть самое первое объявление новая тип struct. Новый тег объявляется в области, в которой он появляется. В вашем конкретном примере это файл область (поскольку язык C не имеет "области классов", как это было бы в C++).
более интересным примером такого поведения является объявление в прототипе функции:
void foo(struct X *p); // assuming `struct X` has not been declared before
в этом случае struct X
декларация
мне это тоже интересно. Оказывается, что struct NOTHING_LIKE_xyz * z
вперед объявлении struct NOTHING_LIKE_xyz
. В качестве запутанного примера,
typedef struct {
struct foo * bar;
int j;
} foo;
struct foo {
int i;
};
void foobar(foo * f)
{
f->bar->i;
f->bar->j;
}
здесь f->bar
относится к типу struct foo
, а не typedef struct { ... } foo
. Первая строка будет компилироваться нормально, но вторая даст ошибку. Тогда не так много пользы для реализации связанного списка.
когда объявлена переменная или поле типа структуры, компилятор должен выделить достаточно байтов для хранения этой структуры. Поскольку для структуры может потребоваться один байт или тысячи, компилятор не может знать, сколько места ему нужно выделить. Некоторые языки используют многопроходные компиляторы, которые смогут узнать размер структуры за один проход и выделить для нее место на более позднем проходе; так как C был разработан, чтобы обеспечить компиляцию за один проход, однако это невозможно. Таким образом, C запрещает объявление переменных или полей неполных типов структуры.
с другой стороны, когда объявлена переменная или поле типа указатель на структуру, компилятор должен выделить достаточно байтов для хранения указателя на структуру. независимо от того, занимает ли структура один байт или миллион, указатель всегда будет требовать одинакового объема пространства. эффективно, компилятор может наступить указатель на неполный тип как void*, пока он не получит больше информации о своем типе, а затем обработает его как указатель на соответствующий тип, как только он узнает больше о нем. Указатель неполного типа не совсем аналогичен void*, поскольку с void* можно делать то, что нельзя делать с неполными типами (например, если p1-указатель на структуру s1, а p2-указатель на структуру s2, нельзя назначить p1 p2), но нельзя ничего делать с указателем на неполный тип, что нельзя сделать с void*. В основном, с точки зрения компилятора, указатель на неполный тип представляет собой blob размером с указатель байтов. Он может быть скопирован в или из других подобных указателей размером с капли байтов, но это все. компилятор может генерировать код для этого, не зная, что еще будет делать с пузырьками размером с указатель байтов.