Раздельная компиляция программ на C++ Часть 2 | Разработка игровых платформ
  1. Доброго времени суток. В связи с появлением спамеров, активация функций новых пользователей (Создавать темы, писать сообщения), теперь будет только после проверки администратором! Для регистрации отписываемся в лс, в вк. vk.com/tehnik777 (Пишем только с реальных страниц)
    Скрыть объявление

Раздельная компиляция программ на C++ Часть 2

Тема в разделе "С++/С#, HTML, PHP, JavaScript, XML...", создана пользователем Zloy_Enot, 6 ноя 2016.

Обсуждение темы Раздельная компиляция программ на C++ Часть 2 в разделе С++/С#, HTML, PHP, JavaScript, XML... на форуме zetta-forum.ru.

  1. Zloy_Enot

    Zloy_Enot Модератор

    Регистрация:
    31 окт 2016
    Сообщения:
    26
    Симпатии:
    13
    Баллы:
    3
    Пол:
    Мужской
    Адрес:
    Калининград
    Что может быть в заголовочном файле

    Правило 1. Заголовочный файл может содержать только объявления. Заголовочный файл не должен содержать определения.


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

    Единственным «исключением» из этого правила является определение метода в объявлении класса. Но по стандарту языка, если метод определён в объявлении класса, то для этого метода используется инлайновая подстановка. Поэтому, такое объявление не порождает исполняемого кода — код будет генерироваться компилятором только при вызове этого метода.

    Аналогичная ситуация и с объявлением переменных-членов класса: код будет порождаться при создании экземпляра этого класса.

    Правило 2. Заголовочный файл должен иметь механизм защиты от повторного включения.

    Защита от повторного включения реализуется директивами препроцессора:


    Код:
    #ifndef SYMBOL
    #define SYMBOL
    
    // набор объявлений
    
    #endif
    

    Для препроцессора при первом включении заголовочного файла это выглядит так: поскольку условие "символ SYMBOL не определён" (#ifndef SYMBOL) истинно, определить символ SYMBOL (#define SYMBOL) и обработать все строки до директивы #endif. При повторном включении — так: поскольку условие " символ SYMBOL не определён" (#ifndef SYMBOL) ложно (символ был определён при первом включении), то пропустить всё до директивы #endif.

    В качестве SYMBOL обычно применяют имя самого заголовочного файла в верхнем регистре, обрамлённое одинарными или сдвоенными подчерками. Например, для файла header.h традиционно используется #define __HEADER_H__. Впрочем, символ может быть любым, но обязательно уникальным в рамках проекта.

    В качестве альтернативного способа может применяться директива #pragma once. Однако преимущество первого способа в том, что он работает на любых компиляторах.

    Заголовочный файл сам по себе не является единицей компиляции.

    Что может быть в файле реализации

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

    Правило 3. В файле реализации должна быть директива включения соответствующего заголовочного файла.

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

    Правило также гарантирует соответствие между описанием и реализацией. При несовпадении, допустим, сигнатуры функции в объявлении и определении компилятор выдаст ошибку.

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

    При выполнении Правила 3, нарушение Правила 4 приведёт к ошибкам компиляции.

    Практический пример

    Допустим, у нас имеется следующая программа:

    main.cpp


    Код:
    #include <iostream>
    
    using namespace std;
    
    const int cint = 10;        // глобальная константа
    
    int global_var = 0;         // глобальная переменная
    
    int module_var = 0;         // глобальная переменная для func1 и func2
    
    int func1() {
        ++global_var;
        return ++module_var;
    }
    
    int func2() {
        ++global_var;
        return --module_var;
    }
    
    class CClass {
    public:
        CClass() : priv(cint) { ++counter; }
        ~CClass() { --counter; }
        void change(int arg);
        int get_priv() const;
        int get_counter() const;
    private:
        int priv;
        static int counter;
    };
    
    int CClass::counter = 0;
    
    void CClass::change(int arg) {
        priv += arg;
    }
    
    int CClass::get_priv() const {
        return priv;
    }
    
    int CClass::get_counter() const {
        return counter;
    }
    
    int main()
    {
        int balance;
        balance = func1();
        balance = func2();
        cout << "balance: " << balance << " counter: " << global_var << endl;
    
        CClass c1, c2;
        if (c1.get_priv() == cint)
            cout << "Ok" << endl;
        cout << c2.get_counter() << endl;
        return 0;
    }
    

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

    Итак, что у нас имеется?

    1. Глобальная константа cint, которая используется и в классе, и в main;
    2. Глобальная переменная global_var, которая используется в функциях func1, func2 и main;
    3. Глобальная переменная module_var, которая используется только в функциях func1 и func2;
    4. Функции func1 и func2;
    5. Класс CClass;
    6. Функция main.

    Вроде вырисовываются три единицы компиляции: (1) функция main, (2) класс CClass и (3) функции func1 и func2 с глобальной переменной module_var, которая используется только в них.

    Не совсем понятно, что делать с глобальной константой cint и глобальной переменной global_var. Первая тяготеет к классу CClass, вторая — к функциям func1 и func2. Однако предположим, что планируется и эту константу, и эту переменную использовать ещё в каких-то, пока не написанных, модулях программы. Поэтому прибавится ещё одна единица компиляции.

    Теперь пробуем разделить программу на модули.

    Сначала, как наиболее связанные сущности (используются во многих местах программы), выносим глобальную константу cint и глобальную переменную global_varв отдельную единицу компиляции.

    globals.h


    Код:
    #ifndef __GLOBALS_H__
    #define __GLOBALS_H__
    
    const int cint = 10;            // глобальная константа
    extern int global_var;          // глобальная переменная
    
    #endif // __GLOBALS_H__
    

    globals.cpp


    Код:
    #include "globals.h"
    
    int global_var = 0;         // глобальная переменная
    

    Обратите внимание, что глобальная переменная в заголовочном файле имеет спецификатор extern. При этом получается объявление переменной, а не её определение. Такое описание означает, что где-то существует переменная с таким именем и указанным типом. А определение этой переменной (с инициализацией) помещено в файл реализации. Константа описана в заголовочном файле.

    С объявлением констант в заголовочном файле существует одна тонкость. Если константа тривиального типа, то её можно объявить в заголовочном файле. В противном случае она должна быть определена в файле реализации, а в заголовочном файле должно быть её объявление (аналогично, как для переменной). «Тривиальность» типа зависит от стандарта (см. описание того стандарта, который используется для написания программы).

    Также обратите внимание (1) на защиту от повторного включения заголовочного файла и (2) на включение заголовочного файла в файле реализации.

    Затем выносим в отдельный модуль функции func1 и func2 с глобальной переменной module_var. Получаем ещё два файла:

    funcs.h


    Код:
    #ifndef __FUNCS_H__
    #define __FUNCS_H__
    
    int func1();
    int func2();
    
    #endif // __FUNCS_H__
    

    funcs.cpp


    Код:
    #include "funcs.h"
    #include "globals.h"
    
    int module_var = 0;         // глобальная переменная для func1 и func2
    
    int func1() {
        ++global_var;
        return ++module_var;
    }
    
    int func2() {
        ++global_var;
        return --module_var;
    }
    

    Поскольку переменная module_var используется только этими двумя функциями, её объявление в заголовочном файле отсутствует. Из этого модуля «на экспорт» идут только две функции.

    В функциях используется переменная из другого модуля, поэтому необходимо добавить #include "globals.h".

    Наконец выносим в отдельный модуль класс CClass:

    Поскольку переменная module_var используется только этими двумя функциями, её объявление в заголовочном файле отсутствует. Из этого модуля «на экспорт» идут только две функции.

    В функциях используется переменная из другого модуля, поэтому необходимо добавить #include "globals.h".

    Наконец выносим в отдельный модуль класс CClass:

    CClass.h


    Код:
    #ifndef __CCLASS_H__
    #define __CCLASS_H__
    
    class CClass {
    public:
        CClass();
        ~CClass();
        void change(int arg);
        int get_priv() const;
        int get_counter() const;
    private:
        int priv;
        static int counter;
    };
    
    #endif // __CCLASS_H__
    

    CClass.cpp


    Код:
    #include "CClass.h"
    #include "globals.h"
    
    int CClass::counter = 0;
    
    CClass::CClass() : priv(cint) {
        ++counter;
    }
    
    CClass::~CClass() {
        --counter;
    }
    
    void CClass::change(int arg) {
        priv += arg;
    }
    
    int CClass::get_priv() const {
        return priv;
    }
    
    int CClass::get_counter() const {
        return counter;
    }
    

    Обратите внимание на следующие моменты.

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

    (2) Класс имеет статический член класса. Т.е. для всех экземпляров класса эта переменная будет общей. Её инициализация выполняется не в конструкторе, а в глобальной области модуля.

    (3) В файл реализации добавлена директива #include "globals.h" для доступа к константе cint.

    Классы практически всегда выделяются в отдельные единицы компиляции.

    В файле main.cpp оставляем только функцию main. И добавляем необходимые директивы включения заголовочных файлов.

    main.cpp


    Код:
    #include <iostream>
    #include "funcs.h"
    #include "CClass.h"
    #include "globals.h"
    
    using namespace std;
    
    int main()
    {
        int balance;
        balance = func1();
        balance = func2();
        cout << "balance: " << balance << " counter: " << global_var << endl;
    
        CClass c1, c2;
        if (c1.get_priv() == cint)
            cout << "Ok" << endl;
        cout << c2.get_counter() << endl;
        return 0;
    }
    

    Последний шаг: необходимо изменить «проект» построения программы так, что бы он отражал изменившуюся структуру файлов исходного кода. Детали этого шага зависят от используемой технологии построения программы и используемого ПО. Но в любом случае сначала должны быть откомпилированы четыре единицы компиляции (четыре cpp-файла), а затем полученные объектные файлы должны быть обработаны компоновщиком для получения исполняемого файла.

    Типичные ошибки

    Ошибка 1. Определение в заголовочном файле.

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

    Ошибка 2. Отсутствие защиты от повторного включения заголовочного файла.

    Тоже проявляет себя при определённых обстоятельствах. Может вызывать ошибку компиляции «многократное определение символа ...».

    Ошибка 3. Несовпадение объявления в заголовочном файле и определения в файле реализации.

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

    Ошибка 4. Отсутствие необходимой директивы #include.

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

    Ошибка 5. Отсутствие необходимого модуля в проекте построения программы.

    Вызывает ошибку компоновки «не определён символ ...». Обратите внимание, что имя символа в сообщении компоновщика почти всегда отличается от того, которое определено в программе: оно дополнено другими буквами, цифрами или знаками.

    Ошибка 6. Зависимость от порядка включения заголовочных файлов.

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

    Заключение

    В рамках небольшой статьи невозможно рассмотреть все случаи, возникающие при раздельной компиляции. Бывают ситуации, когда разделение программы или большого модуля на более мелкие кажется невозможным. Обычно это бывает, когда программа плохо спроектирована (в данном случае, части кода имеют сильные взаимные связи). Конечно, можно приложить дополнительные усилия и всё-таки разделить код на модули (или оставить как есть), но эту мозговую энергию лучше потратить более эффективно: на изменение структуры программы. Это принесёт в дальнейшем гораздо большие дивиденды, чем просто силовое решение.
     

Поделиться этой страницей