Правило 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 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 : 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 #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. Зависимость от порядка включения заголовочных файлов. Не совсем ошибка, но таких ситуаций следует избегать. Обычно сигнализирует либо об ошибках в проектировании программы, либо об ошибках при разделении исходного кода на модули. Заключение Мы рассмотрели не все случаи, возникающие при раздельной компиляции. Бывают ситуации, когда разделение программы или большого модуля на более мелкие кажется невозможным. Обычно это бывает, когда программа плохо спроектирована (в данном случае, части кода имеют сильные взаимные связи). Конечно, можно приложить дополнительные усилия и всё-таки разделить код на модули (или оставить как есть), но эту мозговую энергию лучше потратить более эффективно: на изменение структуры программы. Это принесёт в дальнейшем гораздо большие дивиденды, чем просто силовое решение. |