JavaRush /Курси /C++ SELF /Помилка компіляції vs. помилка лінкування

Помилка компіляції vs. помилка лінкування

C++ SELF
Рівень 30 , Лекція 1
Відкрита

1. Важливо відрізняти compile error від link error

Коли ви тільки починаєте програмувати, кнопка «Build/Run» в IDE часто сприймається як «магічна кнопка, що все робить сама». І це нормально: мозок намагається заощадити сили. Але така економія має ціну: ви бачите червоний текст і починаєте лагодити все підряд — ніби у вас зламався автомобіль, а ви спершу міняєте двірники, бо «вони ближче».

У C++ помилка компіляції й помилка лінкування — це дві різні «поломки» на двох різних етапах. Якщо їх плутати, ви майже напевно шукатимете проблему не там і виправлятимете не те. Добра новина в тому, що відрізнити їх можна дуже швидко: за характерними словами і за тим, на що саме скаржиться повідомлення.

Мапа процесу: компілятор і лінковник

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

Ключовий момент: компілятор працює з кожним .cpp окремо — з однією одиницею трансляції, translation unit. Лінковник працює з результатами компіляції всіх .cpp і намагається звʼязати їх в один виконуваний файл. Тому одні проблеми видно «відразу» — під час компіляції, а інші спливають лише «наприкінці» — під час лінкування.

Нижче — коротка таблиця, яка буде нашим орієнтиром упродовж усієї лекції.

Етап Що перевіряється Типові формулювання в повідомленнях Де зазвичай лагодити
Компіляція (compile) синтаксис, типи, імена, доступність заголовків, видимість оголошень error: expected ..., error: ... was not declared, no such file or directory, no matching function у конкретному .cpp або в підключеному .hpp
Лінкування (link) чи знайдено визначення для всіх використаних функцій і змінних та чи немає дублікатів визначень undefined reference, unresolved external, multiple definition, LNK... у тому, які .cpp беруть участь у збиранні, і де розташовані визначення

2. Помилка компіляції: «я не розумію цей файл»

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

Щоб приклади не були надто абстрактними, продовжимо наш навчальний консольний застосунок TaskBox — найпростіший менеджер задач. Ми спеціально робимо маленькі функції й розносимо їх по файлах, щоб наочно ловити різні типи помилок.

Синтаксична помилка: «не можу це прочитати»

Синтаксична помилка — класика жанру. Це ситуація, коли у вас бракує ;, закривальної } або ви випадково написали щось «не за граматикою C++».

#include <iostream>

int main() {
    std::cout << "TaskBox started\n"   // <- забули ';'
    return 0;
}

Тут компілятор зазвичай скаже щось на кшталт expected ‘;’ або expected .... Важливо розуміти: доки ви не виправите синтаксис, рухатися далі немає сенсу. Компілятор не може коректно розібрати файл, тож частина наступних повідомлень може бути лише «луною» першої проблеми.

Імʼя не знайдено: «я не знаю, що це таке»

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

#include <iostream>

int main() {
    std::cout << addTask("buy milk") << '\n';
    return 0;
}

Якщо addTask ніде не оголошено у видимій області — наприклад, ви забули #include "task.hpp", — компілятор скаже was not declared in this scope або identifier not found. Це помилка компіляції, бо компілятор не може навіть «описати» виклик: він не знає, що таке addTask.

Заголовок не знайдено: «мені не дали потрібний файл»

Іноді проблема взагалі не в самій мові C++, а в тому, що компілятор не може знайти файл.

#include "task.hpp"
#include "missing.hpp" // файла немає

int main() {}

Повідомлення буде схожим на No such file or directory. Це теж compile error, бо компілятор не може зібрати одиницю трансляції: йому бракує тексту, який ви просите підключити.

3. Помилка лінкування: «я зібрав частини, але не зміг їх звʼязати»

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

Якщо компілятор — це перевірка граматики, то лінковник — перевірка того, чи для кожної «згадки персонажа» у сценарії справді є актор на сцені.

Link error: undefined reference / unresolved external

Це найчастіший link error: десь ви оголосили функцію або підключили заголовок з оголошенням, а визначення, тобто реалізація, не потрапило до збирання.

Зробімо справді «правильну» структуру нашого TaskBox.

task.hpp (оголошення):

