Внутренняя и внешняя линковка в C++

http://www.goldsborough.me/c/c++/linker/2016/03/30/19-34-25-internal_and_external_linkage_in_c++/
  • Перевод
Всем добрый день!

Представляем вам перевод интересной статьи, который подготовили для вас рамках курса «Разработчик C++». Надеемся, что она будет полезна и интересна для вас, как и нашим слушателям.

Поехали.

Сталкивались ли вы когда-нибудь с терминами внутренняя и внешняя связь? Хотите узнать, для чего используется ключевое слово extern, или как объявление чего-то static влияет на глобальную область? Тогда эта статья для вас.

В двух словах

В единицу трансляции включены файл реализации (.c/.cpp) и все его заголовочные файлы (.h/.hpp). Если внутри единицы трансляции у объекта или функции есть внутреннее связывание, то этот символ виден компоновщику только внутри этой единицы трансляции. Если же у объекта или функции есть внешнее связывание, то компоновщик сможет видеть его при обработке других единиц трансляции. Использование ключевого слова static в глобальном пространстве имен дает символу внутреннее связывание. Ключевое слово extern дает внешнее связывание.
Компилятор по умолчанию дает символам следующие связывания:

  • Non-const глобальные переменные — внешнее связывание;
  • Const глобальные переменные — внутреннее связывание;
  • Функции — внешнее связывание.



Основы

Поговорим сначала о двух простых концепциях, необходимых для обсуждения связывания.

  • Разница между объявлением и определением;
  • Единицы трансляции.

Также обратите внимание на названия: мы будем использовать понятие “символ”, когда речь идет о любой “сущности кода”, с которой работает компоновщик, например с переменной или функцией (или с классами/структурами, но на них мы не будем акцентироваться).

Объявление VS. Определение

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

В некоторых ситуациях компилятору недостаточно объявления, например, когда элемент данных класса имеет тип ссылки или значения (то есть не ссылка, и не указатель). В то же время, разрешен указатель на объявленный (но неопределенный) тип, так как ему нужен фиксированный объем памяти (например, 8 байт в 64-битных системах), не зависящий от типа, на который указывает. Чтобы получить значение по этому указателю, потребуется определение. Также для объявления функции нужно объявить (но не определить) все параметры (не важно взятые ли по значению, ссылке или указателю) и возвращаемый тип. Определение типа возвращаемого значения и параметров необходимо только для определения функции.

Функции

Разница между определением и объявлением функции весьма очевидна.

int f();               // объявление
int f() { return 42; } // определение

Переменные

С переменными все немного иначе. Объявление и определение обычно не разделяются. Главное, что это:

int x;

Не только объявляет x, но и определяет его. Происходит это благодаря вызову дефолтного конструктора int. (В C++ в отличие от Java, конструктор простых типов (таких как int) по умолчанию не инициализирует значение в 0. В примере выше х будет иметь равен любому мусору, лежащему в адресе памяти, выделенном компилятором).

Но вы можете явно разделить объявление переменной и ее определение при помощи ключевого слова extern.

extern int x; // объявление
int x = 42;   // определение

Однако, при инициализации и добавлении extern к объявлению, выражение превращается в определение и ключевое слово extern становится бесполезным.

extern int x = 5; // то же самое, что и 
int x = 5;

Предварительное Объявление

В C++ существует концепция предварительного объявления символа. Это значит, что мы объявляем тип и имя символа для использования в ситуациях, не требующих его определения. Так нам не понадобится включать полное определение символа (обычно — заголовочный файл) без явной необходимости. Тем самым, мы снижаем зависимость от файла, содержащего определение. Главное преимущество — при изменении файла с определением, файл, где мы предварительно объявляем этот символ, не потребует повторной компиляции (а значит, и все прочие файлы его включающие).

Пример

Предположим, у нас есть объявление функции (называемое прототипом) для f, принимающее объект типа Class по значению:

// file.hpp

void f(Class object);

Сразу включить определение Class — наивно. Но так как мы пока только объявили f, достаточно предоставить компилятору объявление Class. Таким образом, компилятор сможет узнать функцию по ее прототипу, а мы сможем избавиться от зависимости file.hpp от файла, содержащего определение Class, скажем class.hpp:

// file.hpp

class Class;

void f(Class object);

