Как я могу избежать включения файлов реализации класса? - PullRequest
3 голосов
/ 10 января 2010

Вместо того, чтобы делать

#include "MyClass.cpp"

Я бы хотел сделать

#include "MyClass.h"

Я читал в Интернете, что не это считается плохой практикой.

Ответы [ 7 ]

13 голосов
/ 10 января 2010

Отдельная компиляция в двух словах

Во-первых, давайте рассмотрим несколько быстрых примеров:

struct ClassDeclaration;   // 'class' / 'struct' mean almost the same thing here
struct ClassDefinition {}; // the only difference is default accessibility
                           // of bases and members

void function_declaration();
void function_definition() {}

extern int global_object_declaration;
int global_object_definition;

template<class T>           // cannot replace this 'class' with 'struct'
struct ClassTemplateDeclaration;
template<class T>
struct ClassTemplateDefinition {};

template<class T>
void function_template_declaration();
template<class T>
void function_template_definition() {}

Единица перевода

A единица перевода (TU) - это один исходный файл (должен быть файл **. Cpp *) и все файлы, которые он включает, и они включают в себя, и т. Д. Другими словами: результат предварительной обработки одного файла.

Headers

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

Включите работу охраны, сделав последующие #include no-ops с определениями, доступными из первого include. Из-за их ограниченного характера макросы, управляющие параметрами заголовков, должны быть согласованными по всему проекту (странные заголовки, такие как вызывают проблемы), а все #include открытых заголовков должны находиться вне любого пространства имен, класса и т. Д. верх любого файла.

См. Мой include guard рекомендации по именованию , включая короткую программу для генерации include guard .

Объявления

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

Определения

Классы могут быть определены не более одного раза [1] на TU; это обычно происходит, когда вы включаете заголовок для определенного класса. Функции и объекты должны быть определены один раз ровно в одном TU; это обычно происходит, когда вы реализуете их в файле **. cpp *. Однако встроенные функции , включая неявно встроенные функции внутри определений классов, могут быть определены в нескольких TU, но определения должны быть идентичными.

Для практических целей [2] , шаблоны (как шаблоны классов, так и шаблоны функций) определены только в заголовках, и если вы хотите использовать отдельный файл, используйте другой заголовок [3] .

[1] Из-за ограничения не более одного раза заголовки используют защитные устройства для предотвращения многократного включения и, следовательно, множественных ошибок определения.
[2] Я не буду здесь останавливаться на других возможностях.
[3] Назовите его blahblah_detail.hpp , blahblah_private.hpp или аналогичным, если вы хотите документально подтвердить, что он не является общедоступным.

Руководство

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

  • Всегда заголовки имен последовательно в одном проекте, например **. H * для C и **. Hpp * для C ++.
  • Никогда не содержит файл, который не является заголовком.
  • Всегда именуют файлы реализации (которые будут скомпилированы напрямую), такие как **. C * и **. Cpp *.
  • Используйте систему сборки , которая может автоматически компилировать ваши исходные файлы. make является каноническим примером, но есть много альтернатив. Сохраняйте это простым в простых случаях. Например, make может использовать свои встроенные правила и даже без make-файла.
  • Используйте систему сборки, которая может генерировать зависимости заголовка. Некоторые компиляторы могут генерировать это с помощью переключателей командной строки, таких как -M , так что вы можете легко создать удивительно полезную систему .

Процесс сборки

(Вот небольшая часть, которая отвечает на ваш вопрос, но вам нужно большинство из вышеперечисленного, чтобы попасть сюда.)

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

  1. компилировать каждый файл реализации в виде TU, создавая объектный файл (**. O *, **. Obj *)
    • каждый компилируется независимо от других, поэтому каждый TU нуждается в объявлениях и определениях
  2. связывает эти файлы вместе с указанными библиотеками в один исполняемый файл

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

Выбор системы сборки - это почти религиозный опыт, подобный выбору редактора, за исключением того, что вам придется работать с большим количеством людей (все работают над одним и тем же проектом) и, вероятно, будут намного более ограничены прецедентом и соглашением. Вы можете использовать IDE, которая обрабатывает те же детали для вас, но это не дает никакой реальной выгоды от использования комплексной системы сборки, и вы действительно должны знать, что она делает изнутри.

Шаблоны файлов

* +1135 * example.hpp
#ifndef EXAMPLE_INCLUDE_GUARD_60497EBE580B4F5292059C8705848F75
#define EXAMPLE_INCLUDE_GUARD_60497EBE580B4F5292059C8705848F75
// all project-specific macros for this project are prefixed "EXAMPLE_"

#include <ostream> // required headers/"modules"/libraries from the
#include <string>  // stdlib, this project, and elsewhere
#include <vector>

