Является ли хорошей практикой помещать определение классов C ++ в заголовочный файл? - PullRequest
30 голосов
/ 10 февраля 2011

Когда мы разрабатываем классы на Java, Vala или C #, мы помещаем определение и объявление в один и тот же исходный файл. Но в C ++ традиционно предпочтительнее разделять определение и объявление в двух или более файлах.

Что произойдет, если я просто использую файл заголовка и помещаю в него все, как Java? Есть ли потеря производительности или что-то в этом роде?

Ответы [ 5 ]

50 голосов
/ 10 февраля 2011

Ответ зависит от того, какой класс вы создаете.

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

Если вы возьмете не шаблонный класс и поместите и определение класса, и реализацию в заголовочный файл, у вас могут возникнуть проблемы с одним правилом определения. В частности, предположим, что у меня есть два разных файла .cpp, которые я компилирую, оба из которых #include ваш заголовок, содержащий как реализацию, так и интерфейс. В этом случае, если я попытаюсь связать эти два файла вместе, компоновщик обнаружит, что каждый из них содержит копию кода реализации для функций-членов класса. На этом этапе компоновщик сообщит об ошибке, потому что вы нарушили одно правило определения: есть две разные реализации всех функций-членов класса.

Чтобы предотвратить это, программисты на C ++ обычно разделяют классы на заголовочный файл, который содержит объявление класса вместе с объявлениями его функций-членов без реализаций этих функций. Реализации затем помещаются в отдельный файл .cpp, который можно скомпилировать и связать отдельно. Это позволяет вашему коду избежать проблем с ODR. Вот как. Во-первых, всякий раз, когда вы #include файл заголовка класса делите на несколько разных файлов .cpp, каждый из них просто получает копию объявлений функций-членов, а не их определения , и поэтому ни один из клиентов вашего класса не получит определения. Это означает, что любое количество клиентов может #include ваш заголовочный файл без проблем во время соединения. Поскольку ваш собственный файл .cpp с реализацией является единственным файлом, который содержит реализации функций-членов, во время компоновки вы можете без проблем объединить его с любым количеством других объектных файлов клиента. Это основная причина разделения файлов .h и .cpp.

Конечно, у ODR есть несколько исключений. Первый из них - шаблонные функции и классы. В ODR прямо указано, что вы можете иметь несколько разных определений для одного и того же класса шаблона или функции, при условии, что все они эквивалентны. Это в первую очередь облегчает компиляцию шаблонов - каждый файл C ++ может создавать один и тот же шаблон без коллизии с другими файлами. По этой причине и по нескольким другим техническим причинам шаблоны классов обычно имеют файл .h без соответствующего файла .cpp. Любое количество клиентов может #include файл без проблем.

Другое серьезное исключение из ODR касается встроенных функций. В спецификации конкретно указано, что ODR не применяется к встроенным функциям, поэтому, если у вас есть файл заголовка с реализацией функции-члена класса, помеченной как встроенный, это прекрасно. Любое количество файлов может #include этот файл, не нарушая ODR. Интересно, что любая функция-член, которая объявлена ​​и определена в теле класса, неявно встроена, поэтому, если у вас есть такой заголовок:

#ifndef Include_Guard
#define Include_Guard

class MyClass {
public:
    void DoSomething() {
        /* ... code goes here ... */
    }
};

#endif

Тогда вы не рискуете нарушить ODR. Если переписать это как

#ifndef Include_Guard
#define Include_Guard

class MyClass {
public:
    void DoSomething();
};

void MyClass::DoSomething()  {
    /* ... code goes here ... */
}

#endif

тогда будет прерывать ODR, так как функция-член не помечена как встроенная, и если несколько клиентов #include, этот файл будет иметь несколько определений MyClass::DoSomething.

Итак, подведем итог - вам, вероятно, следует разделить ваши классы на пару .h / .cpp, чтобы избежать нарушения ODR. Однако, если вы пишете шаблон класса, вам не нужен файл .cpp (и, вероятно, не должен иметь его вообще), и если вы в порядке, помечая каждую функцию-член вашего класса встроенной, вы также можете избегайте .cpp файла.

5 голосов
/ 10 февраля 2011

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

Заголовочный файл A - содержит определение metahodA ()

Заголовочный файл B - включает заголовочный файл A.

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

2 голосов
/ 10 февраля 2011

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

0 голосов
/ 10 февраля 2011

Две конкретные проблемы с размещением всего в шапке:

  1. Время компиляции будет увеличено, иногда значительно. Время компиляции C ++ достаточно велико, чтобы вы этого не хотели.

  2. Если в реализации есть циклические зависимости, хранить все в заголовках сложно или невозможно. например:

    header1.h

    struct C1
    {
      void f();
      void g();
    };
    

    header2.h

    struct C2
    {
      void f();
      void g();
    };
    

    impl1.cpp

    #include "header1.h"
    #include "header2.h"
    
    void C1::f()
    {
      C2 c2;
      c2.f();
    }
    

    impl2.cpp

    #include "header2.h"
    #include "header1.h"
    
    void C2::g()
    {
      C1 c1;
      c1.g();
    }
    
0 голосов
/ 10 февраля 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...