Пример 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, то потребуется включить через #include заголовок для А. Это требуется для того, чтобы компилятор мог проверить сигнатуру используемой функции-члена А или тип переменной-члена А. Вот иллюстрация кода, требующего #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;
void reset();
private:
unsigned long uptime_;
std::string status_;
};
class DeviceMgr {
public:
void getDeviceIds(std::list<std::string>& ids) const;
Device getDevice(const std::string& id) const;
// Other stuff...
};
}
#endif // DEVICES_H__
// Devices.cpp
#include "Devices.h"
#include <string>
#include <list>
namespace hardware {
using std::string;
using std::list;
unsigned long Device::getUptime() const {
return(uptime__);
}
string Device::getStatus() const {
return(status_);
}
void DeviceMgr::getDeviceIds(list<string>& ids) const {}
Device DeviceMgr::getDevice(const string& id) const {
Device d;
return(d);
}
}
// DeviceWidget.h
#ifndef DEVICEWIDGET_H__ #define DEVICEWIDGET_H__
#include "Devices.h"
namespace ui {
class Widget {/*... */ };
class DeviceWidget : public Widget {
public:
DeviceWidget(const hardware::Device& dev) : device_(dev) {}
// Some other stuff
protected:
hardware::Device device_;
};
}
#endif // DEVICEWIDGET_H__
// main.cpp
#include <iostream>
#include "DeviceWidget.h"
#include "Devices.h"
int main( ) {
hardware::Device d;
ui::DeviceWidget myWidget(d);
// ...
}
Обсуждение
Пример 2.5 более сложен, но давайте разберем его по частям, так как он иллюстрирует несколько ключевых моментов, связанных с пространствами имен. Представьте, что вы пишете управляющее приложение, которое должно взаимодействовать с различным оборудованием. С целью устранения конфликтов имен вы можете разбить это приложение на два или более пространств имен или просто разделить его логически на две части.
Вначале рассмотрим файл Devices.h. Он содержит пару классов, которые управляют элементами оборудования, — Device и DeviceMgr. Однако они не должны находиться в глобальном пространстве имен (что означало бы, что их имена видны в любом месте программы), так что я поместил их в пространство имен hardware.