#pragma once
#include <string>

int addTask(const std::string& title);

task.cpp (реалізація):

#include "task.hpp"

int addTask(const std::string& title) {
    (void)title;
    return 1;
}

main.cpp:

#include <iostream>
#include "task.hpp"

int main() {
    std::cout << addTask("buy milk") << '\n'; // 1
}

Тепер зверніть увагу на типову ситуацію: task.cpp існує в теці проєкту, але не бере участі у збиранні, наприклад ви забули додати файл у проєкт або в ціль збирання. Тоді main.cpp успішно скомпілюється: він побачив оголошення addTask(...) із заголовка і «повірив», що десь у світі є реалізація.

А от лінковник скаже: «не знайдено визначення» — undefined reference, unresolved external symbol. Саме в цей момент він намагався «зшити» виклик із реалізацією, але не зміг.

Link error: multiple definition

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

Поганий приклад:

// config.hpp
#pragma once

int g_nextId = 1; // <- ВИЗНАЧЕННЯ в заголовку (ризик!)

task.cpp:

#include "config.hpp"

int getNextIdForTask() {
    return g_nextId++;
}

main.cpp:

#include "config.hpp"
#include <iostream>

int main() {
    std::cout << g_nextId << '\n'; // 1
}

Обидва .cpp підключили config.hpp, і кожен отримав власне визначення g_nextId. На етапі лінкування це перетворюється на multiple definition. І це безпосередньо повʼязано з тим, що в програмі в ідеалі має бути «одне визначення» таких сутностей. Привіт, ODR.

4. Як компіляція «вірить», а лінкування «перевіряє»

Тут важливо на хвилину зупинитися й спокійно проговорити ключову думку, щоб далі не залишалося відчуття магії. Коли компілятор бачить виклик addTask("buy milk"), він хоче зрозуміти дві речі: чи існує така функція за формою і чи можна коректно передати їй аргументи.

Для цього йому достатньо оголошення:

int addTask(const std::string& title);

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

Саме тому сценарій «оголосили, але не визначили» дає link error, а не compile error. Компілятор не зобовʼязаний здогадуватися, що реалізації немає: він припускає, що ви як доросла людина не брешете. Спойлер: ми брешемо постійно, але випадково.

Практичний детектор: 5 сигналів, за якими ви впізнаєте тип помилки

Зараз буде дуже практична частина. Це той момент, коли в майбутньому ви побачите червоний лог і за 10 секунд скажете: «Ага, це лінкування. Отже, я шукаю реалізацію, участь файла у збиранні або дублікати».

Я опишу ці сигнали звʼязним текстом, без «переліків заради переліків», бо корисно поєднати в голові дві речі: «як читати повідомлення» і «куди йти лагодити».

Перший сигнал — наявність конкретного місця у вихідному коді: main.cpp:12:5: error: .... Це майже завжди компіляція. Лінковник теж іноді згадує файли, але зазвичай це вже імена обʼєктних файлів або бібліотек, а не точний рядок у вашому main.cpp.

Другий сигнал — слова про «очікував»: expected, «не можу розібрати» та будь-які скарги на синтаксис. Лінковник синтаксису не бачить: він працює з результатом компіляції. Тож expected ';' — стовідсотковий compile error.

Третій сигнал — фрази «не знайдено файл» — No such file or directory поруч із #include. Це compile error: компілятор не може зібрати одиницю трансляції, бо йому не дали текст.

Четвертий сигнал — слова undefined reference, unresolved external, LNK2019 у MSVC або ld: .... Це лінкування: йдеться про символи, які мають бути десь визначені, але їх не знайдено.

Пʼятий сигнал — multiple definition або аналоги в MSVC. Це лінкування: визначення вже «приїхали» на збирання, і лінковник виявив, що приїхали двоє однакових людей з одним паспортом.

5. Практика на TaskBox: одна проблема — два етапи

Зараз ми зробимо важливу вправу для мозку: подивімося, як схожа за змістом проблема проявляється як compile error і як link error. Це допомагає перестати плутати «не бачить» і «не знайшов».

Компілятор не бачить оголошення

main.cpp:

#include <iostream>

int main() {
    std::cout << addTask("buy milk") << '\n';
}

Тут немає #include "task.hpp", тому компілятор узагалі не знає, що таке addTask. Це compile error.

