C/C++ структура неполная несогласованность типа элемента?
Допустим, мы определяем две структуры в C или C++ (я получаю одинаковый результат как в C, так и в C++, и я думаю, что правила одинаковы, скажите, если они не являются).
один с элементом значения неполного типа:
struct abc{
int data;
struct nonexsitnow next; //error: field ‘next has incomplete type, makes sense since "struct nonexsitnow" hasn't been declared or defined.
};
и один с указательным элементом неполного типа:
struct abc{
int data;
struct nonexsitnow *next; //pass?!
};
Почему второе определение не вызывает никаких проблем? Он использует struct nonexsitnow
который не был создан!
ОБНОВИТЬ:
Я завершаю этот лист из ответов и комментариев ниже, надеясь, что они будут правы и полезны для разработки.
Как отметил @ArneVogel, обе struct Foo p;
и struct Foo *p;
является неявным объявлением Foo
и struct Foo;
явно делает эту работу (спасибо @Джон Боллинджер). Это почти ничего не значит для c, но имеет значение для c++ и поведения:
In a situation where struct Foo is:
_______________________________________________________
| | undeclared | declared but | defined |
| | | not defined | |
-------------------------------------------------------
| | C | C++ | C | C++ | C | C++ |
-------------------------------------------------------
|struct Foo p; | × | × | × | × | √ | √ |
|struct Foo* p; | √ | √ | √ | √ | √ | √ |
| Foo p; | × | × | × | × | × | √ |
| Foo* p; | × | × | × | √ | × | √ |
Ответы
Ответ 1
Во-первых, вам нужно понять, что значит для типа быть "неполным". C определяет его следующим образом:
В разных точках внутри единицы перевода тип объекта может быть неполным (не имеющим достаточной информации для определения размера объектов этого типа) или полным (имеющим достаточную информацию).
(C2011, 6.2.5/1)
Заметим, что полнота типа является функцией объема и видимости деклараций, а не характерной для типов. Тип может быть неполным в одной точке единицы перевода и заканчиваться в другой точке.
Тем не мение,
Тип указателя может быть получен из типа функции или типа объекта, называемого ссылочным типом. [...] Тип указателя - это полный тип объекта.
(C2011, 6.2.5/20, акцент добавлен)
Без квалификации все типы указателей являются полными типами, даже указатели, ссылочные типы которых сами по себе не являются завершенными. Как конкретная реализация делает эту работу не рассмотрен стандартом, но, как правило, все типы указателей на структуру имеют одинаковый размер и представление (что не имеет ничего общего с представлением их ссылочных типов).
Это оказывается важным, потому что тип структуры является неполным до закрывающей скобки его определения, поэтому, если указатели на неполные типы не были полностью заполнены, структура не могла содержать указатель на другую структуру своего типа, такую как обычно используется для реализации связанных списков, деревьев и других структур данных.
С другой стороны,
Структура или тип объединения неизвестного контента [...] является неполным типом. Он заполняется для всех объявлений такого типа путем объявления той же структуры или тега union с его определяющим контентом позже в той же области.
(C2011, 6.2.5/22)
Это оправдано, поскольку компилятор не может знать, насколько большой тип структуры, если он не знает, каковы его члены. Далее, имеет смысл, что
Структура или объединение не должны содержать элемент с неполным или функциональным типом (следовательно, структура не должна содержать экземпляр самого себя, но может содержать указатель на экземпляр самого себя), за исключением того, что последний элемент структуры с более чем один именованный элемент может иметь неполный тип массива [...].
(C2011, 6.7.2.1/3, добавлено выделение)
Исключение описывает функцию C, называемую "гибким элементом массива", которая содержит несколько предостережений и ограничений. Это тангенциальный вопрос, который вы можете прочитать (или спросить) отдельно.
Кроме того, все вышеизложенное согласуется с тем фактом, что C и C++ позволяют вам ссылаться на тип структуры по ее тегу перед объявлением его членов; то есть, когда он является неполным. Это может быть сделано само по себе как форвардная декларация...
struct foo;
... но это не относится ни к каким документарным целям, потому что непротиворечивое декларирование типов структуры не требуется. Вы можете снова подумать об использовании связанных списков, но эта характеристика никоим образом не ограничивается такими контекстами.
Действительно, относительно распространенным вариантом использования является непрозрачные типы. В таком случае библиотека создает и потребляет тип данных, реализация которых он не хочет раскрывать по какой-либо из нескольких причин. Тем не менее он может передать правильно набранные указатели на экземпляры таких структур на клиентский код и ожидать получения таких указателей. Если он никогда не предоставляет определение ссылочного типа, тогда клиентский код должен обрабатывать ссылочные объекты как непрозрачные капли.
Ответ 2
Почему второе определение структуры не вызывает никаких проблем?
Потому что это указатель. Размер указателя известен компилятору, даже если тип указателя указывает на неполное.
Ответ 3
Компилятору требуется размер структуры/класса, чтобы знать, сколько памяти нужно выделить при создании такого типа.
На данной платформе sizeof(T*)
всегда будет возвращать одно и то же значение для любого типа T, являющегося структурой или типом класса.
Вот почему вы можете использовать указатели для объявленных вперед типов без ошибок. Конечно, чтобы получить доступ к контенту объекта, на который указывает такой указатель или разыменовать его, должно быть предоставлено определение. Однако вы можете присвоить значение этому указателю (если это разрешено с точки зрения совместимости типов).
Важный факт:
В C, где вы обычно используете так называемый "C-стиль", назначение указателя обычно выполняется независимо от типов (вы несете ответственность за правильное поведение и выполнение требований к выравниванию).
Однако в C++ возможен ли выбор между неполными типами, зависит от типа трансляции. Рассмотрим два полиморфных типа:
class A; // forward declaration only
class B; // forward declaration only, actually inherits from A
A* aptr;
B* bptr;
bptr = (B*)(aptr); // ok
bptr = dynamic_cast<B*>(aptr); // error
dynamic_cast<>
не удастся, если компилятор не имеет доступа к определению типов, участвующих в трансляции (что необходимо для выполнения проверки выполнения). Пример: Идеал.
Ответ 4
Короткий ответ заключается в том, что так говорят c и c++.
Другие ответили "потому что компилятор знает, как велики указатели". Но это действительно не отвечает на вопрос. Существуют языки, которые допускают неполные типы внутри других типов, и они работают нормально.
Если какое-либо из этих допущений будет изменено, C/C++ будет поддерживать неполные структуры внутри структур:
-
C/C++ фактически сохраняет значения. Во многих языках, когда задан составной тип данных (класс или структура) внутри другого, храните ссылку или указатель вместо фактических значений этого составного типа данных
-
C/C++ хочет знать, насколько велики полные типы. Он хочет иметь возможность создавать массивы, вычислять их размер, вычислять смещение между элементами.
-
C/C++ хочет выполнить однопроходную компиляцию. Если компилятор был готов отметить, что там был неполный тип, продолжайте компиляцию до тех пор, пока не выясните, насколько велика она, затем вернитесь и вставьте размер типа в сгенерированный код, неполные типы будут в порядке.
-
C/C++ хочет, чтобы типы были полными после их определения. Вы можете легко вставить правило, в котором указано, что abc
был завершен только после того, как определение nonexistnow
было видимым. Вместо этого C++ хочет, чтобы abc
был завершен сразу после закрытия }
, возможно, для упрощения.
Наконец, указатели на структуры удовлетворяют всем этим требованиям, потому что стандарт C++ требует их выполнения. Который кажется полицейским, но это правда:
На некоторых платформах размер указателя зависит от особенностей объекта, на который указывает (в частности, указатели на однобайтные символы на платформах, где основные указатели адресуют четыре слова больше). C/C++ разрешает это, но требует, чтобы void*
был достаточно большим для самого большого указателя, и что указатели на структуру имеют фиксированный размер. Это вредит таким платформам, но они готовы сделать это, чтобы позволить указателям на неполные структуры внутри полных структур.
Вполне возможно, что компилятор будет скорее чем struct small { char c; }
struct small { char c; }
должно быть 1 байт в размере, и, следовательно, указатели на него будут "широкими"; но поскольку все указатели-на-структуру должны иметь одинаковый размер, и они не хотят использовать широкие указатели для каждой структуры, вместо этого у нас есть sizeof(small) == 4
на таких системах.
Это не закон вычисления, что все указатели имеют одинаковый размер, и это не закон, который структурам нужно знать, насколько они велики. Это оба закона c++ и c, которые выбраны по причинам.
Но как только у вас есть эти причины, вы вынуждены заключить, что члены struct
должны иметь известный размер (и выравнивание), а неполные структуры - нет. Между тем, указатели на неполные структуры. Таким образом, один разрешен, а другой нет.