Как работает компоновка в C в отношении непрозрачных указателей?

Итак, у меня была небольшая путаница относительно связывания различных вещей. В этом вопросе я сосредоточусь на непрозрачных указателях.

Я проиллюстрирую свое замешательство на примере. Допустим, у меня есть три файла:

main.c

#include <stdio.h>
#include "obj.h"            //this directive is replaced with the code in obj.h

int main()
{
    myobj = make_obj();
    setid(myobj, 6);

    int i = getid(myobj);
    printf("ID: %i\n",i);

    getchar();
    return 0;
}

obj.c

#include <stdlib.h>

struct obj{
    int id;
};

struct obj *make_obj(void){
    return calloc(1, sizeof(struct obj));
};

void setid(struct obj *o, int i){
    o->id = i;
};

int getid(struct obj *o){
    return o->id;
};

obj.h

struct obj;

struct obj *make_obj(void);

void setid(struct obj *o, int i);

int getid(struct obj *o);

struct obj *myobj;

Из-за директив препроцессора они по сути превратились бы в два файла:

(технически я знаю, что в stdio.h и stdlib.h их код заменит директивы препроцессора, но я не стал заменять их для удобства чтения)

main.c

#include <stdio.h>

//obj.h
struct obj;
struct obj *make_obj(void);
void setid(struct obj *o, int i);
int getid(struct obj *o);
struct obj *myobj;

int main()
{
    myobj = make_obj();
    setid(myobj, 6);

    int i = getid(myobj);
    printf("ID: %i\n",i);

    getchar();
    return 0;
}

obj.c

#include <stdlib.h>

struct obj{
    int id;
};

struct obj *make_obj(void){
    return calloc(1, sizeof(struct obj));
};

void setid(struct obj *o, int i){
    o->id = i;
};

int getid(struct obj *o){
    return o->id;
};

Вот где я немного запутался. Если я попытаюсь создать struct obj в main.c, я получаю ошибку неполного типа, хотя main.c имеет объявление struct obj;.

Даже если я изменю код, чтобы использовать extern, он не будет компилироваться:

main.c

#include <stdio.h>

extern struct obj;

int main()
{
    struct obj myobj;
    myobj.id = 5;

    int i = myobj.id;
    printf("ID: %i\n",i);

    getchar();
    return 0;
}

obj.c

#include <stdlib.h>

struct obj{
    int id;
};

Насколько я могу судить, main.c и obj.c не сообщают структуры (в отличие от функций или переменных для некоторых, которым просто нужно объявление в другом файле).

Итак, main.c не имеет связи с типами struct obj, но по какой-то причине в предыдущем примере он смог создать указатель на один очень хорошо struct obj *myobj;. Как почему? Я чувствую, что упускаю какую-то важную информацию. Каковы правила относительно того, что можно, а что нельзя переходить из одного файла .c в другой?

ДОБАВЛЕНИЕ

Чтобы устранить возможное дублирование, я должен подчеркнуть, что я не спрашивая, что такое непрозрачный указатель, но как он работает в отношении связывания файлов.