Допустим, file.hpp содержится в 100 других файлах. И, допустим, мы меняем определение Class в class.hpp. Если вы добавим class.hpp в file.hpp, file.hpp и все 100 содержащих его файла будут должны перекомпилироваться. Благодаря предварительному объявления Class единственными файлами, требующими повторной компиляции, будут class.hpp и file.hpp (если считать, что f определен там).

Частота использования

Важное отличие объявления от определения состоит в том, что символ может быть объявлен много раз, но определен только однажды. Так вы можете предварительно объявить функцию или класс сколько угодно раз, но определение может быть только одно. Это называется Правилом Одного Определения. В C++ работает следующее:

int f();
int f();
int f();
int f();
int f();
int f();
int f() { return 5; }

А это не работает:

int f() { return 6; }
int f() { return 9; }

Единицы трансляции

Программисты обычно работают с заголовочными файлами и файлами реализации. Но не компиляторы — они работают с единицами трансляции (translation units, кратко — TU), которые иногда называют единицами компиляции. Определение такой единицы довольно простое — любой файл, переданный компилятору, после его предварительной обработки. Если быть точным, это файл, получаемый в результате работы препроцессора расширяющего макрос, включающего исходный код, который зависит от #ifdef и #ifndef выражений, и копипасты всех файлов #include.

Есть следующие файлы:

header.hpp:

#ifndef HEADER_HPP
#define HEADER_HPP

#define VALUE 5

#ifndef VALUE
struct Foo { private: int ryan; };
#endif

int strlen(const char* string);

#endif /* HEADER_HPP */

program.cpp:

#include "header.hpp"

int strlen(const char* string)
{
	int length = 0;

	while(string[length]) ++length;

	return length + VALUE;
}

Препроцессор выдаст следующую единицу трансляции, которая затем передается компилятору:

int strlen(const char* string);

int strlen(const char* string)
{
	int length = 0;

	while(string[length]) ++length;

	return length + 5;
}

Связи

Обсудив основы, можно приступить к связям. В целом, связь — это видимость символов для компоновщика при обработке файлов. Связь может быть либо внешней, либо внутренней.

Внешняя связь

Когда символ (переменная или функция) обладает внешней связью, он становится видимым компоновщикам из других файлов, то есть “глобально” видимым, доступным всем единицами трансляции. Это значит, что вы должны определить такой символ в конкретном месте одной единицы трансляции, обычно в файле реализации (.c/.cpp), так чтобы у него было только одно видимое определение. Если вы попытаетесь одновременно с объявлением символа выполнить его определение, или поместить определение в файл к объявлению, то рискуете разозлить компоновщик. Попытка добавить файл больше чем в один файл реализации, ведет к добавлению определения больше чем в одну единицу трансляции — ваш компоновщик будет плакать.

Ключевое слово extern в C и C++ (явно) объявляет, что у символа есть внешняя связь.

extern int x;
extern void f(const std::string& argument);

Оба символа имеют внешнюю связь. Выше отмечалось, что const глобальные переменные по умолчанию имеют внутреннее связывание, non-const глобальные переменные — внешнее. Это значит, что int x; — то же самое, что и extern int x;, верно? Не совсем. int x; на самом деле аналогичен extern int x{}; (используя синтаксис универсальной/скобочной инициализации, для избежания самого неприятного синтаксического анализа (the most vexing parse)), так как int x; не только объявляет, но и определяет x. Следовательно, не добавить extern к int x; глобально настолько же плохо, как определить переменную при объявлении ее extern:

int x;          // то же самое, что и 
extern int x{}; // скорее всего приведет к ошибке компоновщика.

extern int x;   // а это только объявляет целочисленную переменную, что нормально

Плохой Пример

Давайте объявим функцию f с внешней связью в file.hpp и там же определим ее:

// file.hpp

#ifndef FILE_HPP
#define FILE_HPP

extern int f(int x);

/* ... */

int f(int) { return x + 1; }

/* ... */

#endif /* FILE_HPP */

Обратите внимание, что добавлять здесь extern не нужно, так как все функции явно extern. Разделения объявления и определения тоже не потребуется. Поэтому давайте просто перепишем это следующим образом:

// file.hpp

#ifndef FILE_HPP
#define FILE_HPP

int f(int) { return x + 1; }

#endif /* FILE_HPP */

Такой код можно было бы написать до прочтения этой статьи, либо после ее чтения под воздействием алкоголя или тяжелых веществ (например, булочек с корицей).

