Циклы файла заголовка C

У меня есть пара заголовочных файлов, которые сводятся к следующему:

tree.h:

#include "element.h"

typedef struct tree_
{
    struct *tree_ first_child;
    struct *tree_ next_sibling;
    int tag;
    element *obj;
    ....
} tree;

и element.h:

#include "tree.h"

typedef struct element_
{
    tree *tree_parent;
    char *name;
    ...
} element;

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

Это не работает, потому что для определения «древовидной» структуры структура элемента должна быть уже известна, но для определения структуры элемента должна быть известна древовидная структура.

Как разрешить эти типы циклов (я думаю, это может иметь какое-то отношение к «форвардному объявлению»?)?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
12
0
13 362
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

Они известны как «однократные заголовки». См. http://developer.apple.com/DOCUMENTATION/DeveloperTools/gcc-4.0.1/cpp/Once_002dOnly-Headers.html#Once_002dOnly-Headers

Правильный ответ - использовать охранников включения и использовать форвардные объявления.

Включить охранников

/* begin foo.h */
#ifndef _FOO_H
#define _FOO_H

// Your code here

#endif
/* end foo.h */

Visual C++ также поддерживает #pragma once. Это нестандартная директива препроцессора. В обмен на переносимость компилятора вы уменьшаете вероятность конфликта имен препроцессора и повышаете удобочитаемость.

Предварительные декларации

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

struct tree;    /* element.h */
struct element; /* tree.h    */

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

Итак, в tree.h вместо:

#include "element.h"

делать:

typedef struct element_ element;

Это «объявляет» типы «element» и «struct element_» (говорит, что они существуют), но не «определяет» их (говорит, что они есть). Все, что вам нужно для хранения указателя на blah, - это объявить blah, а не то, что он определен. Только если вы хотите уважать его (например, читать членов), вам нужно определение. Код в вашем файле ".c" должен это делать, но в этом случае ваши заголовки этого не делают.

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

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

Прочтите про предварительные декларации.

т.е.


// tree.h:
#ifndef TREE_H
#define TREE_H
struct element;
struct tree
{
    struct element *obj;
    ....
};

#endif

// element.h:
#ifndef ELEMENT_H
#define ELEMENT_H
struct tree;
struct element
{
    struct tree *tree_parent;
    ...
};
#endif
Ответ принят как подходящий

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

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

Например.

Внутри tree.h:

// tell the compiler that element is a structure typedef:
typedef struct element_ element;

typedef struct tree_ tree;
struct tree_
{
    tree *first_child;
    tree *next_sibling;
    int tag;

    // now you can declare pointers to the structure.
    element *obj;
};

Таким образом, вам больше не нужно включать element.h в tree.h.

Вы также должны поставить include-guard вокруг ваших файлов заголовков.

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

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

Что-то типа:

struct element_;
typedef struct element_ element;

В верхней части tree.h должно быть достаточно, чтобы исключить необходимость включения element.h

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

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

Например (насколько я помню) цель «Эвристика объектно-ориентированного дизайна» - избежать включения охранников, потому что они только маскируют циклическую (физическую) зависимость.

Другой подход - предварительно объявить такие структуры:

element.h:
struct tree_;
struct element_
  {
    struct tree_ *tree_parent;
    char *name;
  };

tree.h: struct element_; struct tree_ { struct tree_ * first_child; struct tree_ * next_sibling; int tag; struct element_ * obj; };

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

Мне не нравятся форвардные объявления, потому что они избыточны и содержат ошибки. Если вы хотите, чтобы все ваши объявления были в одном месте, вам следует использовать файлы include и header с защитой include.

Вы должны рассматривать include как копипаст, когда препроцессор c находит строку #include, просто помещает все содержимое myheader.h в то же место, где была найдена строка #include.

Что ж, если вы напишете include guards, код myheader.h будет вставлен только один раз, когда был найден первый #include.

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

Простое решение - просто не иметь отдельных файлов заголовков. В конце концов, если они зависят друг от друга, вы никогда не будете использовать одно без другого, так зачем их разделять? У вас могут быть отдельные файлы .c, которые используют один и тот же заголовок, но обеспечивают более целенаправленную функциональность.

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

Во многих ответах здесь упоминается «включить охранников» и «предварительное объявление», но ни один из них на самом деле не имеет намерения решить проблему, с которой в настоящее время сталкивается OP. Третий файл с расширением ".h" определенно не является ответом. «Включить защиту» при правильном использовании может разорвать «петлю #include» и в конечном итоге привести к более чистой структуре проекта. Зачем вообще создавать еще один файл заголовка только для typedef, если у вас их уже есть? Ваши файлы заголовков должны быть такими:

/* a.h - dependency of b.h */
#ifndef _A_H
#define _A_H

#include "b.h"

typedef struct a_p {
    b_t *b;
} a_t;

#endif // _A_H
/* b.h - dependency of a.h */
#ifndef _B_H
#define _B_H

typedef struct b_p b_t;

/** 
 * !!!
 * to avoid recursion, only include "a.h" 
 * when "a.h" isn't included before
 */
#ifndef _A_H
    #include "a.h"
    typedef struct b_p {
        a_t a;
    } b_t;
#endif

#endif // _B_H

Чтобы использовать оба заголовочных файла, вам нужен только один include, то есть тот, который также безусловно включает в себя другой (в данном случае a.h). Но если хотите, вы также можете включить "b.h". Но в любом случае это не повлияет (из-за форвардного объявления).

#include "a.h"

int main() {
    a_t aigh;

    return 0;
}

Вуаля! Это оно! Никаких лишних includes нет ничего. Мы получили их!

Другие вопросы по теме