
Бібліотеки визначень
Бібліотека визначень - це спеціальним чином оформлена сукупність різних описів (констант, змінних, типів, підпрограм тощо), яку можна використовувати усередині будь-якої програми.
Бібліотека визначень не є самостійною програмою: вона не може бути запущена на виконання, а може тільки підключатися для використання у програмі або у іншій бібліотеці. При цьому програмі стають доступними всі описи, що містяться у бібліотеці. Бібліотеки, як правило, містять широкий спектр готових функцій, придатних для різного використання. Наприклад, розробник може запропонувати бібліотеку функцій для статистичної обробки даних, оптимізації роботи з пам’яттю комп’ютера тощо.
Підключення бібліотек до вихідного коду програм виконується відповідно до синтаксису мови програмування. Так у Pascal для цього використовується директива uses, у С/С++ - директива препроцесора #include. Наприклад,
#include <stdio.h> // підключення засобів форматованого введення-виведення С/С++
У системах програмування можуть використовуватися два види бібліотек: стандартні та власні, наприклад,
#include <stdio.h> // підключення стандартної бібліотеки С/С++
#include “mylib.h“ // підключення власної бібліотеки С/С++
Визначення даних та оголошення підпрограм в бібліотечних файлах називають інтерфейсами (interface); коди підпрограм, що входять до бібліотек визначень, - реалізацією (implementation). Якщо інтерфейсну частину можна назвати фасадом будівлі, то реалізація являє собою її внутрішню частину.
Інтерфейс та реалізація бібліотек визначень можуть міститися як в одному файлі, так і у різних файлах. Наприклад, у Pascal інтерфейсний розділ та розділ реалізації є складовими єдиного бібліотечного модуля. У С/С++ визначення даних і прототипи функцій задаються у заголовних файлах (.h-файлах), а коди функцій - у срр-файлах.
Інтерфейс - це та частина бібліотеки, яку програміст бачить і за допомогою якої він взаємодіє з бібліотекою. При цьому йому зовсім не обов’язково знати як реалізовані тіла підпрограм. До того ж, розробники бібліотек у більшості випадків просто не надають клієнтам вихідних кодів, щоб їх не можна було нелегально використати чи змінити. Тому коди підпрограм (реалізація бібліотек) найчастіше поставляються вже у скомпільованому вигляді - у вигляді об’єктних (.obj) або бібліотечних (.lib) файлів. Для подальшого використання усі .obj- та .lib-файли компонуються в єдиний виконуваний файл.
Заголовні файли
Як зазначалося вище, заголовні файли (.h-файли) у С/С++ містять інтерфейсну частину визначень. Реалізацію ж функцій подають у файлах .cpp. Тобто, для кожного cpp-файла створюється власний заголовний файл з таким самим ім’ям і розширенням h і, навпаки, здебільшого, для кожного заголовного файла створюється cpp-файл з реалізацією функцій, оголошених у h-файлі.
При підключенні до програми заголовного файла його текст (а з ним і текст відповідного cpp-файла) автоматично вставляється до програми замість рядка з відповідною директивою #include. При компіляції програми всі підключені заголовні файли з файлами їхньої реалізації перекомпільовуються, тому підключення надмірної кількості заголовних файлів уповільнює компіляцію програми.
Заголовні файли поділяють на стандартні і створювані програмістом. Імена стандартних заголовних файлів пишуться у кутових дужках “<” і “>”, наприклад:
#include <math.h> // підключення заголовного файла з математичними функціями
Окрім того, для таких файлів іноді можна не зазначати розширення .h (наприклад, для <iostream>).
Заголовні файли, створені програмістом, зазвичай розташовують у теці проекту. Імена цих файлів у директиві #include пишуться у подвійних лапках і завжди з розширенням h.
Заголовний файл може містити:
оголошення типів (наприклад, typedef double arr [14]; );
оголошення (прототипи) функцій (наприклад, extern int strlen(const char*); );
визначення вбудованих функцій (наприклад, inline char get() { return *p++; } );
оголошення даних (наприклад, extern int a; );
визначення констант (наприклад, const float pi = 3.141593; );
перерахування (наприклад, enum bool { false, true}; );
команди підключення файлів (наприклад, #include <math.h> );
макровизначення (наприклад, #define n 7 );
коментарі (наприклад, /* Перевірка на кінець файла */ ).
Перелік того, що саме слід розміщувати в заголовному файлі, не є вимогою мови С/С++, це є лише порадою розумного використання підключення файлів.
З іншого боку, в заголовному файлі ніколи не повинно бути:
визначення функцій (наприклад, char get() { return *p++; } );
визначення даних (наприклад, int a; );
визначення складених констант (наприклад, const b[i] = { /* ... */ }; ).
Повернемося до попереднього прикладу.
У файлі tabular.h зробимо потрібні оголошення змінних і функцій.
// --------------------------------------- tabular.h - заголовний файл -------------------------------------------
#include<iostream>
#include<stdio.h>
#include<math.h> // підключення математичної бібліотеки
using namespace std;
//=================== визначення глобальних змінних================
float pi=3.14159;
//======================== прототипи функцій================
void Print(float, float, float, float (*)(float), char []);
У файлі tabular.cpp пропишемо визначення функцій, прототипи яких розміщено у файлі tabular.h.
//------------------------------- tabular.cpp. - файл реалізації функцій --------------------------------
#include<iostream>
#include<stdio.h>
#include<math.h> // підключення математичної бібліотеки
using namespace std;
//============== табулювання функції *func ============
void Print(float a, float b, float h, float (*func)(float), char s[]) /* a, b - межі відрізка,
h -відстань між точками, s - текстова назва функції*/
{ float x; //значення аргументу функції *func
cout<<"=================="<<endl; //виведення заголовка таблиці
cout<<" x | "<<s<<endl;
cout<<"=================="<<endl;
x=a;
while (x<b) //поки не досягнуто правої межі відрізка
{ printf("%6.2f |",x);
printf("%8.4f \n", (*func)(x)); //обчислити і вивести значення функції
x+=h;
}
}
Головний файл проекту арр.cpp:
//-------------------- арр.cpp - файл головної функції, містить виклики функцій ---------------------
#include "tabular.h" // підключення власної бібліотеки
using namespace std;
float lower, upper, step; // межі відрізка, крок
int main()
{ cout<<"Enter lower, upper bounds and step: ";
cin>>lower>>upper>>step;
lower*=pi; upper*=pi; step*=pi;
Print(lower, upper, step, sin, "sin(x)"); //табулювання стандартної функції sin
Print(lower, upper, step, sin, "cos(x)"); //табулювання стандартної функції cos
system("pause");
}
Отже, маємо три файли. Заголовний файл tabular.h містить визначення величини і заголовок функції Print, яка табулює значення вказаних функцій на заданому відрізку, файл tabular.cpp — реалізацію даної функції та необхідні «стандартні» директиви, а файл головна програма арр.cpp — включення tabular.h та головну функцію.
Стандартний заголовний файл в директиві include вказується в кутових дужках, як, наприклад, <iostream>, а файл, створений програмістом — у лапках, як "tabular.h". Кутові дужки говорять про те, що препроцесор має шукати заголовний файл, починаючи з підкаталогу include каталогу з IDE та його підкаталогів, лапки — з каталогу з поточним cpp-файлом. Отже, файли tabular.h та арр.cpp краще розмістити в одному й тому самому каталозі.
Тексти у файлах tabular.cpp та арр.cpp є одиницями трансляції (translation unit), або програмними одиницями. Кожна така одиниця містить послідовність функцій, директив препроцесора та інструкцій оголошень імен. Як правило, С/С++-програма складається з кількох одиниць трансляції.
Кожну одиницю трансляції можна скомпілювати окремо. У нашому прикладі результатом будуть об’єктні файли з розширенням ".obj". Далі за допомогою компонувальника з цих файлів можна зібрати виконуваний код програми. Проте зручніше скористатися засобами системи програмування, призначеними саме для створення багатофайлових програм.
В IDE MS Visual Studio засобами меню створимо новий проект. Типом проекту виберемо Win32 Console Application — консольна програма на платформі Win32. За допомогою меню додамо до проекту вхідні файли tabular.cpp та арр.cpp, а також tabular.h. Далі залишається побудувати виконуваний код (він матиме ім’я проекту з розширенням ".exe").
Список файлів проекту ведеться системою програмування в спеціальному файлі (його ім’я й зміст залежать від конкретної системи). Це значно полегшує розробку багатофайлової програми, особливо, якщо їх багато.
Не можна зберігати визначення змінних та функцій в заголовних файлах, якщо вони використовуватимуться декількома файлами багатофайлової програми. Це приводить до помилок повторних визначень на етапі компонування програми. Подібна проблема виникає і тоді, коли помилково підключають один і той самий заголовний файл двічі, наприклад:
// ----------Файл app.cpp-----------
#include "headone.h"
#include "headone.h"
Припустімо, що є файл з кодом програми app.cpp і два заголовних файли headone.h та headtwo.h. До того ж заголовний файл headone.h підключає до себе файл headtwo.h. Якщо є потреба підключити обидва заголовні файли до app.cpp, слід уважно стежити за тим, щоб не підключити один і той самий файл двічі. Наприклад:
// -----------Файл headtwo.h---------
int х;
// -----------Файл headone.h---------
#include "headtwo.h"
// -----------Файл app.cpp-------------
#include "headone.h"
#include "headtwo.h" // повторне підключення заголовного файла
Оскільки директивою #include заголовні файли підключаються до програми з усім їхнім вмістом, файл app.cpp буде таким:
// -----------Файл app.cpp-------------
. . .
int х; // з headtwo.h через headone.h
. . .
int х; // безпосередньо з headtwo.h
Як наслідок, компілятор виведе повідомлення, що змінну х оголошено двічі.
Для попередження помилок повторних включень визначення у заголовному файлі слід розпочинати з директиви препроцесора:
#if!defined(HEADCOM)
На місці HEADCOM може бути який завгодно ідентифікатор. Цей вираз говорить про те, що якщо HEADCOM ще не було визначено, то весь текст звідси і до директиви #endif просто вставлятиметься до файла реалізації. В іншому разі (якщо HEADCOM вже було визначено раніше, в чому можна впевнитися за допомогою директиви #define HEADCOM) наступний за #if текст не буде долучено до початкового коду. Оскільки змінну HEADCOM не було визначено до того як ця директива зустрілася вперше, але одразу ж після #if!defined() вона стала визначеною, увесь текст, розміщений поміж #if і #endif, буде підключено один раз, але це буде перший і останній раз. Наприклад:
#if!defined (HEADCOM) // якщо змінну HEADCOM ще не визначено
#define HEADCOM // визначити ї
int х; // визначити змінну
int func (int a, int b) // визначити функцію func
{ return a+b;
}
#endif // директива, яка закриває умову
Цей підхід слід використовувати завжди, коли існує можливість випадково підключити заголовний файл до початкового понад одного разу.
Раніше використовувалась директива #ifndef, це є те ж саме, що й #if!defined. Зрозуміло, що цей “захист від дурня” з використанням #if!defined спрацює лише у тому разі, коли визначення змінної х (чи іншої змінної або функції) може випадково бути включеним кількаразово до одного й того самого файла реалізації. Вона не спрацює, якщо х визначено у h-файлі, і його підключають до різних файлів F1 і F2. Препроцесор у цьому випадку є безсилий: він не в змозі визначити наявність однакових виразів у окремих файлах.