C++

Організація коду: файли, препроцесор, простори імен

Одиниця трансляції та роль лінкера, попереднє оголошення і ODR, встановлення g++ на Windows/macOS/Linux, заголовкові файли і пара .h/.cpp, директиви препроцесора (#include, #define, умовна компіляція), header guards і #pragma once, простори імен namespace та оператор ::.

Організація коду: файли, препроцесор, простори імен

Межа одного файлу

Усі програми, які ми писали до цього моменту, жили в одному файлі. Це зручно: весь код перед очима, нічого не треба шукати, компіляція — одна команда. Але ця зручність оманлива. Вона зберігається рівно до того моменту, поки програма залишається маленькою.

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

Тепер ускладнимо картину: вас двоє, і обидва хочете одночасно вносити зміни. Один редагує функцію виводу, інший — логіку сортування. Але файл один. Система контролю версій (git) зафіксує конфлікт при спробі об'єднати зміни, навіть якщо ви торкалися абсолютно різних частин коду. Один файл стає вузьким місцем командної роботи.

Рішення, вироблене індустрією за десятиліття, — розбивати програму на кілька файлів. Кожен файл відповідає за чітко визначену частину програми: один містить логіку введення даних, інший — математичні обчислення, третій — форматування виводу. Цей принцип називається поділом відповідальностей (separation of concerns) і є одним із фундаментальних у програмній інженерії.

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

Як компілятор бачить проєкт

Перш ніж навчитися розбивати програму на файли, необхідно зрозуміти фундаментальний принцип роботи компілятора C++: він обробляє кожен .cpp-файл окремо і незалежно.

Цей файл — разом із усіма заголовками, які він підключає через #include — називається одиницею трансляції (translation unit). Одиниця трансляції є базовою одиницею компіляції в C++. Компілятор перетворює її на об'єктний файл (.o на Linux/macOS, .obj на Windows) — машинний код, ще не готовий до виконання.

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

Після компіляції всіх файлів у справу вступає лінкер (linker) — окрема програма, яка збирає об'єктні файли в єдиний виконуваний файл. Лінкер вирішує «посилання»: якщо main.cpp викликає функцію add(), визначену в math.cpp, лінкер знаходить її адресу в math.o і «зшиває» виклик із визначенням.

main.cpp  ──► компілятор ──► main.o ──┐
math.cpp  ──► компілятор ──► math.o ──┼──► лінкер ──► program
utils.cpp ──► компілятор ──► utils.o ─┘

Ця дворівнева архітектура (компіляція + лінкування) існує з перших днів мови C і залишається незмінною досі. Розуміння різниці між помилкою компілятора і помилкою лінкера — одна з перших практичних навичок, яку слід виробити.

Помилки компілятора — це синтаксичні та семантичні проблеми всередині одного файлу. Помилки лінкера — це проблеми зв'язків між файлами: виклик функції, визначення якої ніде не знайдено, або навпаки — два визначення одного імені.

Попереднє оголошення та правило ODR

Щоб компілятор погодився скомпілювати main.cpp, він повинен знати про функцію add() до її виклику. Для цього достатньо попереднього оголошення (forward declaration) — рядка, що повідомляє компілятору ім'я функції, типи її параметрів і тип значення, що повертається.

main.cpp
#include <iostream>

int add(int a, int b); // попереднє оголошення

int main()
{
    std::cout << "3 + 4 = " << add(3, 4) << "\n";
    return 0;
}
add.cpp
int add(int a, int b)
{
    return a + b;
}
./program
$ g++ main.cpp add.cpp -o program && ./program
3 + 4 = 7
Execution finished with exit code 0.

Важливо розрізняти два поняття:

  • Оголошення (declaration) — повідомляє компілятору про існування сутності та її тип. Пам'ять не виділяється. int add(int a, int b); — це оголошення.
  • Визначення (definition) — повне описання сутності з реалізацією. int add(int a, int b) { return a + b; } — це визначення.

Звідси випливає правило одного визначення (One Definition Rule, ODR): у всій програмі кожна функція може мати рівно одне визначення. Оголошень — скільки завгодно. Порушення ODR призводить до помилки лінкера «multiple definition».

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

Встановлення компілятора g++

g++ — це компілятор C++ із набору GCC (GNU Compiler Collection). Він доступний на всіх основних операційних системах, але спосіб встановлення відрізняється.

Windows

На Windows g++ не входить до складу ОС. Оберіть спосіб, що підходить до вашої ситуації.

MSYS2 (рекомендовано)

Крок 1. Завантажте інсталятор з msys2.org, встановіть у C:\msys64.

Крок 2. Відкрийте MSYS2 UCRT64 і оновіть пакети:

pacman -Syu

Крок 3. Встановіть компілятор:

pacman -S mingw-w64-ucrt-x86_64-gcc

Крок 4. Додайте C:\msys64\ucrt64\bin до змінної середовища PATH.

Крок 5. Перевірте у PowerShell:

PowerShell
> g++ --version
g++ (GCC) 14.2.0

Scoop

Не потребує прав адміністратора, автоматично налаштовує PATH.

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
scoop install gcc

Оновлення: scoop update gcc

Chocolatey

Вимагає прав адміністратора.

Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
choco install mingw
Chocolatey вимагає прав адміністратора. Якщо їх немає — оберіть Scoop.

winget

Вбудований у Windows 10/11, не потребує окремого встановлення.

winget install -e --id BrechtSanders.WinLibs.POSIX.UCRT

Після встановлення додайте шлях до bin у PATH вручну.

WSL2

Справжній нативний Linux-GCC на Windows.

wsl --install

Після перезавантаження у терміналі Ubuntu:

sudo apt update && sudo apt install build-essential
WSL2 — найкращий варіант для системного або серверного C++ без встановлення окремої ОС.

MinGW-w64 (ручне встановлення)

Завантажте архів з winlibs.com, розпакуйте в C:\mingw64 і додайте C:\mingw64\bin до PATH.

Visual C++ (MSVC)

Встановлюється разом із Visual Studio. Повністю підтримує C++17/20. Якщо Visual Studio вже є — окремо встановлювати g++ не обов'язково.

Порівняльна таблиця

ВаріантПрава адмінАвтоматичний PATHПідходить коли
MSYS2Бажані❌ вручнуПотрібне Unix-оточення
ScoopНемає прав адміністратора
ChocolateyКорпоративна машина
winget + WinLibs❌ вручнуВбудований менеджер
WSL2✅ (одноразово)Справжній Linux
MinGW-w64 вручну❌ вручнуБез доступу до мережі
MSVCВже є Visual Studio

macOS

xcode-select --install
На macOS g++ є псевдонімом Clang від Apple. Для цілей курсу різниця несуттєва. Якщо потрібен справжній GCC: brew install gcc (потім доступний як g++-14).

Linux

Ubuntu / Debian / Linux Mint:

sudo apt update && sudo apt install build-essential

Fedora / RHEL:

sudo dnf install gcc-c++

Arch Linux:

sudo pacman -S gcc

openSUSE:

sudo zypper install gcc-c++

Перевірка

g++ --version

Якщо бачите рядок із версією — компілятор готовий до роботи.


Компіляція через командний рядок та IDE

Командний рядок

Найпростіший спосіб — передати всі .cpp-файли одразу:

g++ main.cpp add.cpp -o program

Прапор -c компілює файл без лінкування — на виході лише об'єктний файл:

g++ -c add.cpp    # породжує add.o
g++ -c main.cpp   # породжує main.o
g++ main.o add.o -o program

Роздільна компіляція корисна у великих проєктах: при зміні одного файлу перекомпільовується лише він, решта .o залишаються.

ПрапорПризначення
-o ім'яЗадати ім'я вихідного файлу
-cКомпілювати без лінкування
-WallУсі попередження
-WextraДодаткові попередження
-std=c++17Стандарт C++
-gІнформація для дебаґера
-D СИМВОЛВизначити символ препроцесора

Рекомендований мінімум:

g++ -std=c++17 -Wall -Wextra main.cpp add.cpp -o program

CLion

CLion використовує CMake. Додайте новий .cpp до CMakeLists.txt:

add_executable(program
    main.cpp
    add.cpp
)

VS Code

У tasks.json явно перерахуйте файли у полі args:

"args": ["-std=c++17", "-Wall", "main.cpp", "add.cpp", "-o", "program"]
Найпоширеніша помилка в IDE: файл фізично існує в папці проєкту, але не підключений до збірки. Симптом — «undefined reference» при правильному коді. Завжди перевіряйте конфігурацію збірки.

Типові помилки та їх діагностика

При роботі з багатофайловими проєктами виникають три характерні помилки.

«was not declared in this scope» — помилка компілятора. Відсутнє попереднє оголошення функції. Додайте прототип перед першим використанням.

«undefined reference» — помилка лінкера. Оголошення є, але визначення не знайдено. Переконайтеся, що файл із реалізацією є в команді g++ або у конфігурації IDE.

«multiple definition» — помилка лінкера. Визначення зустрічається більше одного разу — найчастіше через визначення функції в заголовку, який підключається в кількох .cpp.

was not declared
Помилка компілятора
Відсутнє попереднє оголошення. Додайте прототип функції у файл, де вона використовується.
undefined reference
Помилка лінкера
Файл із визначенням не підключено до збірки. Перевірте команду g++ або CMakeLists.txt.
multiple definition
Помилка лінкера
Визначення функції більше одного разу. Визначення — лише в одному .cpp, у заголовку — лише оголошення.

Заголовкові файли

Проблема повторення оголошень

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

Це порушення принципу DRY (Don't Repeat Yourself): одна й та сама інформація існує в кількох місцях. Рішення мови C++ — заголовковий файл.

Що таке заголовковий файл

Заголовковий файл (header file) — файл із розширенням .h, що містить оголошення: прототипи функцій, визначення структур і класів, псевдоніми типів, константи. Не реалізації — лише «контракти».

Директива #include "math.h" — це не «підключення модуля». Препроцесор буквально копіює весь вміст файлу math.h і вставляє його на місці рядка. Те, що бачить компілятор — суцільний текст, ніби ви вписали вміст заголовка власноруч.

Пара .h / .cpp — еталонний патерн

Стандартна організація: для кожного .cpp існує відповідний .h. Заголовок описує що робить модуль (інтерфейс), .cppяк (реалізація).

math.h
#ifndef MATH_H
#define MATH_H

int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(double a, double b);

#endif
math.cpp
#include "math.h"

int add(int a, int b)      { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }

double divide(double a, double b)
{
    if (b == 0.0) return 0.0;
    return a / b;
}
main.cpp
#include <iostream>
#include "math.h"

int main()
{
    std::cout << "Сума:    " << add(10, 3)      << "\n";
    std::cout << "Різниця: " << subtract(10, 3) << "\n";
    std::cout << "Частка:  " << divide(10, 3)   << "\n";
    return 0;
}

Зверніть: math.cpp підключає власний заголовок math.h. Це захисний прийом — якщо сигнатура у .h і .cpp розійдуться, компілятор одразу повідомить про невідповідність.

<> vs "" — де шукає препроцесор

Кутові дужки <iostream> — пошук у системних директоріях стандартної бібліотеки. Подвійні лапки "math.h" — спочатку поточна директорія, потім системні.

Правило абсолютне:

  • Стандартна бібліотека та сторонні бібліотеки — <vector>, <cmath>, <string>
  • Власні заголовки проєкту — "math.h", "student.h"

Чому iostream, а не iostream.h

У 1980-х ідентифікатори C++ (включно з cout) жили у глобальному просторі імен, а заголовки мали розширення .h. При стандартизації C++98 весь функціонал перемістили у простір імен std і запровадили нові заголовки без .h. Старі (iostream.h) оголошені застарілими.

Те саме стосується C-заголовків: math.h<cmath>, stdlib.h<cstdlib>, string.h<cstring>.

Ніколи не підключайте <iostream.h> або <string.h> у сучасному C++. Завжди без .h: <iostream>, <string>, <cmath>.

Що можна і що не можна класти у заголовок

Заголовок підключається до багатьох .cpp — кожне підключення породжує копію вмісту. Якщо це визначення — порушується ODR.

✅ Можна у заголовку

  • Прототипи функцій
  • Визначення структур і класів
  • constexpr-константи
  • using-псевдоніми типів

❌ Не можна у заголовку

  • Визначення функцій (тіло)
  • Визначення змінних
  • using namespace std;
  • #include "file.cpp"

Директиви препроцесора

Препроцесор як окрема фаза

Щоразу, коли ви запускаєте компіляцію, відбувається не одна, а кілька послідовних фаз. Більшість програмістів знають про дві: компіляцію і лінкування. Але є ще одна, що виконується першоюпрепроцесінг.

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

Вихідний .cpp  →  Препроцесор  →  Розгорнутий код  →  Компілятор  →  .o файл

Інструкції для препроцесора — директиви — починаються з # і не закінчуються крапкою з комою. Вони не є операторами C++, не виконуються під час роботи програми і не генерують машинного коду.

Препроцесор «не знає» мови C++. Він не розуміє функцій, класів, блоків {}, типів даних — лише потік тексту і рядки, що починаються з #. Звідси виростає більшість пасток, пов'язаних із макросами.

Директива #include: текстова підстановка

Механіка #include до прямолінійності проста: препроцесор копіює весь вміст вказаного файлу і вставляє його на місці рядка директиви. Рядок за рядком, символ за символом. Це не «імпорт модуля» — це текстова підстановка.

greeting.h
void greet(const char* name);
main.cpp
#include "greeting.h"

int main()
{
    greet("Олена");
    return 0;
}

Після роботи препроцесора компілятор бачить:

void greet(const char* name);

int main()
{
    greet("Олена");
    return 0;
}

Рядок #include зник — замість нього вміст файлу. #include <iostream> вставляє тисячі рядків оголошень стандартної бібліотеки. Саме тому компіляція з <iostream> займає більше часу.

Запустіть g++ -E main.cpp — прапор -E зупиняє процес після препроцесінгу і виводить розгорнутий текст. Для файлу з #include <iostream> це кілька десятків тисяч рядків.

Заголовок може підключати інший заголовок — препроцесор розгортає ланцюжок рекурсивно:

main.cpp
  └── #include "geometry.h"
        └── #include "math.h"
              └── #include <cmath>

Директива #define: макроси

#define оголошує макрос — правило підстановки тексту. Є два різновиди.

Макроси з підстановкою тексту — ідентифікатор замінюється на заданий текст скрізь у файлі:

MacroDemo.cpp
#include <iostream>

#define MAX_STUDENTS  30
#define PI            3.14159265
#define GREETING      "Привіт, "

int main()
{
    std::cout << GREETING << "C++!\n";
    std::cout << "Макс. студентів: " << MAX_STUDENTS << "\n";
    std::cout << "Число пі: " << PI << "\n";
    return 0;
}
./MacroDemo
$ ./MacroDemo
Привіт, C++!
Макс. студентів: 30
Число пі: 3.14159265
Execution finished with exit code 0.

Підстановка буквальна: препроцесор не знає типів, пріоритетів операторів чи контексту. Саме тому макроси-функції — антипатерн:

#define SQUARE(x)  x * x
// SQUARE(a+1) → a+1 * a+1 = 11, а не 36 !
Для констант завжди використовуйте constexpr — вона типобезпечна, видима дебаґером і обмежена лексичною областю видимості. #define для констант — антипатерн у сучасному C++.
Критерій#defineconstexpr
Тип❌ немає✅ строго типізована
Область видимості❌ весь файл✅ лексична
Видима дебаґером
РекомендаціяЛише для символів умовної компіляціїДля всіх констант

Макроси без підстановки — символи-прапорці. Вони не замінюють жодного тексту, лише реєструють факт: символ визначений. Це основний легітимний сценарій #define у сучасному C++:

#define DEBUG
#define TESTING
#define PLATFORM_WINDOWS

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

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

DebugLog.cpp
#include <iostream>

// #define DEBUG  ← розкоментуйте для відлагоджувального виводу

int calculateSum(int a, int b)
{
#ifdef DEBUG
    std::cerr << "[DEBUG] calculateSum(" << a << ", " << b << ")\n";
#endif
    return a + b;
}

int main()
{
    std::cout << "Sum: " << calculateSum(3, 4) << "\n";
    return 0;
}
./DebugLog (з DEBUG)
$ ./DebugLog
[DEBUG] calculateSum(3, 4)
Sum: 7
Execution finished with exit code 0.

Основні директиви умовної компіляції:

  • #ifdef СИМВОЛ — якщо символ визначено
  • #ifndef СИМВОЛ — якщо символ не визначено
  • #if вираз / #elif вираз / #else / #endif — повноцінні умовні гілки
  • defined() — перевірка у виразах #if
LogLevel.cpp
#include <iostream>

#define LOG_LEVEL 2

int main()
{
#if LOG_LEVEL >= 3
    std::cout << "[INFO]  Програма запущена\n";
#endif
#if LOG_LEVEL >= 2
    std::cout << "[WARN]  Рівень заряду батареї низький\n";
#endif
#if LOG_LEVEL >= 1
    std::cout << "[ERROR] Підключення втрачено\n";
#endif
    return 0;
}
./LogLevel (LOG_LEVEL=2)
$ ./LogLevel
[WARN] Рівень заряду батареї низький
[ERROR] Підключення втрачено
Execution finished with exit code 0.

Компілятори автоматично визначають символи платформи, що дозволяє писати крос-платформний код:

#if defined(_WIN32)
    using FileHandle = void*;   // HANDLE у Windows API
#else
    using FileHandle = int;     // файловий дескриптор POSIX
#endif
СимволПлатформа
_WIN32Windows (32 і 64-bit)
__linux__Linux
__APPLE__macOS / iOS
__unix__Unix-подібні

Область видимості директив

Три правила, що відрізняють #define від C++-змінних:

  1. Від точки визначення до кінця файлу — символ активний одразу після рядка #define і до кінця поточного .cpp.
  2. Препроцесор не бачить блоків {}#define всередині функції діє на весь файл, ніби він написаний зовні.
  3. Символи не перетинають межі файлів#define DEBUG у main.cpp не впливає на utils.cpp. Кожен файл компілюється окремо.

#undef явно скасовує визначення:

#define TEMP_DEBUG
// ... код з відлагоджувальним виводом ...
#undef TEMP_DEBUG
// Тут TEMP_DEBUG більше не визначено

Визначення символів через командний рядок

Замість #define у коді символи можна передавати через прапор -D під час компіляції. Код залишається незмінним для будь-якого режиму збірки:

g++ main.cpp -o program -DDEBUG           # визначає символ DEBUG
g++ main.cpp -o program -DLOG_LEVEL=3     # LOG_LEVEL зі значенням 3
g++ main.cpp -o program -DNDEBUG          # стандартний release-режим

Header guards і #pragma once

Проблема подвійного підключення

Заголовкові файли породжують тонку, але системну проблему. Розглянемо три файли, кожен із яких написаний бездоганно:

math.h
int getSquareSides() { return 4; }
geometry.h
#include "math.h"
main.cpp
#include "math.h"
#include "geometry.h"

int main() { return 0; }
Помилка компіляції
$ g++ main.cpp -o program
math.h:1:5: error: redefinition of 'int getSquareSides()'

Препроцесор вставляє вміст math.h двічі: перший раз — через прямий #include "math.h" у main.cpp, другий — через geometry.hmath.h. Компілятор бачить два визначення однієї функції — порушення ODR.

Покрокове розгортання показує результат:

// 1-а вставка math.h:
int getSquareSides() { return 4; }

// 2-а вставка через geometry.h → math.h:
int getSquareSides() { return 4; }  // ODR порушено!

int main() { return 0; }

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

Header guards

Header guard — шаблон умовної компіляції, що гарантує: вміст заголовка обробляється рівно один раз, незалежно від кількості підключень.

math.h
#ifndef MATH_H   // якщо MATH_H ще не визначено — входимо
#define MATH_H   // визначаємо символ

int getSquareSides();

#endif           // кінець захисного блоку

Механіка:

  • Перше підключення: MATH_H не визначений → умова #ifndef істинна → визначаємо MATH_H → вміст вставляється.
  • Будь-яке наступне підключення: MATH_H вже визначений → умова #ifndef хибна → весь блок пропускається.

Конвенція іменування: ІМ'Я_ФАЙЛУ_H великими літерами, крапка → підкреслення. math.hMATH_H, Vec2.hVEC2_H, student_record.hSTUDENT_RECORD_H.

У великих проєктах із двома файлами utils.h у різних директоріях простий UTILS_H конфліктуватиме. Вирішення — включати в ім'я шлях або префікс: MYPROJECT_NETWORK_UTILS_H.

#pragma once

Більшість сучасних компіляторів підтримують простішу директиву:

math.h
#pragma once

int getSquareSides();

Один рядок замість трьох. Жодних символів, жодного ризику опечатки. Компілятор запам'ятовує абсолютний шлях до файлу і при повторному #include просто пропускає його, не відкриваючи.

Порівняння

КритерійHeader guards#pragma once
Офіційний стандарт C++⚠️ розширення
GCC / Clang / MSVC
Ризик опечатки⚠️ є
Обсяг коду3 рядки1 рядок
Робота із symlink⚠️ залежить
У сучасних проєктах із GCC, Clang або MSVC — #pragma once є зручним і безпечним вибором. Обидва підходи рівноцінні для цілей цього курсу.

Правило без винятків: кожен заголовковий файл проєкту зобов'язаний мати захист від подвійного підключення — header guard або #pragma once. Це не опція, а норма індустрії. Навіть заголовки стандартної бібліотеки (<iostream>, <vector>) мають такий захист.


Простори імен

Конфлікт імен

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

double log(double x);               // у math_utils.cpp
void   log(const std::string& msg); // у logger.cpp

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

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

Синтаксис і оператор ::

Простір імен (namespace) — іменована область видимості, що об'єднує пов'язані ідентифікатори та ізолює їх від решти. Два ідентифікатори з однаковим іменем у різних просторах — абсолютно різні сутності.

math_utils.h
#pragma once

namespace Math
{
    double log(double x);
    double sqrt(double x);
}
logger.h
#pragma once
#include <string>

namespace Logger
{
    void log(const std::string& msg);
    void warn(const std::string& msg);
}
main.cpp
#include <iostream>
#include "math_utils.h"
#include "logger.h"

int main()
{
    Logger::log("Програма запущена");
    double result = Math::log(2.718);
    std::cout << "log(e) ≈ " << result << "\n";
    return 0;
}
./program
$ ./program
[INFO] Програма запущена
log(e) ≈ 1.0
Execution finished with exit code 0.

Оператор :: (scope resolution) вказує компілятору, де саме шукати ідентифікатор. Math::log — «log у просторі Math». ::log без імені зліва — явне звернення до глобального простору.

Після закриваючої } блоку namespace крапка з комою не потрібна — на відміну від структур і класів.

Розміщення у файлах

Простір імен може бути розкиданий по кількох файлах — частини об'єднуються автоматично. Визначення функцій у .cpp записуються двома рівноцінними способами:

math_utils.cpp
#include "math_utils.h"
#include <cmath>

// Варіант A: всередині блоку namespace
namespace Math
{
    double log(double x) { return std::log(x); }
}

// Варіант B: поза блоком, через ::
double Math::sqrt(double x)
{
    return std::sqrt(x);
}

У промисловому коді частіше зустрічається Варіант B для .cpp-файлів — він чіткіше відокремлює оголошення від визначення.

using namespace — зручність із наслідками

using namespace std; поширений у підручниках, але несе реальні ризики.

Ризик 1: прихований конфлікт. using namespace std вносить усі ідентифікатори STL у поточний простір. Ваша змінна count конфліктуватиме зі std::count, ваш тип distance — зі std::distance.

Ризик 2: отруєння заголовка. using namespace у .h-файлі забруднює простір імен кожного .cpp, що підключить цей заголовок. Програміст, що використовує ваш заголовок, навіть не підозрює про приховані конфлікти.

Ризик 3: майбутні версії стандарту. C++ постійно розширюється. Новий ідентифікатор у std може конфліктувати з вашим існуючим кодом.

Абсолютне правило: using namespaceніколи у заголовкових файлах. Без жодних винятків.

Безпечна альтернатива — using-оголошення для конкретного ідентифікатора, локально у функції:

void printNames(const std::vector<std::string>& names)
{
    using std::cout; // лише cout, лише всередині цієї функції
    for (const auto& name : names)
        cout << name << "\n";
}
ПідхідРизикРекомендація
std::cout (явний)❌ жодного✅ завжди
using std::cout; у функції⚠️ мінімальний✅ прийнятний
using namespace std; у .cpp⚠️ значний❌ небажано
using namespace std; у .h🔴 критичний❌ заборонено

Анонімні простори імен

Допоміжні функції та змінні, потрібні лише в межах одного .cpp, можна приховати через анонімний простір імен:

AppLog.cpp
#include "AppLog.h"
#include <iostream>

namespace // без імені — видимий лише у цьому файлі
{
    void printLine(const char* level, const std::string& msg)
    {
        std::cout << "[" << level << "] " << msg << "\n";
    }
}

void AppLog::info(const std::string& msg)  { printLine("INFO ", msg); }
void AppLog::warn(const std::string& msg)  { printLine("WARN ", msg); }
void AppLog::error(const std::string& msg) { printLine("ERROR", msg); }

printLine недоступна за межами AppLog.cpp. Навіть якщо інший файл визначить однойменну функцію — конфлікту не виникне. Анонімний простір імен є сучасним еквівалентом static для функцій і змінних файлового рівня — і на відміну від static, він застосовується і до типів.

Вкладені простори імен

Простори імен вкладаються один в одного для ієрархічної організації великих проєктів:

AppCore.h
#pragma once
#include <string>

namespace AppCore
{
    namespace Logger
    {
        void log(const std::string& msg);
        void warn(const std::string& msg);
    }

    namespace Config
    {
        std::string get(const std::string& key);
        void        set(const std::string& key, const std::string& value);
    }
}

Доступ — через :: для кожного рівня: AppCore::Logger::log("msg").

C++17 додав скорочений синтаксис:

// До C++17:
namespace AppCore { namespace Logger { void log(const std::string& msg); } }

// C++17:
namespace AppCore::Logger
{
    void log(const std::string& msg);
}

Чому весь STL живе у std

При стандартизації C++98 весь функціонал стандартної бібліотеки перемістили у простір імен std, щоб усунути конфлікти між бібліотечними іменами та іменами користувача. До цього cout, sort, find, max жили у глобальному просторі — і будь-який збіг з вашим іменем ламав код.

Правило курсу: завжди писати std::, не використовувати using namespace std — це виробляє правильну звичку для промислового програмування.


Практика

Завдання нижче охоплюють увесь модуль: організацію файлів, заголовки, препроцесор, header guards і простори імен. Кожен рівень вимагає застосувати кілька концепцій одночасно.

Рівень 1 — Базовий

Рівень 2 — Модульний калькулятор

Рівень 3 — Повноцінний модульний проєкт


Резюме

🏗️ Файли та лінкер

Кожен .cpp — окрема одиниця трансляції. Компілятор обробляє їх незалежно. Лінкер збирає .o-файли в єдиний виконуваний, вирішуючи всі міжфайлові посилання.

📋 ODR і оголошення

Визначення функції — рівно одне. Оголошень — скільки завгодно. Попереднє оголошення дозволяє викликати функцію до її визначення. Заголовки містять оголошення, .cpp — визначення.

📄 Пара .h / .cpp

Еталонний патерн організації: заголовок — інтерфейс (що), .cpp — реалізація (як). Ім'я заголовка збігається з іменем реалізаційного файлу. .cpp підключає власний .h.

⚙️ Препроцесор

Запускається до компілятора. #include — буквальна текстова підстановка. #define для констант — антипатерн, використовуйте constexpr. #define без значення + #ifdef — умовна компіляція. Символи передаються через -D.

🛡️ Header guards

#ifndef ІМ'Я / #define ІМ'Я / #endif або #pragma once — кожен заголовок зобов'язаний мати захист від подвійного підключення. Без нього ланцюгові #include призводять до «redefinition».

📦 namespace і ::

Ізолює ідентифікатори від конфліктів. Доступ через ::. using namespace у заголовках — абсолютно заборонено. Анонімний namespace приховує допоміжні функції файлу. Вкладені простори організують ієрархію великих проєктів.
На цьому завершується модуль організації коду. Ми пройшли повний шлях: від проблеми одного великого файлу — через механізм компіляції і лінкування, заголовки, препроцесор і захист від дублювання — до ізоляції імен у великих проєктах. Наступний модуль — об'єктно-орієнтоване програмування: класи, конструктори, деструктори та інкапсуляція.
Copyright © 2026