Какую задачу решает линкер (компоновщик) в процессе сборки C++ программы?

Ответ

Линкер — это программа, которая принимает на вход один или несколько объектных файлов (.o, .obj), полученных от компилятора, и библиотеки, а на выходе создаёт единый исполняемый файл или библиотеку. Его ключевая задача — разрешение внешних ссылок.

Конкретные функции линкера:

  1. Связывание (Resolution): Объектный файл содержит «ссылки» на имена функций и глобальных переменных, определённых в других файлах или библиотеках (например, cout из стандартной библиотеки). Линкер находит эти определения и подставляет правильные адреса.
  2. Объединение секций: Код из разных .o файлов собирается в секцию .text, данные — в .data или .bss и т.д.
  3. Распределение адресов: Назначает окончательные виртуальные адреса для всех частей программы.

Типичные ошибки линкера:

  • undefined reference to 'symbol': Линкер не нашёл определение для объявленного символа. Причина: забыли подключить объектный файл или библиотеку, опечатка в имени.
  • multiple definition of 'symbol': Обнаружено более одного определения одного символа. Частая причина — определение функции в заголовочном файле без inline или static, который включили в несколько .cpp-файлов.

Пример:

// math_utils.h
#pragma once
extern int global_counter; // Объявление глобальной переменной
int add(int a, int b);     // Объявление функции

// math_utils.cpp
#include "math_utils.h"
int global_counter = 0;    // Определение глобальной переменной
int add(int a, int b) {    // Определение функции
    return a + b;
}

// main.cpp
#include "math_utils.h"
#include <iostream>
int main() {
    global_counter++;
    std::cout << add(5, 3); // Вызов функции, определённой в другом файле
    return 0;
}

Линкер соберёт main.o и math_utils.o, связав вызов add и обращение к global_counter с их определениями.

Ответ 18+ 🔞

Давай разберёмся с этим линкером, а то у меня от этих ошибок уже волосы дыбом встают. Представь, что у тебя есть куча деталек от конструктора — это твои объектные файлы (.o). Компилятор их наштамповал и вывалил тебе на ковёр. А линкер — это такой мужик с похмелья, который должен из этой кучи собрать одну работающую машинку, то есть исполняемый файл.

Его главная работа — разрешить внешние ссылки. Это когда одна деталька кричит: «Эй, а где тут болтик М5?», а другая орет: «А мне нужна гаечка от cout!». Линкер ползает по всей куче, сопит, ищет эти болтики-гаечки в других объектниках или в библиотеках, и скручивает всё вместе. Если не находит — начинается: «undefined reference», и всё, пиздец, машинка не едет.

Что он конкретно делает, этот чувак:

  1. Связывает (Resolution): Это самое главное. Ты в main.cpp написал add(5, 3), а где тело этой add? В другом файле, math_utils.cpp. Компилятор в main.o оставил записку: «Найти функцию add». Линкер берёт эту записку, идёт в math_utils.o, находит там готовый кусок кода и говорит: «Вот же она, сука, бери адрес и подставляй». То же самое с глобальными переменными.
  2. Объединяет секции: Код из всех .o файлов он сгребает в одну кучу (секция .text), глобальные переменные — в другую (.data), неинициализированные — в третью (.bss). Получается аккуратный расклад, а не свалка.
  3. Раздаёт адреса: Он как почтальон в новом районе — назначает каждой функции и каждой переменной их окончательный, уникальный адрес в памяти будущей программы. Чтобы когда программа запустится, процессор знал, куда прыгать.

Типичные косяки, которые он выдает (и от которых хочется биться головой об стену):

  • undefined reference to 'symbol': Классика жанра! Это значит линкер облазил всё, но так и не нашёл, где определён этот самый symbol. Причины — ебанько: либо забыл добавить в сборку нужный .cpp файл или библиотеку (-lm, например), либо опечатался в имени, либо объявил функцию как extern, но так и не написал её реализацию. Чувство доверия к себе после такой ошибки — ноль ебать.
  • multiple definition of 'symbol': А вот это обратная, не менее весёлая ситуация. Линкер нашёл аж несколько определений одного и того же символа. Чаще всего так бывает, когда ты зачем-то определение функции (то есть её тело с { }) запихнул в заголовочный файл (*.h), и этот хедер включили (#include) в несколько .cpp файлов. В итоге каждый .cpp файл скомпилировал свою копию этой функции, и линкер охуел: «Мужики, а какую из них мне брать-то?». Спасение — делать функцию inline или static, или просто выносить определение в .cpp.

Ну и пример, чтобы совсем понятно стало, ёпта:

// math_utils.h
#pragma once
extern int global_counter; // Просто объявление: "Где-то есть такая переменная"
int add(int a, int b);     // Объявление: "Где-то есть такая функция"

// math_utils.cpp
#include "math_utils.h"
int global_counter = 0;    // А вот это уже определение. Вот она, переменная!
int add(int a, int b) {    // И это определение. Вот оно, тело функции!
    return a + b;
}

// main.cpp
#include "math_utils.h"
#include <iostream>
int main() {
    global_counter++; // Ссылка на переменную из другого файла
    std::cout << add(5, 3); // Вызов функции из другого файла
    return 0;
}

Компилятор честно сделает два объектника: main.o и math_utils.o. В main.o будут дырки: «Ищу global_counter» и «Ищу add». А потом приползёт линкер, возьмёт оба файла, найдёт в math_utils.o и переменную, и функцию, заткнёт эти дырки правильными адресами и склеит всё в один исполняемый файл. Если math_utils.o ему не дать — будет истерика undefined reference, ядрёна вошь. Вот и вся магия, без ебушки-воробушки.