Заголовні Файли, Функції таФайли, Статті, Програмування - Програмування C, Delphi, C#

Один заголовний файл

Множинні заголовні файли

Типи у всіх описах одного і того ж об'єкта повинні бути узгодженими. Один із способів це досягти міг би полягати у забезпеченні засобів перевірки типів у компонувальнику, але більшість компонувальників – зразка 1950-х, і їх не можна змінити з практичних міркувань. Інший підхід полягає у забезпеченні того, що вихідний текст, як він передається на розгляд компілятору, або узгоджений, або містить інформацію, що дозволяє компілятору виявити неузгодженість. Один недосконалий, але простий спосіб досягти узгодженості полягає у включенні заголовних файлів, що містять інтерфейсну інформацію, вихідні файли, в яких міститься виконуваний код та/або визначення даних.

p align="justify"> Механізм включення за допомогою #include - це надзвичайно простий засіб обробки тексту для складання шматків вихідної програми в одну одиницю (файл) для її компіляції. Директива

для препроцессування файлу file.c точно так, як це зробила б CC перед запуском власне компілятора. Для включення файлів зі стандартної директорії включення замість лапок використовуються кутові дужки. Наприклад:

#include // із стандартної директорії включення #define \"myheader.h\" // з поточної директорії

І використання <> має ту перевагу, що у програму фактичне ім'я директорії включення не вбудовується (зазвичай, спочатку проглядається /usr/include/CC, та був usr/include). На жаль, прогалини в директиві include істотні:

#include // не знайде

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

З наступне емпіричне правило щодо того, що слід, а що не слід поміщати в заголовні файли, є не вимогою мови, а просто пропозицією щодо розумного використання апарату #include.

У заголовному файлі можуть бути:

Визначення типівstruct point
Опис функційextern int strlen (const char *);
Визначення inline-функційinline char get()
Опис данихextern int a;
Визначення константconst float pi = 3.141593
Перерахуванняenum bool < false, true >;
Директиви include#include
Визначення макросів#define Case break; case
Коментарі/* перевірка на кінець файлу */
але ніколи
Визначення звичайних функційchar get()
Визначення данихint a;
Визначення складних константних об'єктівconst tbl[] = < /*. */ >

У UNIX прийнято, що заголовні файли мають суфікс (розширення) .h. Файли з визначенням даних або функцій повинні мати суфікс .c. Такі файли часто називають, відповідно, ".h файли" і ".c файли". Слід зазначити, що у C++ макроси набагато менш корисні, ніж у C, оскільки C++ має такі мовніконструкції, як const для визначення констант та inline для виключення витрат на виклик функції.

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

Один заголовний файл

Найчастіше вирішити проблему розбиття програми на кілька файлів помістивши функції та визначення даних у відповідне число вихідних файлів і описавши типи, необхідні для їх взаємодії, в одному заголовному файлі, який включається у всі інші файли. Для програми калькулятора можна використовувати чотири .c файли: lex.c, syn.c, table.c і main.c, і заголовний файл dc.h, що містить опис всіх імен, які використовуються більш ніж в одному .c файлі:

// dc.h: загальні описи для калькулятора

enum token_value NAME, NUMBER, END, PLUS="+", MINUS="-", MUL="*", DIV="/", PRINT=";", ASSIGN=" =", LP="(", RP=")" >;

extern int no_of_errors; extern double error(char*s); extern token_value get_token(); extern token_value curr_tok; extern double number_value; extern char name_string[256];

extern double expr(); extern double term(); extern double prim();

struct name char*string; name* next; double value; >;

extern name * look (char * p, int ins = 0); inline name*insert(char*s) < return look(s,1); >

Якщо опустити фактичний код, то lex.c виглядатиме приблизно так:

// lex.c: введення та лексичний аналіз

token_value curr_tok; double number_value; char name_string[256];

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

extern token_value get_token(); //. token_value get_token() < /*. */ >

Це забезпечує те, що компілятор виявить будь-яку неузгодженість у типах, вказаних для імені. Наприклад, якби get_token() була описана як повертаюча token_value, але при цьому визначена як повертаюча int, компіляція lex.c не пройшла б через помилку невідповідності типів.

Файл syn.c виглядатиме приблизно так:

// syn.c: синтаксичний аналіз та обчислення

double prim() < /*. */ > double term() < /*. */ > double expr() < /*. */ >

Файл table.c виглядатиме приблизно так:

extern char * strcmp (const char *, const char *); extern char * strcpy (char *, const char *); extern int strlen(const char*);

const TBLSZ = 23; name* table[TBLSZ];

name* look(char* p; int ins) < /*. */ >

Зауважте, що table.c сам визначає стандартні функції для роботи з рядками, тому жодної перевірки узгодженості цих описів немає. У більшості випадків краще включати заголовний файл, ніж описувати ім'я в .c файлі як extern. При цьому може включатися "надто багато", але це зазвичай не надає серйозного впливу на час, необхідний для компіляції,і зазвичай економить час програміста. Як приклад цього, зверніть увагу на те, як strlen() знову описується в main() (нижче). Це зайві натискання клавіш та можливе джерело неприємностей, оскільки компілятор не може перевірити узгодженість цих двох визначень. Насправді, цієї складності можна було б уникнути, якби всі описи extern поміщені в dc.h, як і пропонувалося зробити. Ця "недбалість" збережена в програмі, оскільки це дуже типово для C програм, дуже спокусливо для програміста, і частіше призводить, ніж не призводить, до помилок, які важко виявити, і до програм, з якими важко працювати. Вас попередили!

І main.c, нарешті, виглядає так:

// main.c: ініціалізація, головний цикл та обробка помилок

double error(char*s) < /*. */ >

extern int strlen (const char *);

main(int argc, char* argv[]) < /*. */ >

Множинні заголовні файли

Розглядаючи організацію калькулятора, ми помічаємо, що error() використовується майже кожною функцією програми, а сама використовує лише . Це звичайна для функції помилок ситуація, тому error() слід відокремити від main():

// error.h: обробка помилок extern int no_errors; extern double error(char*s);

// error.c #include #include "error.h" int no_of_errors; double error(char*s) < /*. */ >

При такому стилі використання заголовних файлів. h файл і пов'язаний з ним.

Таблиця символів не залежить від решти калькулятора за винятком використання функції помилок. Це можна зробити очевидним:

// table.h: опис таблиці імен struct name char*string; name* next; double value; >;

extern name * look (char * p, int ins = 0); inline name* insert(char* s)

// table.c: визначення таблиці імен #include "error.h" #include #include "table.h"

const TBLSZ = 23; name* table[TBLSZ];

name* look(char* p; int ins) < /*. */ >

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

// lex.h: описи для введення та лексичного аналізу

enum token_value NAME, NUMBER, END, PLUS="+", MINUS="-", MUL="*", DIV="/", PRINT=";", ASSIGN="=", LP="(", RP=")" >;

extern token_value curr_tok; extern double number_value; extern char name_string[256];

extern token_value get_token();

Цей інтерфейс лексичного аналізатора досить безладний. Недолік у належному типі лексеми виявляє себе у необхідності давати користувачеві get_token() фактичні лексичні буфери number_value та name_string.

// lex.c: визначення для введення та лексичного аналізу

#include #include #include "error.h" #include "lex.h"

token_value curr_tok; double number_value; char name_string[256];

Інтерфейс синтаксичного аналізатора абсолютно прозорий:

// syn.c: описи для синтаксичного аналізу та обчислення

extern double expr(); extern double term(); extern double prim();

// syn.c: визначення для синтаксичного аналізу та обчислення #include "error.h" #include "lex.h" #include "syn.h"

double prim() < /*. */ > double term() < /*. */ > double expr() < /*. */ >

Головна програма, якзавжди, тривіальна:

// main.c: головна програма #include #include "error.h" #include "lex.h" #include "syn.h" #include "table .h" #include

main(int argc, char* argv[]) < /*. */ >

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

Зауважте, що такий стиль компонування не рекомендується:

// file1.c: // "extern" не використовується int a = 7; const c = 8; void f(long) < /*. */ >

// file2.c: // "extern" у .c файлі extern int a; extern const c; extern f(int); int g() < return f(a+c); >

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

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

// table.c: визначення таблиці імен

#include "error.h" #include #include "table.h"

const TBLSZ = 23; static name* table[TBLSZ];

name* look(char* p; int ins) < /*. */ >

Це гарантує, що будь-який доступ до table дійсно буде здійснюватися саме через look(). "Хувати" константу TBLSZ не обов'язково.