person thepufferfish    schedule 12.04.2020    source источник
comment
Отвечает ли это на ваш вопрос? Что такое непрозрачный указатель в C?   -  person Adrian Mole    schedule 12.04.2020
comment
Обратите внимание, что obj.c должен включать obj.h для обеспечения согласованности между obj.c и main.c - даже если вы используете непрозрачные указатели.   -  person Jonathan Leffler    schedule 12.04.2020
comment
И main.c не имеет сведений о struct obj; он знает, что тип существует, но ничего не знает о том, что он содержит. Вы можете создавать и использовать указатели на struct obj; вы не можете разыменовать эти указатели, даже чтобы скопировать структуру, не говоря уже о доступе к данным внутри структуры, потому что неизвестно, насколько она велика. Вот почему у вас есть функции в obj.c - они предоставляют необходимые вам услуги. Распределение объектов, освобождение, доступ и модификация содержимого (за исключением того, что освобождение объекта отсутствует; возможно, free(obj); в порядке, но лучше предоставить «деструктор»).   -  person Jonathan Leffler    schedule 12.04.2020
comment
@JonathanLeffler Я не на 100% то, что вы имеете в виду под «обеспечением согласованности»; что это влечет за собой и почему это важно? Кроме того, я думал, что вся причина, по которой указатели зависят от типа, заключается в том, что указателям нужен размер, который может варьироваться в зависимости от типа. Как может быть указатель на что-то неизвестного размера?   -  person thepufferfish    schedule 12.04.2020
comment
На данный момент у вас может быть struct obj *make_obj(int initializer) { … } в obj.c, но поскольку вы не включаете obj.h в obj.c, компилятор не может сказать вам, что ваш код в main.c вызовет его без инициализатора, что приведет к квазислучайным (неопределенным) значениям используется для «инициализации» структуры. Если вы включите obj.h в obj.c, компилятор сообщит о несоответствии между объявлением в заголовке и определением в исходном файле, и код не будет компилироваться. Код в main.c тоже не компилируется - после исправления заголовка. [… продолжение…]   -  person Jonathan Leffler    schedule 12.04.2020
comment
[… продолжение…] Файлы заголовков - это «клей», который скрепляет систему, обеспечивая согласованность между определением функции и местами, которые используют функцию (ссылки). Объявление в заголовке гарантирует, что все они согласованы.   -  person Jonathan Leffler    schedule 12.04.2020
comment
Что касается того, почему у вас могут быть указатели на типы, не зная всех деталей, это важная особенность C, которая обеспечивает взаимодействие отдельно скомпилированных модулей. Все указатели на структуры (любого типа) должны иметь одинаковые требования к размеру и выравниванию. Вы можете указать, что тип структуры существует, просто сказав struct WhatEver; там, где это необходимо (обычно в области файла, а не внутри функции; существуют сложные правила для определения (или, возможно, переопределения) типов структур внутри функций). Затем вы можете использовать указатели на этот тип без дополнительной информации для компилятора.   -  person Jonathan Leffler    schedule 12.04.2020
comment
Без подробного тела структуры (struct WhatEver { … };, где фигурные скобки и содержимое между ними имеют решающее значение) вы не можете получить доступ к тому, что находится в структуре, или создать переменные типа struct WhatEver, но вы можете создавать указатели (struct WhatEver ptr = NULL;). Это важно для «безопасности типов» - избегайте void * как универсального типа указателя, когда это возможно, и обычно вы можете этого избежать. Не всегда, но обычно.   -  person Jonathan Leffler    schedule 12.04.2020
comment
@JonathanLeffler О, хорошо, obj.h в obj.c - это средство обеспечения соответствия используемого прототипа определению, вызывая сообщение об ошибке, если они этого не делают. Я до сих пор не совсем понимаю, что все указатели имеют одинаковый размер и выравнивание. Разве размер и выравнивание структуры не будут уникальными для этой конкретной структуры?   -  person thepufferfish    schedule 12.04.2020
comment
Структуры все разные, но указатели на них все одного размера.   -  person Jonathan Leffler    schedule 12.04.2020
comment
@JonathanLeffler И указатели могут быть одинакового размера, потому что указатели структур не могут быть разыменованы, поэтому им не нужны определенные размеры? Кроме того, из любопытства, почему не следует использовать void * в качестве универсального указателя?   -  person thepufferfish    schedule 12.04.2020
comment
Если компилятор знает детали структуры (есть определение типа структуры с присутствующей частью { … }), то указатель может быть разыменован (и могут быть определены переменные типа структуры, а также указатели на нее, конечно ). Если компилятор не знает подробностей, вы можете определять (и использовать) только указатели на тип. Вы избегаете void *, потому что теряете всю безопасность типов. Если у вас есть void *delicate_and_dangerous(void *vptr), вы можете вызывать delicate_and_dangerous(stdin); и delicate_and_dangerous(argv[1]);, и компилятор не может жаловаться. [… продолжение…]   -  person Jonathan Leffler    schedule 12.04.2020
comment
[… продолжение…] Если у вас есть struct SpecialCase *delicate_and_dangerous(struct UnusualDevice *udptr), то компилятор сообщит вам, когда вы вызовете его с неправильным типом указателя, например stdin (a FILE *) или argv[1] (char *, если вы в main()) и др.   -  person Jonathan Leffler    schedule 12.04.2020
comment
@RobertoCaboni - это должно было быть коротким и простым комментарием, но оно сохранилось. становится все длиннее, и дольше, и дольше. К тому же я почти уверен, что эта тема уже обсуждалась, и большая часть замечаний, высказанных мною, уже высказывалась раньше.   -  person Jonathan Leffler    schedule 12.04.2020
comment
@RobertoCaboni - Однако, несмотря на это, я преобразовал свой комментарий выше в ответ ниже.   -  person Jonathan Leffler    schedule 12.04.2020


Ответы (1)


Преобразование комментариев в полусогласованный ответ.

Проблемы со вторым 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, которая обеспечивает взаимодействие отдельно скомпилированных модулей. Все указатели на структуры (любого типа) должны иметь одинаковый размер и требования к выравниванию. Вы можете указать, что этот тип структуры существует, просто сказав struct WhatEver; там, где это необходимо. Обычно это в области видимости файла, а не внутри функции; существуют сложные правила для определения (или, возможно, переопределения) структурных типов внутри функций. Затем вы можете использовать указатели на этот тип без дополнительной информации для компилятора.

Без подробного тела структуры (struct WhatEver { … };, где фигурные скобки и содержимое между ними имеют решающее значение) вы не можете получить доступ к тому, что находится в структуре, или создать переменные типа struct WhatEver, но вы можете создавать указатели (struct WhatEver *ptr = NULL;). Это важно для "безопасности типов". По возможности избегайте void * как универсального типа указателя, и обычно вы можете этого избежать - не всегда, но обычно.

Хорошо, поэтому obj.h в obj.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()) и т. д., или если вы назначаете неверный тип переменной-указателя.

person Jonathan Leffler    schedule 12.04.2020