Что значит иметь структуру без определения? - PullRequest
5 голосов
/ 07 октября 2019

Недавно я наткнулся на следующий код в моей системе stdio.h:

struct _IO_FILE_plus;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

Я привык видеть указатели на структуры, которые объявлены наперед так: extern struct _IO_FILE *stdin;, но сголая структура кажется очень странной, поскольку вы не можете использовать структуру или передать ее функциям. Это просто неоперация?

1 Ответ

5 голосов
/ 07 октября 2019

Код struct _IO_FILE_plus; является объявлением имени _IO_FILE_plus, так что если компилятор увидит, что оно используется в каком-то месте, он знает, что в какой-то момент будет определение, которое фактически описывает его члены.

Модификатор extern указывает, что названный символ является внешним символом, который существует в каком-то другом модуле компиляции. Код, такой как:

extern struct _IO_FILE_plus _IO_2_1_stdin_;

также является объявлением символа, в данном случае _IO_2_1_stdin_, сообщая компилятору, что символ является внешним символом, который определен и существует в некоторой другой единице компиляции (файле) и каков тип символа, в данном случае struct _IO_FILE_plus.

Однако обычно при использовании объявления struct в каком-либо другом объявлении обычно используется указатель на struct, поскольку размерstruct и его расположение не могут быть определены простым объявлением, таким как struct _IO_FILE_plus;.

Однако в этом случае, поскольку он является внешним, если исходный код не содержит какое-либо утверждение, которое требует, чтобы размер икомпоновка struct, доступная для компилятора, с использованием объявленного символа таким образом работает.

Так что, если бы у вас был источник, такой как эти операторы:

struct _IO_FILE_plus *myIo = malloc(sizeof(struct _IO_FILE_plus));

struct _IO_FILE_plus myIo = _IO_2_1_stdin_;  // no pointers here, struct assignment

они бы сгенерировалиошибка, потому что компилятору нужно определение struct _IO_FILE_plus, чтобы определить результат sizeof() или объем памяти, который нужно скопировать для назначения structв этих утверждениях.

Однако, если у вас есть такое утверждение, как:

struct _IO_FILE_plus *myIO = &_IO_2_1_stdin_;

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

Если внешняя переменная не существует, вы получите ошибку «неразрешенный внешний символ» при связывании.

Пример библиотеки API

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

Таким образом, в своей библиотеке вы открываете эти объекты или прокси-объекты как внешние, но сохраняете их внутренние секреты, только предоставляя объявление.

Затем в интерфейсе функции требуется указатель на соответствующий объект или прокси-объект, который будет использоваться с функцией.

Хорошая особенность этого подхода заключается в том, что другие стороны имеют доступ к вашей библиотекевнутренние компоненты могут предоставлять дополнительные прокси-объекты, которые работают с вашей библиотекой but с их собственными прокси-объектами.

Это особенно хорошо работает, когда определение struct содержит указатели для перехвата функций, которые ваша библиотека будет вызывать для выполнения специфических для устройства операций, о которых знает третья сторона, но у вас их нетк. У подключаемых функций есть определенный интерфейс с набором ожидаемых результатов, и то, как это делается, зависит от поставщика функции подключения.

Исходный файл библиотеки:

struct _IO_FILE_plus {
    unsigned char  buffer[1024];
    int bufptr1;
    //  …  other struct member definitions
    int (*hookOne)(struct _IO_FILE_plus *obj);   // third party hook function pointer
    int (*hookTwo)(struct _IO_FILE_plus *obj);   // third party hook function pointer
};

struct _IO_FILE_plus _IO_2_1_stdin_ = { {0}, 0, …. };
struct _IO_FILE_plus _IO_2_1_stdout_ = { {0}, 0, …. };
struct _IO_FILE_plus _IO_2_1_stderr_ = { {0}, 0, …. };

int  funcOne (struct _IO_FILE_plus *obj, int aThing)
{
    int  iResult;

    if (obj->hookOne) iResult = obj->hookOne(obj);

    // do other funcOne() stuff using the object, obj, provided

    return iResult;
}


int  funcTwo (struct _IO_FILE_plus *obj, double aThing)
{
    int  iResult;

    if (obj->hookTwo) iResult = obj->hookTwo(obj);

    // do other funcTwo() stuff using the object, obj, provided

    return iResult;
}

БиблиотекаИсходный файл компилируется нормально, потому что у компилятора есть полное определение struct. Затем в заголовочном файле, поставляемом с библиотекой, у вас есть эти операторы:

struct _IO_FILE_plus ;

extern struct _IO_FILE_plus _IO_2_1_stdin_ ;
extern struct _IO_FILE_plus _IO_2_1_stdout_ ;
extern struct _IO_FILE_plus _IO_2_1_stderr_ ;

extern int  funcOne (struct _IO_FILE_plus *obj, int aThing);
extern int  funcTwo (struct _IO_FILE_plus *obj, double aThing);

Все это работает, потому что ни один из этих исходных операторов не требует, чтобы фактическое определение struct было доступно для компилятора. Компилятору нужно только знать, что где-то определены такие символы.

В исходном файле, использующем их, вы можете иметь выражение вроде:

int k = funcOne(&_IO_2_1_stdin_, 5);

, и опять же это требует только от компилятораЗнайте, что символ существует, и в какой-то момент будет доступен адрес этого символа.

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

#define DO_FUNCONE(io,iVal)  funcOne(&(io), (iVal))

#define DO_FUNCONE_STDIN(iVal)  funcOne(&_IO_2_1_stdin_,(iVal))

#define IO_STDIN  (&_IO_2_1_stdin)

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

int k = doFuncOne (_IO_2_1_stdin_);  // compiler error. definition of struct _IO_FILE_plus not available

где определение функции функции doFuncOne() выглядит следующим образом:

// compiler error. definition of struct _IO_FILE_plus not available
int doFuncOne (struct _IO_FILE_plus obj)  // notice this is struct and not pointer to struct
{
    // do some setup then call funcOne().
    return funcOne(&obj, 33);
}

Однако изменение интерфейса функции doFuncOne() позволит ее скомпилировать:

// following would compile as only declaration is needed by the compiler.
int doFuncOne (struct _IO_FILE_plus *obj)  // notice this is now pointer to struct
{
    // do some setup then call funcOne().
    return funcOne(obj, 33);
}

Библиотека может предоставить версию функции funcOne(), скажем funcOneStruct(), которая допускает аргумент struct, а не указатель на struct, потому что компилятор имеет определение struct, доступное при компиляцииисходные файлы библиотеки. Однако люди, использующие библиотеку, не смогут использовать эту функцию, потому что у пользователей библиотеки есть только объявление struct, доступное для них, а не определение struct.

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

...