Может кто-нибудь помочь уточнить, как работают заголовочные файлы? - PullRequest
6 голосов
/ 23 июня 2010

Я работаю с C ++ уже пару недель, но механизм заголовочных файлов (или компоновщик, я полагаю?) Сбивает меня с толку.У меня есть привычка создавать "main.h", чтобы группировать другие мои заголовочные файлы и сохранять порядок в main.cpp, но иногда эти заголовочные файлы жалуются на невозможность найти другой заголовочный файл (даже если он объявленв "main.h").Я, вероятно, не очень хорошо объясняю это, поэтому вот сокращенная версия того, что я пытаюсь сделать:

//main.cpp

#include "main.h"
int main() {
    return 0;
}

-

//main.h

#include "player.h"
#include "health.h"
#include "custvector.h"

-

//player.h

#include "main.h"
class Player {
    private:
        Vector playerPos;
    public:
        Health playerHealth;
};

-

//custvector.h

struct Vector {
   int X;
   int Y;
   int Z;
};

-

//health.h
class Health {
    private:
        int curHealth;
        int maxHealth;
    public:
        int getHealth() const;
        void setHealth(int inH);
        void modHealth(int inHM);
};

Я не буду включать health.cpp, потому что он немного длинен (но работает), имеет #include "health.h".

В любом случае, компилятор (Code :: Blocks) жалуется, что «player.h» не может найти типы «Health» или «Vector».Я подумал, что если бы я использовал #include "main.h" в "player.h", он смог бы найти определения для Health и Vector в том смысле, что они включены в "main.h".Я подумал, что они бы как бы туннелировали бы свой путь (player.h -> main.h -> health.h).Но это не сработало.Есть ли какая-то диаграмма или видео, которые могли бы прояснить, как это должно быть настроено?Google не сильно помог (ни моя книга).

Ответы [ 5 ]

10 голосов
/ 23 июня 2010

Лучший способ думать о ваших заголовочных файлах - это «автоматическое копирование и вставка».

Хороший способ подумать об этом (хотя и не о том, как это на самом деле реализовано) заключается в том, что при компиляции файла C или файла C ++ сначала запускается препроцессор. Каждый раз, когда он встречает оператор #include, он фактически вставляет содержимое этого файла вместо оператора #include. Это делается до тех пор, пока больше нет включений. Последний буфер передается компилятору.

Это вводит несколько сложностей:

Во-первых, если A.H включает B.H, а B.H включает A.h, у вас есть проблема. Потому что каждый раз, когда вы хотите вставить A, вам понадобится B, а внутри будет A! Это рекурсия. По этой причине в заголовочных файлах используется #ifndef, чтобы гарантировать, что одна и та же часть не будет прочитана несколько раз. Это, вероятно, происходит в вашем коде.

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

8 голосов
/ 23 июня 2010

Другие ответы здесь эффективно объяснили, как работают заголовочные файлы и препроцессор. Самая большая проблема, с которой вы сталкиваетесь, - это круговые зависимости, которые, насколько я знаю, могут быть королевской болью. Кроме того, когда это начинает происходить, компилятор начинает вести себя очень странным образом и выдает сообщения об ошибках, которые не очень полезны. Метод, которому меня учил гуру C ++ в колледже, заключался в том, чтобы каждый файл (например, заголовочный файл) начинался с

//very beginning of the file
#ifndef HEADER_FILE_H //use a name that is unique though!!
#define HEADER_FILE_H
...
//code goes here
...
#endif
//very end of the file

При этом используются директивы препроцессора для автоматического предотвращения циклических зависимостей. По сути, я всегда использую заглавную версию имени файла. custom-vector.h становится

#ifndef CUSTOM_VECTOR_H
#define CUSTOM_VECTOR_H

Это позволяет вам включать файлы willie-nillie без создания циклических зависимостей, потому что, если файл включен несколько раз, его переменная препроцессора уже определена, поэтому препроцессор пропускает файл. Позже это также облегчает работу с кодом, потому что вам не нужно просеивать старые файлы заголовков, чтобы убедиться, что вы еще ничего не включили. Я повторю еще раз, но убедитесь, что имена переменных, которые вы используете в своих операторах #define, уникальны для вас, иначе вы можете столкнуться с проблемами, когда что-то не включается должным образом;

Удачи!

3 голосов
/ 23 июня 2010

У вас круговая зависимость. Player включает в себя main.h, но main.h включает player.h. Решите это, удалив одну или другую зависимость. \

Player.h должен включать health.h и custvector.h, и на данный момент я не думаю, что main.h нуждается в каких-либо включениях В конце концов может понадобиться player.h.

2 голосов
/ 23 июня 2010

включает работу очень просто, они просто командуют препроцессором, чтобы добавить содержимое файла туда, где установлено включение.Основная идея - включить заголовки, от которых вы зависите.в player.h вы должны включить custvector.h и Health.h.В основном только player.h, потому что все необходимые включения будут осуществляться с игроком.и вам вообще не нужно включать main.h в player.h.

хорошо также убедиться, что заголовок включен только один раз.В этом вопросе дано общее решение Как предотвратить множественные определения в C? В случае Visual Studio вы можете использовать #pragma once, если Borland c ++ также есть хитрость, но я забыл об этом.

1 голос
/ 23 июня 2010

Вы хотите организовать свои #includes (и библиотеки в этом отношении) в DAG (направленный ациклический граф). Это сложный способ сказать «избегать циклов между заголовочными файлами»:

Если B включает A, A не должно включать B.

Таким образом, использование "одного большого мастера main.h" не является правильным подходом, потому что трудно включить только прямые зависимости.

Каждый файл .cpp должен включать свой собственный файл .h. Этот файл .h должен содержать только то, что ему самому нужно для компиляции.

Обычно нет main.h, потому что main.cpp никому не нужно определение main.

Кроме того, вам нужно, чтобы включали охрану , чтобы защитить вас от нескольких включений.

Например

//player.h
#ifndef PLAYER_H_
#define PLAYER_H_
#include "vector.h"  // Because we use Vector
#include "health.h"  // Because we use Health
class Player {
    private:
        Vector playerPos;
    public:
        Health playerHealth;
};
#endif

-

//vector.h
#ifndef VECTOR_H_
#define VECTOR_H_
struct Vector {
   int X;
   int Y;
   int Z;
};
#endif

-

//health.h
#ifndef HEALTH_H_
#define HEALTH_H_
class Health {
    private:
        int curHealth;
        int maxHealth;
    public:
        int getHealth() const;
        void setHealth(int inH);
        void modHealth(int inHM);
};
#endif

Единственный раз, когда вы хотите объединить несколько #include с в один заголовок, это когда вы предоставляете его для удобства очень большой библиотеки.

В вашем текущем примере вы немного перегружены - каждому классу не нужен собственный заголовочный файл. Это может все идти в main.cpp.

Препроцессор c буквально вставляет файл из #include в файл, который включает его (если он не был уже вставлен, поэтому вам нужны защитные элементы включения). Это позволяет вам использовать классы, определенные в этих файлах, потому что теперь у вас есть доступ к их определению.

...