namespace example { // main namespace for this project
template<class T>
struct TemplateExample { // for practical purposes, just put entire
  void f() {}            // definition of class and all methods in header
  T data;
};

struct FooBar {
  FooBar(); // declared
  int size() const { return v.size(); } // defined (& implicitly inline)
private:
  std::vector<TemplateExample<int> > v;
};

int main(std::vector<std::string> args); // declared
} // example::

#endif

example.cpp

#include "example.hpp" // include the headers "specific to" this implementation
// file first, helps make sure the header includes anything it needs (is
// independent)

#include <algorithm> // anything additional not included by the header
#include <iostream>

namespace example {
FooBar::FooBar() : v(42) {} // define ctor

int main(std::vector<std::string> args) { // define function
  using namespace std; // use inside function scope, if desired, is always okay
  // but using outside function scope can be problematic
  cout << "doing real work now...\n"; // no std:: needed here
  return 42;
}
} // example::

main.cpp

#include <iostream>
#include "example.hpp"

int main(int argc, char const** argv) try {
  // do any global initialization before real main
  return example::main(std::vector<std::string>(argv, argv + argc));
}
catch (std::exception& e) {
  std::cerr << "[uncaught exception: " << e.what() << "]\n";
  return 1; // or EXIT_FAILURE, etc.
}
catch (...) {
  std::cerr << "[unknown uncaught exception]\n";
  return 1; // or EXIT_FAILURE, etc.
}
3 голосов
/ 10 января 2010

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

2 голосов
/ 10 января 2010

Вам не нужно включать файлы .c или .cpp - компилятор скомпилирует их независимо от того, включены они в другие файлы или нет. Однако код в файлах .c / .cpp бесполезен, если другие файлы не знают о классах / методах / функциях / global vars /, что бы в них ни содержалось. И именно здесь в игру вступают заголовки. В заголовки вы помещаете только объявления, такие как эта:

//myfile.hpp
class MyClass {
    public:
        MyClass (void);
        void myMethod (void);
        static int myStaticVar;
    private:
        int myPrivateVar;
};

Теперь все .c / .cpp файлы, которые будут включать в себя #include "myfile.hpp", смогут создавать экземпляры MyClass, работать с myStaticVar и вызывать MyClass :: myMethod (), даже если здесь нет реальной реализации! См

Реализация (фактический код) идет в myfile.cpp, где вы сообщаете компилятору, что все ваши вещи делают:

//myfile.cpp
int MyClass::myStaticVar = 0;

MyClass::MyClass (void) {
    myPrivateVar = 0;
}

void MyClass::myMethod (void) {
    myPrivateVar++;
}

Вы никогда нигде не включаете этот файл, он абсолютно не нужен.

Совет: создайте файл main.hpp (или main.h, если хотите - без разницы) и поместите туда все файлы #include. Каждый файл .c / .cpp должен иметь следующую строку: #include "main.hpp". Этого достаточно, чтобы иметь доступ ко всем классам, методам и т. Д., Которые вы объявили во всем своем проекте:).

2 голосов
/ 10 января 2010

В дополнение к сокрытию деталей реализации в файлах cpp (проверьте другие ответы), вы можете дополнительно скрыть детали структуры с помощью декларации пересылки классов.

class FooPrivate;  

class Foo  
{  
  public:  
  // public stuff goes here  
  private:  
  FooPrivate *foo_private;  
};

Выражение class FooPrivate говорит, что FooPrivate полностью определено где-то еще (предпочтительно в том же файле, где находится реализация Foo, до того, как поступит Foo). Таким образом, вы убедитесь, что детали реализации Foo (Private) не предоставляется через заголовочный файл.

1 голос
/ 10 января 2010

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

Для этого вам нужно указать компилятору, используя #define, включать файл только в том случае, если #define еще не существует.

Например (MyClass.h):

#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass 
{
// Memebers and methods
}
#endif
// End of file

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

1 голос
/ 10 января 2010

Файлы Cpp должны быть определены в вашем скрипте компилятора для компиляции как объектные файлы.

Какой идеал вы используете? Я собираюсь предположить, что вы компилируете с помощью gcc, поэтому вот команда для компиляции двух файлов .cpp в один исполняемый файл

gcc -o myclasses.out myclass.cpp myotherclass.cpp

Вы должны использовать только #include для включения определений классов, а не внедрения

1 голос
/ 10 января 2010

Вы не должны включать исходный файл (.c или .cpp). Вместо этого вы должны включить соответствующий заголовочный файл (.h), содержащий объявления. Исходные файлы необходимо скомпилировать отдельно и связать вместе, чтобы получить конечный исполняемый файл.

...