Компілятор бачить оголошення, але лінковник не знаходить визначення

main.cpp:

#include <iostream>
#include "task.hpp"

int main() {
    std::cout << addTask("buy milk") << '\n';
}

task.hpp є, оголошення є, тож компіляція пройде. Але якщо task.cpp не бере участі у збиранні або в ньому немає визначення addTask, то помилка виникне на етапі лінкування — з undefined reference.

Сенс тут простий: помилка не «містично переїхала». Вона просто проявилася на тому етапі, де її вже можна перевірити.

Дуже часта причина link error: «сигнатури розійшлися»

Цей момент — типова пастка: ви думаєте, що визначення є, але лінковник усе одно каже undefined reference.

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

task.hpp:

#pragma once
#include <string>

int addTask(const std::string& title);

task.cpp (помилка):

#include "task.hpp"

int addTask(std::string title) { // <- немає const&, інший параметр
    (void)title;
    return 1;
}

Для компілятора це може пройти, особливо якщо ви випадково не підключили task.hpp у task.cpp, що саме по собі є поганою практикою. Але лінковник шукатиме addTask(const std::string&), а знайде addTask(std::string). Для нього це дві різні функції.

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

6. Міні-алгоритм читання лога збирання

Хочеться дати вам не філософію, а робочий алгоритм. Уявіть, що ви натиснули Build і отримали 200 рядків логу. Не треба читати його як роман, хоча інколи за драматургією він цілком тягне на «Оскар». Треба щоразу діяти однаково.

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

Нижче — проста блок-схема, яку зручно тримати в голові:

flowchart TD
    A["Збирання завершилося з помилкою"] --> B{"У повідомленні є undefined reference / multiple definition / LNK... ?"}
    B -- Так --> L["Це помилка лінкування. Шукаємо: де визначення? чи немає дублікатів? чи всі .cpp беруть участь у збиранні?"]
    B -- Ні --> C{"У повідомленні є file:line:col і 'error:' ?"}
    C -- Так --> D["Це помилка компіляції. Шукаємо: синтаксис/типи/імена/#include/видимість оголошень"]
    C -- Ні --> E["Дивимося вище в лог: іноді IDE приховує реальний етап"]

7. Типові помилки: чому вони повторюються

Помилка № 1: лагодити не ту стадію.
Найчастіший сценарій: ви бачите undefined reference, але лізете переписувати if/else у main, бо «раптом логіка неправильна». Лінковнику байдуже до вашої логіки: він не може знайти визначення. Лікується це звичкою спершу класифікувати помилку, а вже потім діяти.

Помилка № 2: «файл існує — значить, він бере участь у збиранні».
Це психологічно зрозуміло: якщо task.cpp лежить поруч, здається, що його «мають» врахувати. Але збирання — це список файлів, які реально компілюються й лінкуються. Якщо task.cpp не додано в проєкт або в ціль збирання, його наче не існує. У результаті ви отримуєте undefined reference і відчуття, ніби реальність з вас насміхається.

Помилка № 3: визначати змінні в заголовках.
Новачок думає: «Я ж хочу, щоб усі бачили g_nextId, отже, покладу його в .hpp». І це призводить до multiple definition, бо заголовок підключається в кілька одиниць трансляції, і кожна отримує своє визначення. Це як роздрукувати один і той самий «паспорт» у двох офісах і спробувати пройти контроль. Такі конфлікти напряму впираються в правило одного визначення — ODR.

Помилка № 4: розійшлися оголошення й визначення, але .cpp не підключає свій .hpp.
Це тиха міна. Ви змінили сигнатуру в .hpp, забули змінити її в .cpp, а потім дивуєтеся undefined reference. Якби .cpp підключав свій .hpp, компілятор спіймав би проблему одразу. Тому «підключай свій заголовок» — це не просто стиль, а практичний захист від помилок збирання.

Помилка № 5: намагатися «перемогти» лінкування додаванням #include.
Іноді людина бачить undefined reference і думає: «мабуть, я не той заголовок підключив». Але #include впливає на компіляцію, а не на наявність визначення у збиранні. Заголовок дає оголошення, а лінкування вимагає реалізацію. Можна підключити заголовок десять разів — визначення від цього не зʼявиться, а проблем, навпаки, може стати більше.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