Главная->Інформатика та програмування->Содержание->Взаимодействие исходных файлов

Р. ЛАФОРЕ ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ В 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); //и это

 

Одного объявления, как видите, недостаточно компилятору для генерации

кода, который мог бы работать с объектами класса (за исключением указателей

и ссылок на объекты).

Невозможно определить класс дважды в одном исходном файле, но каждый

исходный файл может иметь свое определение того же класса. На самом деле,

конечно, файлу нужно определение класса, только если в нем этот класс исполь-

зуется. В следующем параграфе мы покажем более правильный способ межфай-

ловой коммуникации классов — с использованием заголовочных файлов.

 

11