Р. ЛАФОРЕ ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В C++ (4-Е ИЗДАНИЕ) (часть13) онлайн
Взаимодействие исходных файлов
Давайте рассмотрим, как сообщаются между собой отдельные исходные файлы.
Мы обратим внимание на три основных элемента: переменные, функции и клас-
сы. Для каждого из них есть свои правила межфайлового взаимодействия. В этом
месте полезно будет вспомнить о понятии зоны действия, для этого придется
вернуться ненадолго к главе 5. Зона действия (или зона видимости) — это часть
программы, внутри которой к данной переменной или другому элементу про-
граммы имеется доступ. Так, элементы, объявленные внутри функции, имеют
локальную зону видимости, то есть доступ к ним может осуществляться только
из данной функции. Соответственно, компоненты класса видны только внутри
класса (кроме случаев использования оператора явного задания).
Элементы программы, объявленные вне всех функций и классов, имеют гло-
бальную зону видимости, то есть доступ к ним осуществляется из любого места
файла исходного текста программы. Как мы сейчас увидим, из других файлов
того же проекта глобальные переменные тоже видны.
Межфайловые переменные
Начнем с простых переменных. Для этого вспомним разницу между объявлени-
ем и определением. Мы объявляем, декларируем какую-то простую переменную,
задавая ее тип и имя. Это не означает, что для нее сразу же резервируется место
в памяти. Объявление переменной просто сообщает компилятору о том, что где-
либо там, в программе, может встретиться переменная с таким-то именем и тако-
го-то типа. Переменная определяется, когда под нее в памяти резервируется мес-
то, которое способно содержать любое ее значение. Определение, так сказать,
создает реальную переменную.
Большинство объявлений являются также и определениями. Точнее, единст-
венным объявлением простой переменной, не являющимся определением, явля-
ется объявление с использованием зарезервированного слова extern (без ини-
циализации):
int SomeVar; //объявление и определение в одном флаконе
extern int someVar; //только объявление
Можно догадаться, что глобальная переменная может быть определена толь-
ко один раз во всей программе, независимо от того, из скольких файлов она со-
стоит.
//файл A
int globalvar; //определение в файле A
//файл В
int globalVar; //как НЕЛЬЗЯ делать: то же определение в
//файле B
Конечно, такие строгости касаются только глобальных переменных. Если
переменные являются локальными по отношению к каким-либо классам или
функциям, то вы можете сколько душе угодно определять одни и те же пере-
менные с одинаковыми именами и типами данных. Не забудьте только разнести
их по разным зонам видимости. Но лучше, конечно, вообще так не делайте —
переменные с одинаковыми именами ничего, кроме сумятицы, в программу не
вносят.
Как же обеспечить доступ к глобальным переменным, находящимся в одном
файле, из другого? Тот факт, что компоновщик будет воротить нос при попытке
определения одной и той же глобальной переменной в разных файлах, еще не
означает, что она будет отовсюду видна. Все-таки переменную нужно объявлять во
всех файлах, в которых она используется. Например, такой вариант не подходит:
//файл A
int globalVar; //Определение в файле A
//файл В
globalVar=3; //НЕЛЬЗЯ! globalVar тут никто не знает
Компилятор справедливо заметит, что globalVar — неидентифицированный
идентификатор.
Чтобы то, что мы так рекламировали в начале этого пункта, было действи-
тельно правдой, то есть чтобы переменная, определенная в одном файле, была
видна в другом, нужно объявлять ее во всех остальных файлах с помощью заре-
зервированного слова extern.
//файл A
int globalVar; //Определение в файле A
//файл В
extern int globalVar; //Объявление в файле B
globalVar = 3; //Вот теперь все хорошо
Как вы уже, наверное, догадались, объявление глобальной переменной в фай-
ле A сделало ее видимой в файле B, Зарезервированное слово extern означает,
что объявление в данном случае — это только объявление, ничего более. Оно
просит компилятор, который в каждый момент времени видит только один файл,
не обращать внимание на то, что переменная globalVar не определена в файле B.
Компоновщик, который с высоты своего особого статуса обозревает все файлы
проекта, позаботится об установке ссылки на переменную, не определенную
в данном файле.
Необходимо помнить об одном, возможно, неожиданном, ограничении: пере-
менную нельзя инициализировать в объявлении с extern. Выражение
extern int globalVar = 27; //не то, что вы имеете в виду
заставит компилятор думать, что вы хотите определить globalVar, а не просто объ-
явить ее. То есть он просто проигнорирует слово extern и сделает из объявления
определение. Если эта же переменная где-то в другом файле уже определена, то
компоновщик выдаст ошибку повторного определения.
Но что, если вам действительно никак не прожить без глобальных перемен-
ных с одинаковыми именами в разных файлах? В этом случае необходимо опре-
делять их с помощью зарезервированного слова static. Тем самым область види-
мости глобальной переменной сужается до файла, в котором она определена.
Другие переменные с тем же именем могут в других файлах использоваться без
ограничений,
//файл A
static int globalVar; //определение: переменная видна
//только в A
//файл В
static int globalVar; //определение: переменная видна
//только в В
Хотя определены две переменные с одинаковыми именами, конфликта не воз-
никает. Код, в который входит обращение к globalVar, будет обращаться только к
переменной, определенной в данном файле. В этом случае говорят, что статиче-
ская переменная имеет внутреннее связывание. Нестатические глобальные пере-
менные имеют внешнее связывание. (Как мы увидим чуть позднее, для сужения
области видимости до данного файла может использоваться пространство имен.)
В многофайловых программах мы рекомендуем делать глобальные перемен-
ные статическими, если по логике работы к ним не требуется доступ более чем
из одного файла. Это предотвратит возникновение ошибки, связанной со слу-
чайным заданием того же имени переменной в другом файле. К тому же лис-
тинг в этом случае становится более прозрачным — не нужно заботиться о про-
верке разных файлов на предмет совпадения имен.
Обратите внимание, что зарезервированное слово static имеет несколько зна-
чений в зависимости от контекста. В главе 5 «Функции» мы обсуждали, что ког-
да static изменяет локальную переменную (то есть определенную внутри функ-
ции), изменяется ее продолжительность жизни от функции до всей программы,
но видимость сохраняется неизменной, ограниченной функцией. Как уже говори-
лось в главе 6 «Объекты и классы», компонентные данные статических классов
имеют одинаковое значение для всех объектов, а не для каждого — свое. Однако
в контексте глобальных переменных слово static просто сужает область видимо-
сти до одного файла.
Переменная, определенная с помощью const, в общем случае не видна за пре-
делами одного файла. В этом смысле она такая же, как static. Но ее можно сде-
лать видимой из любого файла программы и для определения, и для объявле-
ния с помощью слова extern:
//файл A
extern const int conVar2 = 99; //определение
//файл В
extern const int conVar2; //объявление
Здесь файл В будет иметь доступ к переменной conVar2, определенной в фай-
ле A. Компилятор различает объявление и определение константы по наличию
или отсутствию инициализации.
Межфайловые функции
Мы все помним, что объявление функции задает ее имя, тип возвращаемых дан-
ных и типы всех аргументов. Определение функции — это объявление плюс тело
функции (тело функции — код, содержащийся внутри фигурных скобок).
Когда компилятор генерирует вызов функции, ему, в общем-то, незачем знать,
как она работает. Все, что нужно знать, это ее имя, тип и типы ее аргументов.
Все это есть в объявлении функции. Поэтому запросто можно определить функ-
цию в одном файле, а создавать ее вызовы из другого. Никаких дополнительных
разрешений (типа слова extern) не требуется. Нужно только объявить функцию
в том файле, откуда будет производиться вызов.
//файл A
int add(int a, int b) //определение функции
{return a+b; } // (с телом функции)
//файл В
int add(int,int); //обьявление функции (без тела)
...
int answer = add(2, 3); //вызов функции
Зарезервированное слово extern с функциями не используется, так как ком-
пилятор в состоянии отличить определение от объявления по наличию или от-
сутствию тела функции.
Можно совершенно случайно объявить (не определить!) функцию или дру-
гой элемент программы несколько раз. Компилятор сохраняет спокойствие, но
только до тех пор, пока объявления не противоречат друг другу.
//файл A
int add(int,int); //объявление функции (без тела)
int add(int,int); //второе объявление. Пусть будет.
Подобно переменным, функции могут быть сделаны невидимыми для других
файлов. Для этого при их объявлении используется то же самое слово static.
//файл A
static int add(int a, int b) //определение функции
{return a+b; }
//файл В
static int add(int a, int b) //другая функция
{return a+b; }
Этот код создает две разные функции. Ни одна из них не видна вне того
файла, в котором она определена.
Межфайловые классы
Классы отличаются от простых переменных тем, что определение класса не
подразумевает резервирования памяти. Оно разве что информирует компиля-
тор о том, что именно входит в класс. Это примерно как оговаривать, сколько
байтов резервировать под переменную типа int, с той лишь разницей, что ком-
пилятор уже знаком с int, но не знаком с типом someCLass, пока вы его не опре-
делите.
Определение класса содержит определения или объявления всех его членов:
class someClass //определение класса
{
private:
int memVar; //определение компонентной переменной
public:
int memFunc(int, int); //объявление метода
};
Компоненты класса должны быть объявлены, но не обязательно определены.
Как известно, определения методов помешаются вне класса и идентифицируют-
ся с помощью оператора разрешения контекста.
Объявление класса лишь говорит о том, что то или иное имя принадлежит
данному классу. В нем ничего не сообщается компилятору о компонентах класса,
class someClass; //объявление класса
Не путайте определение класса с определением (созданием) объекта класса:
someClass anObJ;
В отличие от определения класса, определение объекта подразумевает резер-
вирование памяти для его размещения,
Классы ведут себя в межфайловых отношениях не так, как переменные и
функции, Чтобы иметь доступ к классу из любого файла, входящего в програм-
му, необходимо определять (а не просто объявлять) класс в каждом файле,
Определение класса в файле A и объявление его в файле B не означает, что
компилятор сможет создать в файле B объекты данного класса.
Почему же все так строго с классами? Дело в том, что компилятору необхо-
димо знать тип данных всего, что он компилирует. Объявления для переменных
достаточно потому, что в нем указывается уже известный тип.
//объявление
extern int someVar; //видя объявление.
someVar = 3; // компилятор может обработать это.
Объявление функции тоже рассказывает компилятору обо всех типах исполь-
зуемых данных.
//объявление
int someFunc(int, int); //видя объявление.
var1 = someFunc(var2,var3); //компилятор может обработать это.
Что касается класса, то требуется именно его определение, чтобы указать все
типы используемых данных и методов.
//определение
class someClass //видя определение, компилятор
{
private:
int memVar;
public:
int memFunc(int, int);
};
someClass someObj; //может обработать это
v1 = someObj.memFunc(v2, v3); //и это
Одного объявления, как видите, недостаточно компилятору для генерации
кода, который мог бы работать с объектами класса (за исключением указателей
и ссылок на объекты).
Невозможно определить класс дважды в одном исходном файле, но каждый
исходный файл может иметь свое определение того же класса. На самом деле,
конечно, файлу нужно определение класса, только если в нем этот класс исполь-
зуется. В следующем параграфе мы покажем более правильный способ межфай-
ловой коммуникации классов — с использованием заголовочных файлов.
12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
