Как можно ссылаться на неопределенный тип внутри структуры? - PullRequest
8 голосов
/ 24 мая 2010

В ответ на другой вопрос я наткнулся на такой код, который gcc компилирует без жалоб.

typedef struct {
    struct xyz *z;
} xyz;
int main (void) {
    return 0;
}

Это средство, которое я всегда использовал для создания типов, которые указывают на себя (например, связанные списки), но я всегда думал, что вам нужно назвать структуру, чтобы вы могли использовать самоссылку , Другими словами, вы не можете использовать 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?


Обновление: struct NOSUCHTYPE *variable; также компилируется, так что это не просто внутри структур, где оно кажется допустимым. Я не могу найти ничего в стандарте c99, который позволял бы эту снисходительность для указателей структуры.

Ответы [ 7 ]

7 голосов
/ 24 мая 2010

Как сказано в предупреждении во втором случае, struct NOTHING_LIKE_xyz - это неполный тип , такой как void или массивы неизвестного размера. Неполный тип может появляться только как указанный тип, за исключением массивов неизвестного размера, которые разрешены в качестве последнего члена структуры, что делает саму структуру неполным типом в этом случае. Следующий код не может разыменовать любой указатель на неполный тип (по уважительной причине).

Неполные типы могут предлагать некоторую инкапсуляцию типов данных в C ... Соответствующий абзац в http://www.ibm.com/developerworks/library/pa-ctypes1/ кажется хорошим объяснением.

6 голосов
/ 24 мая 2010

Следующие части стандарта C99: 6.7.2.3, пункт 7:

Если указатель типа формы struct-or-union identifier происходит кроме как часть одного из вышеупомянутых формы, и никакой другой декларации идентификатор как тэг виден, то он объявляет неполную структуру или тип объединения и объявляет идентификатор в качестве тега этого типа.

... и пункт 6.2.5:

Структура или тип объединения неизвестен содержимое (как описано в 6.7.2.3) неполный тип. Завершено, для всех объявлений этого типа объявляя ту же структуру или союз тег с его определяющим содержанием позже в та же область.

2 голосов
/ 24 мая 2010

1-й и 2-й случаи четко определены, поскольку размер и выравнивание указателя известны. Компилятору C требуется только информация о размере и выравнивании для определения структуры.

3-й случай недействителен, потому что размер этой фактической структуры неизвестен.

Но учтите, что для того, чтобы первый случай был логичным, вам нужно дать имя структуре:

//             vvv
typedef struct xyz {
    struct xyz *z;
} xyz;

в противном случае внешняя структура и *z будут считаться двумя разными структурами.


Второй случай имеет популярный вариант использования, известный как "непрозрачный указатель" (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, вы не можете связываться с внутренностями объекта. Это своего рода инкапсуляция.

1 голос
/ 24 мая 2010

Ну ... все, что я могу сказать, это то, что ваше предыдущее предположение было неверным. Каждый раз, когда вы используете конструкцию struct X (отдельно или как часть более крупного объявления), она интерпретируется как объявление типа структуры с тегом структуры X. Это может быть повторное объявление ранее объявленного типа структуры. Или это может быть самое первое объявление типа new struct. Новый тег объявляется в области видимости, в которой он появляется. В вашем конкретном примере это область видимости файла (поскольку язык C не имеет «области видимости класса», как это было бы в C ++).

Более интересный пример такого поведения - когда объявление появляется в прототипе функции:

void foo(struct X *p); // assuming `struct X` has not been declared before

В этом случае новое объявление struct X имеет область действия функции-прототипа , которая заканчивается в конце прототипа. Если вы объявляете область действия файла struct X, позже

struct X;

и попытайтесь передать указатель типа struct X в вышеуказанную функцию, компилятор выдаст вам диагностику несоответствующего типа указателя

struct X *p = 0;
foo(p); // different pointer types for argument and parameter

Это также сразу означает, что в следующих декларациях

void foo(struct X *p);
void bar(struct X *p);
void baz(struct X *p);

каждое struct X объявление - это объявление другого типа , каждое локальное для своей области видимости прототипа функции.

Но если вы предварительно объявите struct X, как в

struct X;
void foo(struct X *p);
void bar(struct X *p);
void baz(struct X *p);

все struct X ссылки во всех прототипах функций будут ссылаться на того же ранее объявленного struct X типа.

1 голос
/ 24 мая 2010

Вы должны назвать это. В этом:

typedef struct {
    struct xyz *z;
} xyz;

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

int main()
{
    xyz me1;
    xyz me2;
    me1.z = &me2;   // this will not compile
}

Вы получите сообщение о несовместимых типах.

0 голосов
/ 21 ноября 2011

Когда объявляется переменная или поле типа структуры, компилятор должен выделить достаточно байтов для хранения этой структуры. Поскольку для структуры может потребоваться один байт или тысячи, компилятору не удастся узнать, сколько места ему нужно выделить. Некоторые языки используют многопроходные компиляторы, которые могли бы определить размер структуры за один проход и выделить место для нее на более позднем проходе; поскольку C был спроектирован так, чтобы обеспечить однопроходную компиляцию, это невозможно. Таким образом, C запрещает объявление переменных или полей с неполными типами структур.

С другой стороны, когда объявляется переменная или поле типа указатель на структуру, компилятор должен выделить достаточно байтов для хранения указателя на структуру. Независимо от того, занимает ли структура один байт или миллион, указателю всегда будет требоваться одинаковый объем пространства. По сути, компилятор может указывать указатель на неполный тип как void *, пока не получит больше информации о его типе, а затем рассматривать его как указатель на соответствующий тип, как только он узнает о нем больше. Указатель неполного типа не совсем аналогичен void *, так как можно делать вещи с void *, что нельзя делать с неполными типами (например, если p1 - указатель на struct s1, а p2 - указатель на struct s2, нельзя назначить p1 для p2), но нельзя сделать что-либо с указателем на неполный тип, что нельзя сделать для аннулирования *. По сути, с точки зрения компилятора указатель на неполный тип представляет собой двоичный объект размером с указатель. Он может быть скопирован в или из других подобных байтовых блоков размером с указатель, но это все. компилятор может сгенерировать код, чтобы сделать это, без необходимости знать, что еще нужно делать с каплями байтов размером с указатель.

0 голосов
/ 24 мая 2010

Мне тоже было интересно об этом. Оказывается, что 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. Первая строка будет компилироваться нормально, но вторая выдаст ошибку. Тогда для реализации связанного списка не так уж много пользы.

...