Подтвердить что ты не робот

Является ли хорошей практикой поставить определение классов С++ в заголовочный файл?

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

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

4b9b3361

Ответ 1

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

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

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

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

Конечно, в ODR есть несколько исключений. Первая из них включает в себя функции и классы шаблонов. ODR явно заявляет, что вы можете иметь несколько разных определений для одного и того же класса или функции шаблона при условии, что все они эквивалентны. Это прежде всего упрощает сбор шаблонов - каждый файл С++ может создавать один и тот же шаблон без столкновения с другими файлами. По этой причине и еще несколько технических причин шаблоны классов имеют простой файл .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 файла.

Ответ 2

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

Файл заголовка A - содержит определение metahodA()

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

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

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

    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();
    }