Давайте посмотрим, почему так делать не стоит. Теперь у нас есть два файла реализации: a.cpp и b.cpp, оба включены в file.hpp:

// a.cpp

#include "file.hpp"

/* ... */


// b.cpp

#include "file.hpp"

/* ... */

Теперь пусть поработает компилятор и сгенерирует две единицы трансляции для двух файлов реализации выше (помните что #include буквально означает копировать/вставить):

// TU A, from a.cpp

int f(int) { return x + 1; }

/* ... */

// TU B, from b.cpp

int f(int) { return x + 1; }

/* ... */

На этом этапе вмешивается компоновщик (связывание происходит после компиляции). Компоновщик берет символ f и ищет определение. Сегодня ему повезло, он находит аж два! Одно в единице трансляции A, другое в B. Компоновщик замирает от счастья и говорит вам примерно следующее:

duplicate symbol __Z1fv in:
/path/to/a.o
/path/to/b.o

Компоновщик находит два определения для одного символа f. Поскольку у f есть внешнее связывание, он виден компоновщику при обработке и A, и B. Очевидно, это нарушает Правило Одного Определения и вызывает ошибку. Точнее это вызывает ошибку повторяющегося символа (duplicate symbol error), которую вы будете получать не реже, чем ошибку неопределенного символа (undefined symbol error), возникающую, когда вы объявили символ, но забыли определить.

Использование

Стандартным примером объявления переменных extern являются глобальные переменные. Предположим, вы работаете над самовыпекаемым тортом. Наверняка есть глобальные переменные, связанные с тортом, которые должны быть доступны в разных частях вашей программы. Допустим, тактовая частота съедобной схемы внутри вашего торта. Это значение естественно требуется в разных частях для синхронной работы всей шоколадной электроники. (Злой) C-способ объявления такой глобальной переменной имеет вид макроса:

#define CLK 1000000

Программист C++, испытывающий к макросам отвращение, лучше напишет настоящий код. Например такой:

// global.hpp

namespace Global
{
	extern unsigned int clock_rate;
}

// global.cpp
namespace Global
{
	unsigned int clock_rate = 1000000;
}

(Современный программист C++ захочет использовать разделительные литералы: unsigned int clock_rate = 1'000'000;)

Внутренняя Связь

Если у символа есть внутренняя связь, то он будет виден только внутри текущей единицы трансляции. Не путайте видимость с правами доступа, например private. Видимость означает, что компоновщик сможет использовать этот символ только при обработке единицы трансляции, в которой был объявлен символ, а не позже (как в случае символов с внешней связью). На практике, это значит, что при объявлении символа с внутренней связью в заголовочном файле, каждая единица трансляции, включающая в себя этот файл, получит уникальную копию этого символа. Как если бы вы предопределили каждый такой символ в каждой единице трансляции. Для объектов это значит, что компилятор будет буквально выделять совершенно новую, уникальную копию для каждой единицы трансляции, что, очевидно, может привести к высоким расходам памяти.

Для объявления символа с внутренней связью, в C и C++ существует ключевое слово static. Такое использование отличается от применения static в классах и функциях (или, в целом, в любых блоках).

Пример

Приведем пример:

header.hpp:

static int variable = 42;

file1.hpp:

void function1();

file2.hpp:

void function2();

file1.cpp:

#include "header.hpp"

void function1() { variable = 10; }


file2.cpp:

#include "header.hpp"

void function2() { variable = 123; }

main.cpp:

#include "header.hpp"
#include "file1.hpp"
#include "file2.hpp"

#include <iostream>

auto main() -> int
{
	function1();
	function2();

	std::cout << variable << std::endl;
}

Каждая единица трансляции, включающая header.hpp получает уникальную копию переменной, в силу наличия у нее внутренней связи. Есть три единицы трансляции:

  1. file1.cpp
  2. file2.cpp
  3. main.cpp

При вызове function1 копия переменной file1.cpp получает значение 10. При вызове function2 копия переменной file2.cpp получает значение 123. Однако, значение, которое выдается в main.cpp, не меняется и остается равным 42.

Анонимные пространства имен

В С++ существует другой способ объявления одного и более символов с внутренней связью: анонимные пространства имен. Такое пространство гарантирует, что символы, объявленные внутри него, видны только в текущей единице трансляции. По сути, это просто способ объявить несколько символов static. Какое-то время от использования ключевого слова static в целях объявления символа с внутренней связью отказались в пользу анонимных пространств имен. Однако, им снова стали пользоваться в силу удобства объявления одной переменной или функции с внутренней связью. Есть еще несколько незначительных отличий, на которых я не буду останавливаться.

В любом случае, это:

namespace { int variable = 0; }

Делает (почти) то же самое, что и:

static int variable = 0;

Использование

Так в каких же случаях пользоваться внутренними связями? Использовать их для объектов — плохая идея. Расход памяти больших объектов может быть очень высок из-за копирования под каждую единицу трансляции. Но, в основном, это просто вызывает странное, непредсказуемое поведение. Представьте, что у вас есть синглтон (класс, в котором вы создаете экземпляр только одного инстанса) и неожиданно появляется несколько инстансов вашего “синглтона” (по одному на каждую единицу трансляции).

Однако, внутреннюю связь можно использовать для скрытия из глобальной области локальных хелпер-функций единицы трансляции. Допустим, есть хелпер-функция foo в file1.hpp, которую вы используете в file1.cpp. В то же время у вас есть функция foo в file2.hpp, используемая в file2.cpp. Первая и вторая foo отличаются друг от друга, но вы не можете придумать другие имена. Поэтому вы можете объявить их static. Если вы не будете добавлять и file1.hpp, и file2.hpp в одну и ту же единицу трансляции, то это скроет foo друг от друга. Если этого не сделать, то они будут неявно иметь внешнюю связь и определение первой foo столкнется с определением второй, вызвав ошибку компоновщика о нарушении правила одного определения.

THE END

Вы всегда можете оставить свои комментарии и\или вопросы тут или зайти к нам на день открытых дверей.
Отус
336,00
Профессиональные онлайн-курсы для разработчиков
Поделиться публикацией

Похожие публикации

Комментарии 34

    +11
    Для полноты картины можно также поговорить о слабой связи, ключевое слово inline. Если применять этот вид связи можно также обходить One Definition Rule (например писать реализации функций в хедере), как и в случае со static. Но механизм тут другой.

    Когда используешь static, символы помечаются как внутрение, и One Definition Rule не нарушается. При inline символы остаются внешними (видны всем), но когда линковщик видит несколько одинаковых слабых символов, он оставляет только один из них, а остальные отбрасывает (как именно не стандартизированно, но 99%, что оставит тот, что увидел первым).

    Тут есть небольшой плюс в том, что в бинарник попадет только один экземпляр кода, а не N (размер бинарника меньше). Однако с этим нужно быть очень осторожным. Вы вполне можете случайно (ну или специально) сделать несколько реализаций inline функций с одинаковым именем. Тогда программа будет работать немного странно =) Вот тут я набросал такой пример, где вывод программы зависит от того, в каком порядке передавать линкеру object файлы.

    Ну и напоследок — все шаблонные функции обладают слабым связыванием.
      +3
      Добавьте еще в пример поведение со включенной оптимизацией…
        +1
        ооо, прикольно, об этом я не знал. То есть тут ещё и разное поведение в дебаге и релизе… В пример добавлю.

        Для остальных: при оптимизации компилятор может честно заинлайнить эти функции и вообще не добавить эту функцию в таблицу символов.

        Он это может сделать, видимо, потому-что несколько inline функций с разными реализациями — это Undefined behavior (не проверял, это мое предположение), а компилятор считает, что UB не существует => функции все одинаковые => если получится заинлайнить, то можно вообще функцию выбросить.
          +2
          Ну, на самом деле он это может сделать прежде всего потому что именно в этом и заключается смысл инлайна — в том, что функция вообще не должна попадать в бинарник.

          А все остальное: слабые символы, UB при их различии — добавлено уже для того чтобы этот самый inline стал возможен.
            0
            А все остальное: слабые символы, UB при их различии — добавлено уже для того чтобы этот самый inline стал возможен.
            Всё-таки это для обслуживания случая, когда inline невозможен добавлено было. Чтобы программа вдруг не превращалась из валидной в невалидную в зависимости от настроек оптимизатора…

            Но так-то да, «штатный» режим — это когда inline-функции в файл не попадают…
              0
              Я бы сказал так, inline был для того, чтобы код функции напрямую подставлять в код вызывающей функции. А вот должна ли она при этом оставаться в бинарнике мне не очень очевидно (я вообще зарекся использовать это слово вместе со словом оптимизация =) ).
            0
            Посылаю голову пеплом… Пока я писал комментарий о том как очевидно, что случится при оптимизации… для кого-то это оказалось неожиданностью… Так бывает…
            0
            Там как раз всё просто. Но вообще инлайн-функции и переменные с разными определениями в разных единицах компиляции — это неопределённое поведение. Вплоть до того, что легко представить случай, когда это приведёт и к пресловутому «rm -rf» (хотя не уверен, что можно заставить такое существующие компиляторы проделать).
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Увы, но не всегда.
            Хоть стандарт и требует, чтобы они такими были, но разработчики компиляторов иногда имеют свое мнение. Например компилятор TI для архитектуры C2000 не инициализирует статические локальные переменные. В мануале они это решение обьясняют желанием ускорить старт прошивки. Об инициализации предлагают пользователям самим озаботиться.
              +3
              Так это не компилятор должен делать, а среда выполнения перед запуском программы (для С в ISO/IEC 9899:TC3 5.1.2) Если у вас bare-metal, то это должно делаться start.S или его аналоге.
              Заголовок спойлера
              у нас, например, arm-none-eabi-gcc-6.3.1 кладет это в bss-секцию, которую руками очищаем при старте.
                0
                У вас неправильный код инициализации. Для GCC/ARM если вы код вот так в лоб запустите, то тоже не будет нулевых значений — нужен код для инициализации сегментов BSS и DATA. Обратите внимание, что у вас, скорее всего, ещё и начальные значение переменным не присваиваются после старта — явный признак того, что в стартап-коде чего-то не хватает.
                  0
                  Понятно, что это проблема в коде инициализации. Но дело в том, что все что исполняется до main() поставляется производителем контроллера. Мы, иногда даже не видим, что работает раньше, а если и видим то иногда менять не имеем права. Описанное поведение, оказалось не проблеммой, а намерянными действиями производителя тулчейна.
                  Собственно мой коментарий был только о том, что если у вас нет контроля над тем что исполняется до main() то не стоит надеяться, что нстатические локальные переменные будут всегда проинициализированными нулем. Более переносимое решение, делять явную инициализацию.
              0
              Предположим есть функция загрузки изображения LoadImage которая по расширению файла определяет какой формат изображения будет использоваться (например jpg,png,tiff,webp,...) и реализация лежит в статической библиотеке в куче объектных фалов. Как в таких случаях принято в современном C++ указывать линковщику какие форматы файлов я хочу чтобы прилинковались, а какие нет.
                +1
                В С++ нет динамического связывания. Так что или через явное использование функций (тогда неиспользуемые не линкуются), или через макросы.
                  0
                  Можно через реестр и глобальные конструкторы, но с библиотеками это работать не будет, нужно оставлять (и линковать) объектные файлы.

                  Можно реализацию — в библиотеку, а в микрообъектых файлах в каждом только функцию-конструктор, которая зарегистриует формат и всё.
                    0
                    Мы про линковку. Предлагаемые вами приседания на неё не влияют. Ну разве что вы этих «микрообъектных» файлах прикрутите динамическую загрузку библиотек и создание прокси-объектов к ним, что не портабильно никак и является весьма низким уровнем системного программирования.
                      0
                      Вы сейчас серьёзно или издеваетесь? Вы хотите что вот так сделать не умеете:

                      $ g++ -c *.cc
                      $ g++ main.o registry.o gif.o jpg.o png.o -o all-formats
                      $ ./all-formats 
                      gif
                      jpg
                      png
                      $ g++ main.o registry.o gif.o -o gif-only
                      $ ./gif-only 
                      gif
                      

                      Мне казалось, что так любой уважающий себя программист на C++ должен такое уметь. Proof-of-concept — под спойлером, грязноватый, правда, но суть передаёт. Ну элементарно же! Всё в рамках стандарта…
                      sources
                      formats.h
                      #ifndef FORMATS_H
                      #define FORMATS_H
                      
                      extern void RegisterFormat(const char*);
                      
                      class Registered {
                       public:
                        Registered(const char* format) {
                          RegisterFormat(format);
                        }
                      };
                      
                      extern void PrintRegisteredFormats();
                      
                      #endif
                      

                      registry.cc
                      #include <iostream>
                      
                      #include "formats.h"
                      
                      namespace {
                      
                      const char* formats[10];
                      int format_count = 0;
                      
                      }
                      
                      void RegisterFormat(const char* format) {
                        formats[format_count++] = format;
                      }
                      
                      void PrintRegisteredFormats() {
                        for (int i = 0; i < format_count; ++i) {
                          std::cout << formats[i] << std::endl;
                        }
                      }
                      

                      main.cc
                      #include "formats.h"
                      
                      int main() {
                        PrintRegisteredFormats();
                      }
                      

                      gif.cc
                      #include "formats.h"
                      
                      static Registered format("gif");
                      

                      jpg.cc
                      #include "formats.h"
                      
                      static Registered format("jpg");
                      

                      png.cc
                      #include "formats.h"
                      
                      static Registered format("png");
                      

                      registry.o и всё реализацию — можно в библиотеку засунуть, но «ключевые» файлы gif.o/jpg.o/png.o — нельзя (это уже не стандарт C++, это уже устройство библиотек… но они, в общем, на всех платформах одинаково работают).
                        0

                        не прочел последний абзац, пришлось все удалить :)

                          0
                          Да, я сразу как-то не понял, видимо.
                          Но оно компиляторо- и платформозависимо и вообще не по стандарту.
                          Например, линкер имеет полное право выбросить ваш static объект, как неиспользуемый, нет? На практике он этого, скорее всего, не сделает, из-за того, что это традиционные костыли, но всё же. Порядок инициализации реестров и самого реестра, тоже зависит от порядка линковки и фаз Луны.
                          И не дай Бог где-то что-то забыл, какие-то горизонтальные связи, и всё — у тебя тихо-мирно ликнуется куча неиспользуемого мусора, о котором ты даже и не подозреваешь.
                          Ещё можно через weak-references сделать. Тоже компиляторо-зависимо, но все поддерживают.
                            0
                            Но оно компиляторо- и платформозависимо и вообще не по стандарту.
                            Что значит «не по стандарту»? Если эти файлы, каким-то образом, окажутся частью вашей программу — то они таки отработают ровно как написано — в полном соответствии со стандартом.

                            А как работают библиотеки — это да, «не по стандарту»… ну так в стандарте библиотеки вообще не рассматриваются. Вообще. Никак. В принципе. Как определяется какие файлы в программу входят, какие нет — стандарт не описывает совсем.

                            Собственно именно поэтому так тяжело ввести модули в язык — он традиционно не указывал в этом месте ничего и у разных производителей, разумеется, разные идеи.

                            Например, линкер имеет полное право выбросить ваш static объект, как неиспользуемый, нет?
                            Нет. Если вы задали объектный файл в качестве аргумента — то вы, тем самым, чётко дали понять что уж как минимум содержимое этого объектного файла вы хотите в программе увидеть. Иначе непонятно что мешало бы линкеру и все другие файлы, которые компилятор ему подсовывает (всякие crtbeginS.o и crtendS.o) выкинуть.

                            На практике он этого, скорее всего, не сделает, из-за того, что это традиционные костыли, но всё же.
                            Оличная идея: взять функцию, ради которой программы была изначально создана — и объявить её «костылями».

                            «Возьми вот эти вот объектные файлы, сложи их вместе и свяжи их между собой» — это то, чем линкер занимался ещё до моего (и, скорее всего, вашего) рождения в OS/360 и RSX-11. Да-да, FORTRAN 66 и всё вот это вот. В те времена он даже назывался «link editor», потому что он вообще не решал никаких других задач: все объектники, которые он получал он «склеивал» друг за другом в большой модули и «связывал» вместе переменные из разных модулей. И всё.

                            Да, потом на него навесили разные дополнительные задачи, научили работать с библиотеками, выкидывать ненужный код (всякие --gc-sections)… но основную работу у него никто пока не отбирал… уж очень много на это поведение всего завязано.

                            И не дай Бог где-то что-то забыл, какие-то горизонтальные связи, и всё — у тебя тихо-мирно ликнуется куча неиспользуемого мусора, о котором ты даже и не подозреваешь.
                            Ну это уже другая история.

                            Ещё можно через weak-references сделать. Тоже компиляторо-зависимо, но все поддерживают.
                            А вот это — уже выходит за рамки стандарта. Хотя на практике, зачастую, удобнее, да.
                              0
                              Не по стандарту, это порядок инициализации статическикй переменных между модулями и поведение компилятора/линкера в случае неиспользуемых объектов.
                                0
                                поведение компилятора/линкера в случае неиспользуемых объектов
                                Это вне рамок стандарта просто — но почти все известные науке (и точно все популярные) действуют одинаково.

                                порядок инициализации статическикй переменных между модулями
                                А это как раз неважно — гарантируются что все они будут вызваны до начала работы основной программы, а как сделать так, чтобы от порядка вызова ничего не зависело — уже дело техники.
                    +4
                    Уже пяток вариантов ответа удалил :)

                    Понял, что все они зависят от ответа на вопрос «Как и где реализована связь между расширением и форматом?»
                    0
                    Джим Керри не произнес букву Р .)
                      –10
                      Вопрос к уважаемому автору: почему вместо устоявшегося в литературе термина «связывание» Вы используется англицизм «линковка»? Слова такого в русском языке вроде не наблюдается, и звучит как «приспособа», чем-то из токарно-столярного дела отдает.
                        +6
                        Да? А я думал что линковка это и есть устоявшийся термит.
                          +3
                          Не уверен, что речь об одном и том же.

                          Препроцессор препроцессит (никогда так не говорю), компилятор компилирует, линковщик (или линкер?) линкует.

                          Линковка это низкоуровневый процесс, который дает нам то, что можно назвать ранним связыванием.

                          Линковку можно выполнить в рантайме, при загрузке динамической библиотеки и получить позднее связывание.

                          Позднее связывание можно получить и при использовании статической линковки, например, при использовании указателей на функции или виртуальных методов. Линковка уже прошла, но какая именно реализация под указателем будет понятно в рантайме (во время работы), на этом построены многие паттерны (шаблоны уже заняты).

                          Пусть уж лучше линковка останется линковкой. Уже были переводы applet как «приложеньице». У нас же нет цели стихи про березы и линковку писать :)
                            –8

                            Разве есть такое слово "линковщик"? Компиляция и компилятор — это слова русского языка, а вот препроцессор с линковщиком — нет. Кстати, почему линковщиком, а не линкование? Или ещё хорошее слово: "препроцессировка", здорово, правда? Не знаю, есть английский глагол to link, по нашему связывать. Назначение программы linker состоит в связывании различных модулей компиляции в модуль исполнения, где тут "линковка" совершенно непонятно. Предлагаю ввести в обиход слова: мапировка, хэшировка, лукапирование и т.п.

                              +3
                              Компиляция и компилятор — это слова русского языка, а вот препроцессор с линковщиком — нет.
                              И «препроцессор» и «линкер» — достаточно часто встречаются даже и в документации, а уж в статьях — так и подавно. Статья Препроцессор Си даже в энциклопупии есть. И линковка, кстати, тоже: Компоновщик (также редактор связей, от англ. link editor, linker) — инструментальная программа, которая производит компоновку («линковку»).

                              А на слово линкер аж два варианта «на выбор» предлагается: «синтетический олигонуклеотид, присоединяемый с помощью ДНК-лигазы к фрагменту ДНК для того, чтобы придать концам этого фрагмента структуру с заданными свойствами» и, собственно «компоновщик» (думаю быть ему линкером в конце-концов).

                              Предлагаю ввести в обиход слова: мапировка, хэшировка, лукапирование и т.п.
                              Будут достаточно часто нужны — появятся. Хватит уже этой борьбы за мокроступы.
                                +2
                                Тут есть две темы:

                                — расширение русского языка новыми терминами — могу поучаствовать как тролль, но лучше пропущу, хотя это хорошая и серьезная тема

                                — разделение понятий связывания имен на стадии ld и связывание конкретных объектов во время исполнения. Так получилось, что первая стадия просто исторически часто называется линковкой, а вторая стадия (точнее даже не стадия, а трактовка поведения), более свежая и молодая уже именуется поздним/ранним связыванием. Проблема такая же как и в случае с шаблонами проектирования, которые стали путаться с шаблонами языка. Зато теперь у нас есть прекрасные русские слова «паттерн» и «темплейт» :)
                            0
                            мимо
                              0

                              Еще есть слабое связывание через weak атрибут

                                0
                                За рамками стандарта вообще много чего есть — но это уже отдельную статью писать нужно.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое