5.7. Модульная организация программы

    По окончании этой главы должно сложиться более-менее цельное представление о программе, написанной в традиционной технологии программирования: это иерархия типов данных, порожденных ими переменных и набор вызывающих друг друга функций. При небольшом объеме проекта все это можно «свалить в кучу». Однако имеющиеся средства организации процесса трансляции в Си (см.1.7) позволяют навести здесь элементарный порядок. Посмотрим, как в традиционной технологии приживается принцип модульной организации программы – разделение исходного текста на отдельные файлы с независимой трансляцией и последующей сборкой (компоновкой). Но для начала необходимо разобраться со свойствами переменных и функций, имеющих отношение к модульности.

    Прежде всего, в модульном проектировании программист сталкивается с двумя принципиально различными по назначению конструкциями, которые в Си синтаксически похожи: определение и объявление.

    Определение – это текстовое описание объекта программы (переменной, функции, класса), по которому транслятор создает его внутреннее (двоичное) представление (размещает в памяти).

    Определение объекта должно производиться в программе только один раз, а соответствующий ему внутренний программный код размещается в создаваемом при трансляции объектом модуле.

    Объявление – сообщение транслятору об имени и свойствах объекта программы (переменной, функции), который в данной точке недоступен (неизвестен транслятору), но который используется программой.

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

    Понятие доступности (известности, видимости) опять-таки относит нас к организации процесса трансляции. Поэтому можно указать, как минимум, две причины, по которым возникает необходимость в объявлениях:

    • определение переменной или функции находится в другом модуле (файле);

    • определение переменной или функции находится «вниз по тексту». Большинство трансляторов «не заглядывает вперед» в поисках определений, поэтому если обращение производится до определения объекта, то он нуждается в дополнительном объявлении.

    Время жизни и область действия переменных

    В зависимости от способа и места своего определения переменные обладают рядом свойств, от которых зависит функциональное назначение переменной в программе. Это время жизни и область действия.

    Время жизни переменной - интервал времени работы программы, в течение которого переменная существует, для нее отведена память, и она может быть использована. При этом время существования переменной тесно связано с местом ее размещения в одном из сегментов программы (см.1.2). Возможны три случая:

    1. переменная создается функцией в стеке в момент начала выполнения функции и уничтожается при выходе из нее, переменная существует «от скобки до скобки». Местом размещения таких переменных является стек программы (сегмент стека) ;

    2. переменная создается транслятором при трансляции программы и размещается в программном модуле - такая переменная существует в течение всего времени работы программы, то есть «всегда». Местом размещения служит сегмент статических (глобальных) данных;

    3. переменная создается и уничтожается работающей программой в те моменты, когда она «считает это необходимым» - динамические переменные (см.5.6). Строго говоря, динамические переменные в Си не управляются транслятором: этим занимается библиотека динамического распределения памяти (в классическом Си она вообще вынесена за пределы языка). Тем не менее, с точки зрения использования в программе они являются вполне «настоящими». Местом их размещения служат сегменты динамических данных, в которых организуется «куча».

    Область действия (видимости) переменной - та часть программы, в которой эта переменная может быть использована, то есть является доступной. Областью действия переменной могут быть:

    1. тело функции или блока, то есть «от скобки до скобки»;

    2. текущий модуль от места определения или объявления переменной до конца модуля, то есть в текущем файле;

    3. все модули программы, проект в целом.

    Прежде чем перечислить виды переменных (классы памяти), имеющиеся в Си, отметим синтаксическую разницу между определением и объявлением переменной.

    Определение переменной – обычное контекстное определение, задающее тип, имя переменной, производящее инициализацию. При трансляции определения вычисляется размерность и резервируется память. Размерность массивов в определении обязательно должна быть задана.

    int a = 5, B[10] = { 1,6,3,6,4,6,47,55,44,77 };

    Объявление переменной имеет синтаксис определения переменной, предваренный словом extern. В нем задается тип и имя переменной, запоминается факт наличия переменной с указанным именем и типом. Размерность массивов в объявлении может отсутствовать.

    extern int a, B[];

    Итак, в зависимости от сочетания основных свойств – времени жизни и области действия – имеют место переменные различных видов (различных классов памяти).

    Автоматические переменные (свойства 1,1). Создаются при входе в функцию или блок и имеют областью действия тело той же функции или блока. При выходе - уничтожаются. Место хранения - стек программы. Инициализация таких переменных заменяется обычным присваиванием значений при их создании. Если функция рекурсивна, то на каждый вызов создается свой набор таких переменных. Общепринятый термин для таких переменных - локальные. Термин «автоматические» характеризует особенность их создания при входе в функцию, то есть время жизни.

    Синтаксис определения: любая переменная, определенная в начале тела функции или блока, по умолчанию является автоматической.

    Внешние переменные (свойства 2,3). Создаются транслятором и имеют областью действия все модули программы. Размещаются транслятором в объектном модуле, а затем компоновщиком в программном файле (сегменте данных) и инициализируются там же. Термин «внешние» характеризует доступность этих переменных из других модулей, или область действия. Общепринятый термин для таких переменных - глобальные.

    Синтаксис определения: любая переменная, определенная вне тела функции, по умолчанию является внешней.

    Несмотря на то, что внешняя переменная потенциально доступна из любого модуля, сам факт ее существования должен быть известен транслятору. Если переменная определена в модуле, то она доступна от точки определения до конца файла. В других модулях требуется произвести объявление внешней переменной.

    CPP
    //-------------------------------------------------------------------------
    //  Файл a.cpp  - определение переменных      
    
    int         a,B[20]={1,5,4,7};
    
    ... область действия ...
    
    //-------------------------------------------------------------------------
    
     //  Файл b.cpp - объявление внешних переменных
    
    extern int a,B[];    
    
    ... область действия ...

    Определение переменной должно производиться только в одном модуле, при трансляции которого она создается и в котором размещается. Соответствие типов переменных в определении и объявлениях транслятором не может быть проверено. Ответственность за это соответствие ложится целиком на программиста.

    Статические переменные. Имеют сходные с внешними переменными характеристики времени жизни («всегда») и размещения в памяти (сегмент данных), но ограниченную область действия.

    Синтаксис определения: аналогичен внешним или автоматическим переменным, но предваряется словом static.

    Собственные статические переменные функции (свойства 2,1) определяются аналогично автоматическим переменным внутри блока. Область действия также аналогична - тело функции или блок. При рекурсивном вызове функции не дублируются. Назначение собственных статических переменных - сохранение значений, используемых функцией, между ее вызовами.

    Статические переменные, определенные вне функции (свойства 2,2) имеют область действия, ограниченную текущим модулем. Они предназначены для создания собственных переменных модуля, которые не должны быть «видны» извне, чтобы не вступать в конфликт с одноименными внешними переменными в других модулях.

    Определение и объявление функций

    Функция обладает только одним свойством, да и то в ограниченных вариациях. С точки зрения области действия все функции по умолчанию являются внешними (т.е. глобальными). Это значит, что определенная в одном месте функция может вызываться во всем проекте. Однако, если она «не видна» из текущей точки, то дополнительно требуется задать ее объявление.

    Указанное свойство приводит к тому, что любая функция, даже созданная «для внутреннего употребления» попадает в пространство имен проекта, поэтому в других модулях определить функции с таким именем уже нельзя. Чтобы не засорять проект лишними именами, рекомендуется определять функции как статические. Это делается добавлением слова static перед заголовком функции. В результате чего область действия функции сужается и она становится доступной (видимой) только в текущем модуле (файле).

    Синтаксис определения и объявления функций отличается значительным разнообразием, поэтому требует отдельного рассмотрения.

    Определение функции – обычное задание функции в программе в виде заголовка и тела, по которому она транслируется во внутреннее представление в том модуле, где встречается.

    Объявление функции – информация транслятору о наличии функции с заданным заголовком (прототипом) либо в другом модуле, либо далее по тексту текущего модуля - «вниз по течению». Объявление функции состоит из прототипа, предваренного словом extern, либо просто из прототипа функции.

    Прототип функции – заголовок функции со списком формальных параметров, заданных в виде абстрактных типов данных.

    C
    int         clrscr();                                     // без контроля соответствия (анахронизм)
    
    int         clrscr(void);                                // без параметров
    
    int         strcmp(char*, char*);
    
    extern int strcmp();                                 // без контроля соответствия (анахронизм)
    
    extern int strcmp(char*, char*);

    Организация проекта в классической технологии

    Возможность объявлений переменных и функций позволяют любым способом разбить программу на модули (файлы), используя взаимные объявления. Но все-таки желательно, чтобы структура проекта отражала логическую организацию программы. На этот счет имеются следующие рекомендации:

    • модуль (файл) должен содержать определения внешних (глобальных) переменных и функций, имеющих общее назначение, работающих с одинаковыми структурами (и типами) данных. Под такое определение подходит понятие библиотека функций. Возможны и другие основания для объединения;

    • определения типов данных, используемых переменными и функциями, локализованными в данном модуле, выделяются в одноименный заголовочный файл, который размещается в каталогах проекта там же, где основной cpp-модуль. Основное требование к заголовочному файлу – он не должен содержать конструкций, порождающих программный код (определений переменных и функций);

    • для того, чтобы обратиться к объекту (например, вызвать функцию) из другого модуля, необходимо директивой include подключить из текущего каталога заголовочный файл того модуля, где локализован объект.

    В результате получим примерно такую картину. Сразу же заметим, что использование глобального динамического массива «не есть хорошо»: он приведен здесь в иллюстративных целях. На практике лучше «скрывать» собственные данные модуля, определяя их как static. Отдельные, несистематизированные объявления можно делать вне заголовочных файлов.

    CPP
    //---------------------------------------------------------------------------------- MyLib.h
    // Заголовочный файл библиотеки
    struct MyData { // Определение типов данных};
    
    typedef MyData *PData, MData[];
    
    extern PData DM;                                  // Объявление собственных переменных
    
    extern int n,sz;                                      // - динамический массив
    
    extern void add(PData);                          // Объявление функций модуля
    
    extern PData remove();              
    
    //---------------------------------------------------------------------------------- MyLib.cpp
    
    // Модуль исходного текста библиотеки
    
    #include “MyLib.h”                                  // Подключение собственного заголовочника
    
    PData DM;                                            // Определение собственных переменных
    
    int n,sz;                                                            // - размерности динамического массива
    
     
    
    void add(PData pp){…тело…}                  // Определение функций
    
    PData remove()  {…тело…}                    
    
    //---------------------------------------------------------------------------------- foo.cpp
    
    // Набор разнородных функций
    
    #include “MyLib.h”                                  // Подключение заголовочника
    
    void fufu(){                                              // для определения ТД PData
    
    PData q=new MyData;                           // и вызова библиотечных функций
    
    add(q); }
    
    void gogo(){fufu(); }
    
    //---------------------------------------------------------------------------------- main.cpp
    
    // Основной модуль
    
    #include “MyLib.h”                                  // Подключение заголовочника
    
    void main(){                                            // для вызова библиотечных функций
    
    extern void gogo();                                  // Отдельное объявление для внешней функции
    
    void where_you();                                   // Объявление для ссылки впередgogo();                                                  // Вызов внешней функции
    
    where_you();                                          // Вызов функции «вниз по течению»
    
    }
    
     
    
    void where_you(){}

    Вопросы без ответов

    Определите, где задано определение, объявление и вызов функции.

    CPP
    //------------------------------------------------------57-01.cpp
    
    //------------------------------------------------------1
    
     void F(void) { puts("Hello, Dolly"); }
    
     //------------------------------------------------------2
    
     void F(void) { puts("Hello, Dolly"); }
    
     void G(void){ F(); }
    
     //------------------------------------------------------3
    
     void F(void);
    
     void G(void){ F(); }
    
     //------------------------------------------------------4
    
     void G(void){
    
     void F(void);
    
     F(); }
    
     void F(void) { puts("Hello, Dolly"); }
    
     //------------------------------------------------------5
    
     void F(void);
    
     void G(void){ F(); }
    
     void F(void) { puts("Hello, Dolly"); }
    
     //------------------------------------------------------6
    
     extern void F(void);
    
     void G(void){ F(); }