Как работает связывание в C в отношении непрозрачных указателей? - PullRequest
2 голосов
/ 12 апреля 2020

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

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

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;. Как почему? Я чувствую, что мне не хватает какой-то важной информации. Каковы правила относительно того, что может или не может go из одного. c файла в другой?

ADDENDUM

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

1 Ответ

4 голосов
/ 12 апреля 2020

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

Проблемы со вторым 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. или если вы присваиваете неверный тип переменной указателя.

...