Неактивна зіркаНеактивна зіркаНеактивна зіркаНеактивна зіркаНеактивна зірка
 

Розділ 10. Модульне програмування

10.1. Основні поняття

Програми, що вирішують реальні практичні задачі, мають великий обсяг та складаються з тисяч функцій. Записувати всі функції програми в один файл незручно, оскільки зі збільшенням програми в ньому стає важко орієнтуватися, а також стає неможливою спільна робота кількох програмістів.

Технологія модульного програмування полягає у розбитті великої задачі на частини, з наступною реалізацією кожної підзадачі в окремому файлі, який називають модуль. На практиці один програмний продукт може включати модулі, написані різними мовами програмування. На вибір мови впливає складність реалізації алгоритму.

Для того, щоб зрозуміти принцип побудови багатомодульної програми, спочатку потрібно уявити велику програму, весь код якої міститься в одному файлі. Оскільки будь-яка функція повинна бути оголошена перед її першим викликом, а кожна функція програми може викликати будь-яку іншу функцію, то на самому початку програми повинні бути розміщені оголошення всіх функцій, крім функції main.

Нехай цю ж програму розбито на кілька модулів, тобто частину функцій перенесено в один файл, частину інших функцій – у другий і т.д. Функція, розміщена в одному модулі, може викликати функцію, що міститься в іншому модулі. А це означає, що на початку кожного модуля повинні міститись оголошення (прототипи) всіх зовнішніх функцій – функцій з інших модулів, які викликаються з цього модуля.

Прототипи зовнішніх функцій можна записувати у текст модуля вручну, але при великій кількості функцій це дуже незручно. Набагато зручніше перенести прототипи функцій в окремий файл і за потреби підключати його до відповідного модуля за допомогою директиви #include. Такий файл називається заголовочний і має розширення .h.

Створюючи модуль, програміст в один файл з розширенням .c записують тіла функцій, а в інший файл з розширенням .h (заголовочний) – лише прото­типи цих функцій. Про заголовочний файл часто кажуть, що він містить інтерфейс модуля, а про -файл, що він є реалізацією модуля.

Трансляція багатомодульної програми має важливу особливість: компілятор опрацьовує багатомодульну програму не як одне ціле, а окремо модуль за модулем. Модулі компілюю­ться незалежно один від одного, тобто поки компілятор компілює один модуль, він “не знає” про існування інших модулів.

У наслідок цього з кожного -файлу створюється об’єктний файл (переважно він має таке ж ім’я, що й відповідний -файл, але з розширенням .obj). В об’єктному файлі операції над даними та оператори управління зберігаються у машинному коді, при цьому виклики функцій залишаються нерозв’язаними.

Приклад 1.1. Об’єктний файл a.obj містить виклик функції int f(int), який немо­жливо перетворити на машинну команду переходу до тіла функції, тому що модуль а.obj нічого не знає про об’єктний файл b.obj, в якому міститься тіло відповідної функції.

Після компіляції модулів спрацьовує ще одна спеціальна програ­ма – редактор зв’язків, яка об’єднує всі об’єктні файли, що входять до складу програми, в один виконуваний файл, розв’язуючи при цьому посилання на імена функцій, заміня­ючи виклик функції за ім’ям, яке міститься в одному модулі, на конкретну адресу її тіла, взятого з іншого модуля.

10.2. Включення файлів

У кожній програмі на мові С для підключення заголовочного файла використовується директива #include.

Синтаксис директиви такий: після слова #include записується ім’я файла з розширенням, взяте у кутові дужки або у подвійні лапки.

Директива #include <filename.h> повідомляє транслятор, що файл, ім’я якого вказано в кутових дужках, потрібно шукати у спеціальній директорії, призначеній для стандартних заголовочних файлів. Са­ме тому в кутових дужках вказують імена таких заголовочних файлів, як stdio.h, math.h та інших, які постачаються разом з транслятором.

Директива #include "filename.h" означає, що транслятор повинен шукати цей файл в директорії для користувацьких заголовочних файлів, у тому числі, у тій же директорії, де знаходиться текст модуля.

Сенс директиви #include полягає у наступному: зустрівши в будь-якому місці програми директиву #include, транслятор шукає файл з відповідним ім’ям, та підставляє весь його вміст замість директиви #include.

Приклад 1.2.

Нехай задано заголовочний файл mylib.h такого вмісту:

typedef unsigned int un_int;

#define MAX_LEN 255

un_int f(int* k, un_int x);

та файл mycode.c такого вмісту:

void g(int);

#include "mylib.h" /* підключення модуля */

/* визначення функції main() */

int main() {

       int m[MAX_LEN];

       un_int s, r = 0;

       s = un_int f(m, r) ;

       /* інші оператори */

       return 0;

}

Коли компілятор буде опрацьовувати файл mycode.c, то, прочитавши другий рядок коду #include "mylib.h"), замінить його на вміст файла mylib.h. Після такої підстановки компілятор “подумає”, що у файла mycode.c такий вміст:

void g(int);

typedef unsigned long int un_int;

#define MAX_LEN 255

un_int f(int* k, un_int x);

/* визначення функції main() */

int main() {

       int m[MAX_LEN];

       un_int s, r = 0;

       s = un_int f(m, r) ;

       /* інші оператори */

       return 0;

}

Процес роботи директиви #include такий: транслятор зчитує файл рядок за рядком; коли він бачить у тексті директиву #include, то тимчасово призупиняє опрацьовування поточного файла; шукає файл, ім’я якого вказано після слова include, і починає так само його опра­цьовувати; дійшовши до кінця цього файла, транслятор повертається до недочитаного файла і продовжує опрацьовувати його з місця зупинки.

Стандарт та означення мови С не містять правил щодо імен та розширень заголовочних файлів. Транслятор не пере­віряє, чи знаходяться у підключеному файлі оголошення або інші конструкції мови С. Єдине, що транслятор вимагає, це щоб у підключеному файлі був текст, правильний, з точки зору граматики мови програмування С. У директиві include можна вказати ім’я файла з будь-яким роз­ширенням, проте модулям прийнято задавати розширення .h.

У заголовочних файлах прийнято розміщувати оголошення, потрібні в основному тексті програми.

Правила мови С дають змогу розбити довгий програмний текст на кілька частин і кожну з них розмістити в окремому файлі, а в ще одному файлі зібрати всі ці частини разом за допомогою директив include. Але, незважаючи на те, що створені таким чином програми нормально компілюються і працюють, у жодному разі не можна за­стосовувати цю “технологію”, оскільки вона грубо суперечить нормам доброго стилю програмування.

Типова помилка полягає у тому, що поняття “модульність” підміняють включенням частин програмного коду з різних файлів. Модулі повинні компілюватися незалежно один від одного, щоб програма могла збиратися з цих частин. При помил­ковому підході всі частини програмного коду компілюються разом.

У заголовочних файлах переважно розміщують прототипи функцій, оголошення типів (особливо структур), означення макросів, тобто всі оголошення, що використовуються на етапі компіляції, але в жодному разі не можна розміщувати опис тіл функцій.

10.3. Проблема повторного включення

На практиці можливі ситуації, коли один заголовочний файл безпосередньо чи через інші файли підклю­чається до певного модуля більше ніж один раз. Це означає, що кожне оголошення цього заголовочного файлу потрапить у текст програми кілька разів, а це може викликати помил­ку компіляції.

Розглянемо приклад. Нехай у заголовочному файлі myType.h оголошено лише один структурний тип TNumber:

typedef struct TNumber {

int m, n;

} Number;

Нехай маємо ще два заголовочні файли, в яких оголошуються функції для роботи з об’єктами цього структурного типу:

  • файл number_io.h містить оголошення функцій для введення та виведення об’єктів:

#include "myType.h"

Number number_read();

void number_print(Number);

  • файл number_m.h містить оголошення функцій для математичних обчислень:

#include "myType.h"

Number number_add(Number, Number);

Number number_mpy(Number, Number);

У першому рядку кожного з цих заголовочних файлів підключається файл myType.h.

Модуль, що використовує ці файли має таку структуру:

#include "number_io.h"

#include "number_m.h"

/* програмний код */

Розглянемо, як транслятор зчитає цей текст. Натрапивши на перший рядок, він замість цього рядка підставляє вміст файла number_io.h та пробує опрацювати його, але в цьому файлі є директива включення файлу myType.h, тому транслятор ще раз підставляє вміст цього файлу. Так само, опрацьовуючи другий рядок, транслятор змушений ще раз вставити текст файлу myType.h. Отже, з точки зору транслятора, модуль після опрацювання всіх директив виглядатиме так:

typedef struct TNumber {

int m, n;

} Number;

Number number_read();

void number_print(Number);

typedef struct TNumber {

int m, n;

} Number;

Number number_add(Number, Number);

Number number_mpy(Number, Number);

/* програмний код */

Під час компіляції модуля транслятор побачить два означення структурного типу Number, що є помилкою, з точки зору граматики мови С.

1.4. Умовна компіляція

Мова С містить директиви препроцесора #ifdef та #ifndef, називаються директивами умовної компіляції. На етапі компіляції залежно від певної умови вони дають змогу вилучити або включити певний фрагмент тексту у код програми.

Конструкцію

#ifdef <ім’я>

/* певний програмний текст */

#endif

компілятор опрацьовує у такому порядку: спочатку він перевіряє, чи був десь раніше у цьому модулі оголошений макрос з таким же ім’ям; якщо ні, то весь програмний текст між #ifdef та #endif пропускається і не компілюється. Директива #ifndef працює навпаки: вона вилучає з компіляції текст між #ifndef та #endif, якщо макрос з таким ім’ям вже оголошений.

Проблема повторного включення (див. попередній пункт) вирішується додаванням у модуль myType.h директиви #ifndef:

#ifndef _myType_H_

#define _myType_H_

typedef struct TNumber {

int m, n;

} Number;

#endif

Нехай файл myType.h, як і раніше, підключається до модуля через інші заголовочні файли двічі. При першому включенні компілятор, натрапивши на директиву ifndef, продовжить компіляцію, оскільки макрос з іменем _myType_H_ ще не був означений. Під час другого включення того ж заголовочного файлу компілятор помітить, що макрос _myType_H_ вже є, тому опрацьовуючи директиву ifndef,  пропустить весь вміст файлу myType.h.

Тобто тепер після опрацювання всіх директив модуль виглядатиме так:

typedef struct TNumber {

int m, n;

} Number;

Number ratio_read();

void ratio_print(Number);

Number ratio_add(Number, Number);

Number ratio_mpy(Number, Number);

/* певний програмний текст */

1.5. Зовнішні змінні

Єдиним оголошенням простої змінної, яке не є визначенням, є оголошення з використанням зарезервованого слова  extern (без ініціалізації):

int a;       // Оголошення і визначення змінної

extern int b;       // Лише оголошення змінної

Глобальну змінну можна визначити у програмі лише один раз, незалежно від того, скільки файлів підключається у програмі. Локальні змінні з одна­ковими іменами можуть бути визначені у різних областях видимості. Але вико­ристовувати такі змінні, навіть у різних блоках програми, не рекомендовано, оскільки це може призвести до помилок.

// Файл А

int X;         // Визначення змінної Х у файлі А

// Файл В

int X;         // НЕПРАВИЛЬНО: визначення такої ж змінної Х вже є у файлі А

Наступний запис також є неправильним, оскільки у файлі В змінна Х невідома:

// Файл А

int X;           // Визначення змінної Х   у файлі А

// Файл В

X=3;                    // НЕПРАВИЛЬНО, оскільки змінна Х у файлі В є невідома

Щоб глобальна змінна, визначена в одному файлі, була видимою в усіх файлах програми, її слід оголосити із зарезервованим словом  extern:

// Файл А

int X;        // Визначення змінної Х у файлі А

// Файл В

extern int X; // Визначення змінної Х у файлі B

X=3;

Тут оголошення змінної Х у файлі А зробило її видимою у файлі В. Зарезервоване слово extern означає, що виконується лише оголошення без визначення. Воно спонукає компілятор, який у кожний момент часу бачить лише один з файлів, не звертати уваги на те, що змінна Х невизначена у файлі В.

ОБМЕЖЕННЯ: не можна ініціалізувати змінну зі словом extern при її оголошенні.

Вираз extern int X=27; примусить компілятор вважати, що це не оголошення, а визначення. Тобто він проігнорує слово extern і зрозуміє оголошення як визначення. При цьому якщо змінна вже визначена в іншому файлі, то буде виникне помилка повторного визначення.

Якщо необхідно використовувати дві глобальні змінні з однаковими іменами у двох різних файлах, то їх слід визначати із зарезервованим словом static. Таким чином об­ласть видимості змінної звужується до файла, в якому вона визначена. Інші змін­ні з таким же ім’ям в інших файлах можуть використовуватися без обмежень.

// Файл А

static int X;       // Визначення: змінна Х є видима лише у файлі А

// Файл В

static int X;       // Визначення: змінна Х є видима лише у файлі В

Хоча тут визначені дві змінні з однаковими іменами, конфлікту це не викликає, тому що код, до якого входить звертання до Х, буде звертатись до змінної, визначеної у даному файлі. У такому разі вважають, що статична змінна має вну­трішнє зв’язування. Нестатичні глобальні змінні мають зовнішнє зв’язування.

У багатофайлових програмах глобальні змінні рекомендується робити ста­тичними, якщо за логікою роботи доступ до них вимагається не більше ніж з одного файла. Це допомагає запобігти появі помилки, пов’язаної з випад­ковим використанням того ж самого імені в іншому файлі.

ВАЖЛИВО: зарезервоване слово static має декілька значень залежно від контексту. У контексті глобальних змінних слово static звужує область видимості змінної до одного файла.

Змінна-константа, яка визначається за допомогою слова const, у загальному випадку є невидимою за межами одного файла. У цьому вона схожа до static, але її можна зробити видимою з кожного файла програми за допомогою слова extern:

// Файл А

extern const int X=99;         // Визначення Х

// Файл В

extern const int X;           // Оголошення Х

Тут файл В матиме доступ до константи Х, визначеної в файлі А. Компілятор розріз­няє оголошення та визначення константи за наявністю чи відсутністю ініціа­лізації.

Приклад 1.3. Багатофайлова програма, що використовує користувацький програмний файл.

Програмний файл modul.cpp:

/* Вміст файла modul.cpp */

#include <stdio.h>

// оголошення зовнішніх змінних та констант

extern int a;

extern const int k = 8;

extern const double pi = 3.141593;

// визначення зовнішньої функції

void min(double x, double y) {

if (x>y) printf("x>y");

else if (x==y) printf("x=y");

else printf("x<y");

printf("\nextern a=%d\n\n", a);

}

/* Кінець файла modul.cpp */

Основний файл main.cpp:

/* main.cpp */

#include <stdlib.h>

#include <stdio.h>

// ініціалізації зовнішньої змінної "а"

int a = 4;

// підключення зовнішніх змінних

extern const int k;

extern const double pi;

// оголошення зовнішньої функції

void min(double, double);

/* визначення функції main() */

int main() {

double x=2.334, y=5.123;

printf("x=%.3f\ty=%.3f\n", x, y);

min(x, y);

printf("k=%d\ta=%d\n", k, a);

min(k, a);

a=1;

printf("a=%d\tpi=%f\n", a, pi);

min(a, pi);

system("pause");

return 0;

}

// Кінець файла main.cpp

Приклад 1.4. Багатофайлова програма, що використовує користувацький заголовочний файл.

Заголовочний файл modul.h:

/* Вміст файла modul.h */

#ifndef MODUL_H_INCLUDED

#define MODUL_H_INCLUDED

#include <stdio.h>

// Оголошення змінних і констант

#define k 8

extern int a;

const double pi = 3.141593;

enum color {blue, yellow};

// Оголошення функцій

void print();

void min2(double, double);

#endif // MODUL_H_INCLUDED

/* Кінець файла modul.h */

Програмний файл modul.cpp:

/* Вміст файла modul.cpp */

#include "modul.h"

// Визначення функцій

void print() {

printf("Function PRINT!\n\n");

}

void min2(double x, double y) {

if (x>y) printf("%.2f>%.2f\n\n", x, y);

else if (x==y) printf("%.2f=%.2f\n\n", x, y);

else printf("%.2f<%.2f\n\n", x, y);

}

/* Кінець файла modul.сpp */

Основний файл main.cpp:

/* Вміст файла main.cpp */

#include <stdlib.h>

#include "modulA.h"

/* визначення функції main() */

int main() {

printf("Main program!\n\n");

print();

double x=2.334, y=5.123;

min2(x, y);

int a = 4;

min2(k, a);

min2(k, pi);

system("pause");

return 0;

}

/* Кінець файла main.сpp */

(Для ознайомлення з повним текстом статті необхідно залогінитись)