Треки
/
C++
C++
/
Салабус
/
Заголовкові файли
За

Заголовкові файли у C++

2 вправи

Про концепцію Заголовкові файли

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

Заголовний і вихідний файли

Найпоширеніше розширення для заголовних файлів - .h. Деякі проєкти використовують .hpp або взагалі обходяться без розширення.

Визначення містяться в окремому файлі .cpp. Щоб зібрати частини докупи, вихідний файл починається з включення відповідного заголовного файлу.

Якщо ми хочемо написати бібліотеку «quick_math», яка пропонує функцію «super_root», яку ми хочемо часто використовувати, файли матимуть такий вигляд:

// A file named quick_math.h
#pragma once

namespace quick_math {

double super_root(double x, int n);

}
// A file named quick_math.cpp
#include <cmath>

#include "quick_math.h"

double quick_math::super_root(double x, int n) {
    while (n) {
        x = std::sqrt(x), --n;
    }
    return x;
}

Якщо потрібно підключити заголовок, який потрібен лише реалізації, відповідний рядок #include достатньо вказати тільки у вихідному файлі. Усе, що включено в заголовок, також доступне у файлі .cpp, як-от бібліотека string у наведеному нижче прикладі. Увага: після оголошення в заголовному файлі потрібен символ ;, але після визначення у вихідному файлі він не потрібен.

Note

Багато вправ із C++ на Exercism починаються з двох майже порожніх файлів: заголовного та вихідного. Щоб розвʼязати вправу, перевірте файл *_test.cpp: там указано назви та простори імен функцій, яких очікують тести.

Класи та заголовки

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

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0);
    void give_water();
    std::string get_name();
    int get_size();
    void start_next_day();
};
}  // namespace robots
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"

robots::Flower::Flower(std::string name, int size) {
    this->name = "Robotica " + name;
    this->size = size;
}

void robots::Flower::start_next_day() {
    if (!needs_water) ++size;
    needs_water = true;
}

std::string robots::Flower::get_name() { return name; }

int robots::Flower::get_size() { return size; }

Коли заголовок використовують як огляд API, саме там шукають таку інформацію, як типові значення. Тому типове значення параметра size конструктора задають у заголовку, а не в реалізації. Визначення у вихідному файлі мають префікс із простору імен robots і типу класу Flower.

Ще один варіант компонування - бібліотека лише із заголовком, яка взагалі не має файлу .cpp:

// A file named robot_flower.h
#pragma once

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0) {
        this->name = "Robotica " + name;
        this->size = size;
    }
    void give_water() { needs_water = false; }
    std::string get_name() { return name; }
    int get_size() { return size; }
    void start_next_day() {
        if (!needs_water) ++size;
        needs_water = true;
    }
};
}  // namespace robots

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

Захист від повторного включення

Мабуть, ми вже помітили рядок #pragma once у наведеному вище прикладі заголовного файлу. Це називають захистом від повторного включення: він гарантує, що вміст файлу включається під час компіляції лише один раз, щоб уникнути помилок. Існує ще один, складніший різновид захисту від повторного включення, який починається з #ifndef і закінчується #endif; його описано нижче.

Advanced

Попередні оголошення

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

int myFunction(int n) {
    if (n < 10) {
        return n;
    } else {
        return myOtherFunction(n / 10);
    }
}

int myOtherFunction(int m) { return myFunction(m / 2); }

На момент визначення myFunction компілятор ще не знає про myOtherFunction. На жаль, проблему циклічних посилань не можна розвʼязати, просто змінивши порядок.

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

int myFunction(int n);       // Forward declaration of myFunction
int myOtherFunction(int m);  // Forward declaration of myOtherFunction

// Definition of myFunction
int myFunction(int n) {
    if (n < 0) {
        return 0;
    } else {
        return myOtherFunction(n - 2);
    }
}

// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }

Захист від повторного включення за допомогою ifndef

Не має значення, чи той самий файл включено в проєкт кілька разів. Заголовні файли не повинні містити визначень. У межах усього проєкту те саме визначення не може траплятися більше ніж один раз. Це називають «правилом одного визначення». Його перевіряє компілятор.

За допомогою захисту від повторного включення легко уникнути кількаразового ненавмисного включення того самого визначення. Їх створює спеціальний механізм на етапах компіляції. Мета - включити файл лише тоді, коли певну змінну ще не задано, а потім задати її, щойно файл включено. Часто імʼя цієї змінної утворюють як варіацію назви файлу. Інший метод - генерування UUID, щоб зменшити ризик випадково використати те саме імʼя двічі. Нижче показано синтаксис, де як змінну використано MY_HEADER_FILE_H.

#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H

// file content

#endif

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

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Заголовкові файли