}
Ключевое слово extern
— это способ сказать компилятору, что реальная область памяти для переменной выделяется в другом месте, extern
говорит компоновщику, что переменная описана где-то в другом объектном файле и что компоновщик должен найти ее при создании конечного исполняемого файла или библиотеки. Если компоновщик не находит переменной, объявленной как extern
, или если он находит более одного ее определения, он генерирует ошибку компоновки.
Пример 2.3 не слишком впечатляет, но он хорошо иллюстрирует вопрос. Две мои глобальные переменные объявляются в global.cpp:
int x = 7;
std::string s = "Kangaroo";
Мне требуется доступ к ним из других файлов реализации, так что я поместил в заголовочный файл global.h объявление extern
для них:
extern int x;
extern std::string s
;
Разница между объявлением и определением очень важна. В C++ можно объявить что-либо несколько раз, при условии совпадения объявлений, но определить что-либо можно только один раз. Это называется правилом одного определения (на самом деле в некоторых случаях определить объект можно несколько раз, но только если определения абсолютно идентичны — обычно это бессмысленно). Ключевое слово extern
— это механизм, позволяющий сказать компилятору и компоновщику, что определение находится где-то еще и что оно должно быть разрешено при компоновке.
Нельзя сказать, что использование extern
должно быть постоянным. Его следует использовать обдуманно и только тогда, когда это необходимо, так как оно позволяет создавать переменные, глобальные для всего приложения. Иногда оно может потребоваться для поистине глобальных объектов или данных — объекта журналирования, оборудования, большого объекта общих данных, но в большинстве случаев имеются более адекватные альтернативы.
2.3. Снижение числа #include с помощью предварительного объявления классов
Имеется заголовочный файл, который ссылается на классы из других заголовочных файлов, и требуется снизить зависимости компиляции (и, возможно, время).
Чтобы избежать ненужных зависимостей при компиляции, везде, где только возможно, используйте предварительное объявление классов. Пример 2.4. является коротким примером предварительного объявления класса.
Пример 2.4. Предварительное объявление класса
// myheader.h
#ifndef MYHEADER_H__
#define MYHEADER_H__
class A; // заголовок для А включать не требуется
class В {
public:
void f(const A& a);
// ...
private:
A* a_;
};
#endif
Где-то в другом месте имеется заголовочный файл и, вероятно, файл реализации, который объявляет и определяет класс А
, но в файле myheader.h подробности класса А
меня не волнуют: все, что мне требуется знать, — это то, что А
— это класс.
Предварительное объявление класса — это способ игнорировать подробности, о которых не требуется беспокоиться. В примере 2.4 myheader.h не должен знать о классе А
ничего, кроме того, что он существует и что это класс.
Рассмотрим, что случится, если с помощью #include
включить заголовочный файл для А
, или, что более реально, заголовочный файл для полудюжины или более классов, присутствующих в реальном заголовочном файле. Тогда файл реализации (myheader.cpp) будет включать заголовочный файл myheader.h, так как он содержит все объявления. До сих пор все было хорошо. Но если вы измените один из заголовочных файлов, включаемых в myheader.h (или один из заголовочных файлов, включаемых одним из этих файлов), то потребуется перекомпилировать все файлы реализации, включающие myheader.h.
Создайте предварительное объявление класса, и эти зависимости компиляции исчезнут. Использование предварительного объявления просто создает имя, на которое можно ссылаться далее в заголовочном файле. Компоновщик должен будет сам найти в файлах реализаций подходящее определение.
К несчастью, использовать предварительное объявление можно не всегда. Класс В
в примере 2.4 использует только указатели или ссылки на A
, так что ему достаточно только предварительного объявления. Однако если бы в определении класса В
я использовал функцию-член (метод) или переменную А
или если бы создавал объект типа А
, а не только указатель или ссылку на него, то предварительного объявления окажется недостаточно. Причиной этого является то, что файлы, включающие myheader.h, должны знать размер В
, и если A
является членом В
, то компилятор, чтобы определить размер В
, должен знать размер А
. Указатель или ссылка на что-либо всегда имеют один и тот же размер, так что в случае использования указателей или ссылок подробности об А
компилятор не интересуют, и, следовательно, заголовочный файл не требуется
Неудивительно, что если включить в myheader.h какое-либо определение, использующее членов A
, то потребуется включить через #includ
e заголовок для А
. Это требуется для того, чтобы компилятор мог проверить сигнатуру используемой функции-члена А
или тип переменной-члена А
. Вот иллюстрация кода, требующего #include
.
#include "a.h"
class B {
public:
void f(const A& a) {
foo_ = a.getVal(); // требуется знать, допустимо ли a.getVal
}
}
// ...
В общем случае используйте предварительное объявление тогда, когда это позволяет снизить количество #include
, что отражается на времени компиляции.
2.4. Предотвращение конфликта имен с помощью пространств имен
В несвязанных между собой модулях обнаружены конфликтующие имена или требуется заранее избежать возможности таких конфликтов, создав логические группы кода.
Для структурирования кода используйте пространства имен. С помощью пространств имен можно объединять большие группы кода, находящиеся в разных файлах, в единое пространство имен. Для разбиения больших модулей на подмодули можно использовать вложенные пространства имен, и потребители вашего модуля смогут выборочно открывать элементы вашего пространства имен, которые им требуются. Пример 2.5 показывает несколько способов использования пространства имен.
Пример 2.5. Использование пространств имен
// Devices.h
#ifndef DEVICES_H__
#define DEVICES_H__
#include <string>
#include <list>
namespace hardware {
class Device {
public:
Device(): uptime_(0), status_("unknown") {}
unsigned long getUptime() const;
std::string getStatus() const;