Як тільки програми стають більшими і їх код уже не поміщується в декількох файлах, записувати кожен раз попередні оголошення для функцій, які ми хочемо використовувати, але які знаходяться в інших файлах, стає все нудніше і нудніше. Добре було б, якби всі попередні оголошення знаходилися в одному місці, чи не так?
Файли .cpp не є єдиними файлами в проектах. Є ще один тип файлів — заголовкові файли (або “заголовки”), які мають розширення .h. Метою заголовків є зручне зберігання набору оголошень об’єктів для їх подальшого використання в інших програмах.
Заголовкові файли зі Стандартної бібліотеки С++
Розглянемо наступну програму:
1 2 3 4 5 6 7 |
#include <iostream> int main() { std::cout << "Hello, world!" << std::endl; return 0; } |
Результат виконання програми:
Hello, world!
У цій програмі ми використовуємо об’єкт std::cout, який ніде не визначаємо. Як компілятор знає, що це таке? Справа в тому, що std::cout оголошений в заголовку iostream. Коли ми пишемо #include <iostream>
, ми робимо запит, щоб весь вміст заголовка iostream було скопійовано в наш файл. Таким чином, весь вміст iostream стає доступним для використання.
Як правило, в заголовкових файлах записуються тільки оголошення, без визначень. Отже, якщо std::cout тільки оголошений в заголовку iostream, де ж він визначається? Відповідь: “В Стандартній бібліотеці С++, яка автоматично підключається до вашого проекту на етапі лінкінгу”.
Подумайте, що сталося б, якби заголовкового файлу iostream не було? Кожен раз, при використанні std::cout, вам доводилося би вручну копіювати всі попередні оголошення, пов’язані з std::cout, в верхню частину вашого файлу! Добре ж, що можна просто #include <iostream>
, чи не так?
Пишемо свої власні заголовкові файли
Тепер давайте повернемося до прикладу, який ми обговорювали на попередньому уроці. У нас було два файли: add.cpp і main.cpp.
add.cpp:
1 2 3 4 |
int add(int x, int y) { return x + y; } |
main.cpp:
1 2 3 4 5 6 7 8 9 |
#include <iostream> int add(int x, int y); // попереднє оголошення з використанням прототипу функції int main() { std::cout << "The sum of 3 and 4 is " << add(3, 4) << std::endl; return 0; } |
Примітка: Якщо ви все робите з нуля, то не забудьте додати add.cpp в свій проект, щоб він був підключений до компіляції.
Ми використовували попереднє оголошення, щоб повідомити компілятор, що таке add(). Як ми вже говорили, записувати в кожному файлі попередні оголошення використовуваних функцій — справа не дуже захоплююча.
І тут нам на допомогу приходять заголовки. Досить просто написати один заголовок і його можна буде повторно використовувати в будь-якій кількості програм. Також вносити зміни в такий код (наприклад, додати ще один параметр) набагато легше, ніж вносити зміни у всі наявні файли, де використовується цей код.
Написати свій власний заголовок не так вже й складно. Заголовкові файли складаються з двох частин:
Директиви препроцесора — зокрема, header guards, які запобігають викликам заголовка більше одного разу з одного і того ж файлу.
Вміст заголовкового файлу — набір оголошень.
Всі ваші заголовки (які ви написали самостійно) повинні мати розширення .h
.
add.h:
1 2 3 4 5 6 7 8 9 |
// Починаємо з директив препроцесора. ADD_H – це довільне унікальне ім'я (зазвичай використовується ім'я заголовка) #ifndef ADD_H #define ADD_H // А це вже вміст заголовка int add(int x, int y); // прототип функції add() (пам'ятайте про крапку з комою в кінці!) // Закінчуємо директивою препроцесора #endif |
Щоб використовувати цей файл в main.cpp, вам спочатку потрібно буде підключити його до проекту.
main.cpp, в якому ми підключаємо add.h:
1 2 3 4 5 6 7 8 |
#include <iostream> #include "add.h" int main() { std::cout << "The sum of 3 and 4 is " << add(3, 4) << std::endl; return 0; } |
add.cpp залишається без змін:
1 2 3 4 |
int add(int x, int y) { return x + y; } |
Коли компілятор зустрічає #include "add.h"
, то він копіює весь вміст add.h в поточний файл. Таким чином, ми отримуємо попереднє оголошення функції add().
Примітка: При підключенні заголовкового файлу, весь його вміст вставляється відразу ж після рядка #include ...
.
Якщо ви отримали помилку від компілятора, що add.h не знайдено, то переконайтеся, що ім’я вашого файлу точно add.h
. Цілком можливо, що ви могли зробити помилку, наприклад: просто add
(без .h
) або add.h.txt
або add.hpp
.
Якщо ви отримали помилку від лінкера, що функція аdd() не визначена, то переконайтеся, що ви коректно підключили add.cpp до вашого проекту (і до компіляції також)!
Кутові дужки (<>) vs. Подвійні лапки (“”)
Ви, напевно, хочете дізнатися, чому використовуються кутові дужки для iostream і подвійні лапки для add.h. Справа в тому, що, використовуючи кутові дужки, ми повідомляємо компілятор, що заголовковий файл, який підключається, написаний не нами (він є “системним”, тобто тим, який надається Стандартною бібліотекою С++), так що шукати цей заголовок слід в системних директоріях. Подвійні лапки повідомляють компілятор, що ми підключаємо наш власний заголовок, який ми написали самостійно, тому шукати його слід в поточній директорії нашого проекту. Якщо файлу там не виявиться, то компілятор почне перевіряти інші шляхи, у тому числі і системні директорії.
Правило: Використовуйте кутові дужки для підключення “системних” заголовкових файлів і подвійні лапки для всього іншого (ваших власних заголовків).
Варто відзначити, що одні заголовки можуть підключати інші заголовки. Проте я не рекомендую це робити.
Чому iostream пишеться без закінчення .h?
Ще одне часте питання: “Чому iostream (або будь-який інший зі стандартних заголовкових файлів) при підключенні пишеться без закінчення .h
?”. Справа в тому, що є два окремих файла: iostream.h
(заголовок) і просто iostream
! Для пояснення потрібен короткий екскурс в історію.
Коли мова C++ тільки створювалася, всі файли бібліотеки Runtime мали закінчення .h. Оригінальні версії cout і cin оголошені в iostream.h. При стандартизації мови С++ комітетом ANSI, вирішили перенести всі функції з бібліотеки Runtime в простір імен std, щоб запобігти можливості виникнення конфліктів імен з ідентифікаторами користувачів (що, між іншим, є хорошою ідеєю!). Проте, виникла проблема: якщо всі функції перемістити в простір імен std, то старі програми переставали працювати!
Для забезпечення зворотної сумісності ввели новий набір заголовків з тими ж іменами, але без закінчення .h
. Весь їх функціонал знаходиться в просторі імен std. Таким чином, старі програми з #include <iostream.h>
не потрібно було переписувати, а нові програми вже могли використовувати #include <iostream>
.
Коли ви підключаєте заголовки зі Cтандартної бібліотеки C++, переконайтеся, що ви використовуєте версію без .h (якщо вона існує). В іншому випадку ви будете використовувати застарілу версію заголовкового файлу, який вже більше не підтримується.
Крім того, багато бібліотек, успадковані від мови Cі, які до сих пір використовуються в C++, також були продубльовані з додаванням префікса c
(наприклад, stdlib.h
став cstdlib
). Функціонал цих бібліотек також був переміщений в простір імен std, щоб уникнути можливості виникнення конфліктів імен з користувацькими ідентифікаторами.
Правило: При підключенні заголовкових файлів зі Стандартної бібліотеки С++, використовуйте версію без “.h” (якщо вона існує). Користувацькі заголовки ж повинні мати закінчення “.h”.
Чи можна записувати визначення в заголовкових файлах?
Мова C++ не скаржитиметься, якщо ви це зробите, але так робити не прийнято.
Як вже було сказано вище, при підключенні заголовкового файлу, весь його вміст вставляється відразу ж після рядка #include. Це означає, що будь-які визначення, які є в заголовку, скопіюються в ваш файл.
Для невеликих проектів, це, ймовірно, не буде проблемою. Але для більш масштабних проектів це може збільшувати час компіляції (оскільки код повторно компілюватиметься) і розмір виконуваного файлу. Якщо внести зміни до визначень, які знаходяться в файлі .cpp, то перекомпілювати доведеться тільки цей файл. Якщо ж внести зміни до визначень, які записані в заголовку, то перекомпілювати доведеться кожен файл, який підключає цей заголовок, використовуючи директиву препроцесора #include. І ймовірність того, що через одну невелику правку вам доведеться перекомпілювати весь проект, різко зростає!
Іноді робляться винятки для простих функцій, які навряд чи зміняться (наприклад, де визначення складається всього лише з одного рядка).
Поради
Ось декілька порад щодо написання власних заголовкових файлів:
Завжди використовуйте директиви препроцесора.
Не визначайте змінні в заголовкових файлах, якщо це не константи. Заголовкові файли слід використовувати тільки для оголошень.
Не визначайте функції в заголовкових файлах.
Кожен заголовок повинен виконувати свою певну роботу і бути якомога більш незалежним. Наприклад, ви можете помістити всі ваші оголошення, пов’язані з файлом А.cpp в файл A.h, а всі ваші оголошення, пов’язані з B.cpp — в файл B.h. Таким чином, якщо ви працюватимете тільки з А.cpp, то вам достатньо буде підключити тільки A.h і навпаки.
Використовуйте імена ваших робочих файлів в якості імен для ваших заголовків (наприклад, grades.h працює з grades.cpp).
Не підключайте одні заголовки з других заголовків.
Не підключайте файли .cpp, використовуючи директиву препроцесора #include.