Преобразование комментариев в полусвязный ответ.
Проблемы со вторым main.c
возникают из-за отсутствия деталей struct obj
; он знает, что тип существует, но он ничего не знает о том, что он содержит. Вы можете создавать и использовать указатели на struct obj
; Вы не можете разыменовать эти указатели, даже не копировать структуру, не говоря уже о доступе к данным внутри структуры, потому что неизвестно, насколько она велика. Вот почему у вас есть функции в obj.c
. Они предоставляют необходимые вам услуги - размещение объектов, освобождение, доступ и изменение содержимого (за исключением того, что выпуск объекта отсутствует; возможно, free(obj);
в порядке, но лучше предоставить «деструктор»).
Обратите внимание, что obj.c
должен включать obj.h
для обеспечения согласованности между obj.c
и main.c
- даже если вы используете непрозрачные указатели.
Я не на 100% имею в виду то, что вы подразумеваете под «обеспечение согласованности»; что это влечет за собой и почему это важно?
В данный момент вы можете иметь struct obj *make_obj(int initializer) { … }
в obj.c
, но поскольку вы не включите obj.h
в obj.c
, компилятор не может сказать вам, что ваш код в main.c
будет вызывать его без инициализатора, что приведет к квазислучайным (неопределенным) значениям, используемым для «инициализации» структуры. Если вы включите obj.h
в obj.c
, компилятор сообщит о несоответствии между объявлением в заголовке и определением в исходном файле, и код не будет компилироваться. Код в main.c
также не будет компилироваться - после исправления заголовка. Заголовочные файлы представляют собой «клей», который удерживает систему вместе, обеспечивая согласованность между определением функции и местами, в которых используется функция (ссылки). Объявление в заголовке гарантирует, что они все согласованы.
Кроме того, я подумал, что вся причина, по которой указатели являются указателями типа c, заключается в том, что указатели нуждаются в размере, который может варьироваться в зависимости от типа. Как указатель может быть на что-то неизвестного размера?
Что касается того, почему вы можете иметь указатели на типы, не зная всех деталей, это важная особенность C, которая обеспечивает взаимодействие между отдельно скомпилированные модули. Все указатели на конструкции (любого типа) должны иметь одинаковые требования к размеру и выравниванию . Вы можете указать, что тип структуры существует, просто сказав struct WhatEver;
там, где это необходимо. Это обычно в области видимости файла, а не внутри функции; Существуют сложные правила для определения (или, возможно, переопределения) структурных типов внутри функций. И затем вы можете использовать указатели на этот тип без дополнительной информации для компилятора.
Без подробного тела структуры (struct WhatEver { … };
, где решающие скобки и содержимое между ними имеют решающее значение), вы не сможете получить доступ к что в структуре, или создавать переменные типа struct WhatEver
- но вы можете создавать указатели (struct WhatEver *ptr = NULL;
). Это важно для «безопасности типов». Избегайте void *
как универсального типа указателя, когда можете, и обычно вы можете избежать его - не всегда, но обычно.
Да ладно, так что obj.h
в obj.c
является средством обеспечения того, чтобы используемый прототип соответствовал определению, вызывая сообщение об ошибке, если они этого не делают.
Да.
Я все еще не совсем следую в терминах из всех указателей, имеющих одинаковый размер и выравнивание. Разве размер и выравнивание структуры не будут уникальными для этой конкретной структуры?
Все структуры разные, но указатели на них имеют одинаковый размер.
И указатели могут быть одинакового размера, потому что указатели на структуру не могут быть разыменованы, поэтому им не нужны определенные c размеры?
Если компилятор знает детали структуры ( есть определение типа структуры с присутствующей частью { … }
), тогда указатель может быть разыменован (и переменные типа структуры могут быть определены, а также указатели на него, конечно). Если компилятор не знает подробностей, вы можете только определить (и использовать) указатели на тип.
Кроме того, из любопытства, почему бы не использовать void *
как универсальный указатель?
Вы избегаете void *
, потому что теряете безопасность всех типов. Если у вас есть объявление:
extern void *delicate_and_dangerous(void *vptr);
, то компилятор не сможет пожаловаться, если вы напишите вызовы:
bool *bptr = delicate_and_dangerous(stdin);
struct AnyThing *aptr = delicate_and_dangerous(argv[1]);
Если у вас есть объявление:
extern struct SpecialCase *delicate_and_dangerous(struct UnusualDevice *udptr);
, тогда компилятор сообщит вам, когда вы вызываете его с неверным типом указателя, например stdin
(a FILE *
) или argv[1]
(char *
, если вы находитесь в main()
) и т. Д. c. или если вы присваиваете неверный тип переменной указателя.