Перейти к содержимому
Теория · Сборка и препроцессор · урок 6 из 8

Ошибки компоновки и ODR

Заголовок triple.h под защитой #pragma once содержит не объявление, а определение функции: int triple(int x) { return x * 3; }. Его подключают два файла — report.cpp и вот этот main.cpp:

C++
#include <iostream>
#include "triple.h"  // ✗

int main() {
    std::cout << triple(14) << '\n';
}

Каждый файл компилируется без ошибок, но сборка clang++ main.cpp report.cpp останавливается на компоновке:

TEXT
multiple definition of `triple(int)'

Правило одного определения

Компоновщик собирает в программу объектные файлы всех единиц трансляции. В конторе это переплётчик: он сшивает папки всех отделов в одну книгу. Каждая из двух единиц получила из заголовка своё определение triple, и в книге оказались две страницы с телом одной функции. Компоновщик не выбирает между ними, а останавливает сборку.

Правило одного определения, сокращённо ODR, это запрещает: в программе не больше одного определения функции, а у вызванной — ровно одно. #pragma once здесь не поможет: отметка стоит на одной папке, а копии лежат в разных единицах трансляции.

Переключите, что лежит в triple.h, и посмотрите, на каком шаге останавливается сборка.

ВживуюОпределение в заголовке#include: 0 из 2
файлы
triple.h
main.cpp
report.cpp
единицы трансляции
main.cpp
  1. #include <iostream>стандартный заголовок
  2. #include "triple.h"ждёт
  3. int main() {
  4. std::cout << triple(14) << '\n';
  5. }
препроцессор ещё работает
report.cpp
  1. #include "triple.h"ждёт
  2. int report() {
  3. return triple(2);
  4. }
препроцессор ещё работает
компоновка — после компиляции

Препроцессор идёт по строкам #include сверху вниз и вставляет вместо каждой текст файла. Нажмите кнопку.

Сравните объявление, определение и inline

Нарушить правило можно и с другой стороны. Если triple.h содержит только объявление, а triple.cpp не указали в команде сборки, все файлы скомпилируются: объявление есть в каждом. Тогда компоновщик сообщит undefined reference, а в задачах курса — undefined symbol. Он называет функцию, а не файл: найдите .cpp-файл с её определением и проверьте, что он есть в команде.

inline в заголовке

Если определение должно остаться в заголовке, функцию объявляют inline:

C++
#pragma once

inline int triple(int x) {
    return x * 3;
}

Слово inline разрешает определению функции повторяться в разных единицах трансляции, если все копии одинаковы. Копии из одного заголовка одинаковы всегда, и компоновщик оставляет одну из них. Функции constexpr тоже считаются inline, поэтому их определяют прямо в заголовках. Остальные функции обычно делят: объявление — в заголовке, определение — в .cpp-файле.

Частая ошибка

Вместо заголовка подключили .cpp-файл:

C++
#include <iostream>
#include "triple.cpp"  // ✗

int main() {
    std::cout << triple(14) << '\n';
}

Сам main.cpp компилируется, но сборка clang++ main.cpp triple.cpp снова заканчивается ошибкой multiple definition. Определение triple попало в две единицы трансляции: в triple.cpp и в main.cpp, куда его вставил #include. Подключайте заголовок triple.h, а .cpp-файлы передавайте только команде сборки. В задачах курса тесты тоже подключают .cpp-файл с решением, но там вся сборка — одна единица трансляции.

Главное

  • Компоновщик собирает объектные файлы всех единиц трансляции в одну программу.
  • По правилу одного определения у вызванной функции в программе ровно одно определение.
  • Определение в заголовке без inline попадает в каждую единицу и даёт multiple definition.
  • Забытый в команде сборки .cpp-файл даёт undefined reference, а в задачах курса — undefined symbol.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом