6.7 – Внешнее связывание

Добавлено 11 мая 2021 в 16:22

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

Идентификатор с внешним связыванием можно видеть и использовать как из файла, в котором он определен, так и из других исходных файлов (с помощью предварительного объявления). При этом идентификаторы с внешним связыванием действительно «глобальны» в том смысле, что их можно использовать в любом месте вашей программы!

По умолчанию функции имеют внешнее связывание

В уроке «2.7 – Программы с несколькими файлами исходного кода» вы узнали, что вы можете вызывать функцию, определенную в одном файле, из другого файла. Это потому, что функции по умолчанию имеют внешнее связывание.

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

Вот пример:

a.cpp:

#include <iostream>
 
void sayHi() // эта функция имеет внешнее связывание, и ее можно видеть в других файлах
{
    std::cout << "Hi!";
}

main.cpp:

void sayHi(); // предварительное объявление для функции sayHi,
              // делает sayHi доступной в этом файле
 
int main()
{
    sayHi(); // вызов функции, определенной в другом файле,
             // компоновщик подключит этот вызов к определению функции
 
    return 0;
}

Приведенная выше программа напечатает:

Hi!

В приведенном выше примере предварительное объявление функции sayHi() в main.cpp позволяет main.cpp получить доступ к функции sayHi(), определенной в a.cpp. Предварительное объявление удовлетворяет компилятор, а компоновщик может связать вызов функции с определением функции.

Если бы функция sayHi() имела вместо этого внутреннее связывание, компоновщик не смог бы связать вызов функции с определением функции, что привело бы к ошибке линковки.

Глобальные переменные с внешним связыванием

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

int g_x { 2 }; // неконстантные глобальные переменные по умолчанию являются внешними
 
extern const int g_y { 3 };     //  Глобальные переменные const можно определить как extern, 
                                // что делает их внешними.
extern constexpr int g_z { 3 }; //  Глобальные объекты constexpr могут быть определены как extern,
                                // что делает их внешними (но это бесполезно, смотрите примечание
                                // в следующем разделе)
 
int main()
{
    return 0;
}

Неконстантные глобальные переменные по умолчанию являются внешними (если используется ключевое слово extern, оно игнорируется).

Предварительные объявления переменных через ключевое слово extern

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

Ниже показан пример использования предварительного объявления переменной:

a.cpp:

// определения глобальных переменных
int g_x { 2 };              // неконстантные глобальные переменные по умолчанию 
                            // имеют внешнее связывание

extern const int g_y { 3 }; // этот extern дает g_y внешнее связывание

main.cpp:

#include <iostream>
 
extern int g_x; // этот extern является предварительным объявлением переменной с именем g_x, 
                // которая определена где-то еще

extern const int g_y; // этот extern является предварительным объявлением константной переменной 
                      // с именем g_y, которая определена где-то еще
 
int main()
{
    std::cout << g_x; // напечатает 2
 
    return 0;
}

В приведенном выше примере a.cpp и main.cpp ссылаются на одну и ту же глобальную переменную с именем g_x. Таким образом, даже если g_x определена и инициализирована в a.cpp, мы можем использовать ее значение в main.cpp через предварительное объявление g_x.

Обратите внимание, что ключевое слово extern в разных контекстах имеет разное значение. В некоторых контекстах extern означает «дать этой переменной внешнее связывание». В других контекстах extern означает «это предварительное объявление для внешней переменной, которая определена где-то еще». Да, это сбивает с толку, поэтому мы суммируем все эти использования в уроке «6.11 – Резюме по области видимости, продолжительности и связывании».

Предупреждение


Если вы хотите определить неинициализированную неконстантную глобальную переменную, не используйте ключевое слово extern, иначе C++ подумает, что вы пытаетесь выполнить предварительное объявление переменной.

Предупреждение


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

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

// неконстанты
int g_x;        // определение переменной (при желании может иметь инициализатор)
extern int g_x; // предварительное объявление (без инициализатора)
 
// константы
extern const int g_y { 1 }; // определение переменной (const требует инициализатора)
extern const int g_y;       // предварительное объявление (без инициализатора)

Область видимости файла и глобальная область видимости

Термины «область видимости файла» и «глобальная область видимости» склонны вызывать путаницу, и отчасти это связано с тем, как они используются неофициально. Технически в C++ все глобальные переменные имеют «область видимости файла», а свойство связывания определяет, могут ли они использоваться в других файлах или нет.

Рассмотрим следующую программу:

global.cpp:

int g_x { 2 }; // внешнее связывание по умолчанию
// g_x здесь выходит из области видимости

main.cpp:

extern int g_x; // предварительное объявление для g_x - g_x можно использовать 
                // в этом файле, начиная с этого места
 
int main()
{
    std::cout << g_x; // должно напечатать 2
 
    return 0;
}
// предварительное объявление g_x здесь выходит из области видимости

Переменная g_x имеет область видимости файла в global.cpp – ее можно использовать от точки определения до конца файла, но ее нельзя непосредственно увидеть за пределами global.cpp.

Внутри main.cpp предварительное объявление g_x также имеет область видимости файла – ее можно использовать от точки объявления до конца файла.

Однако неформально термин «область видимости файла» чаще применяется к глобальным переменным с внутренним связыванием, а «глобальная область видимости» – к глобальным переменным с внешним связыванием (поскольку они, с соответствующими предварительными объявлениями, могут использоваться во всей программе).

Проблема порядка инициализации глобальных переменных

Инициализация глобальных переменных происходит при запуске программы перед выполнением функции main. Это происходит в два этапа.

Первый этап называется статической инициализацией. На этапе статической инициализации глобальные переменные с инициализаторами constexpr (включая литералы) инициализируются своими значениями. Кроме того, глобальные переменные без инициализаторов инициализируются нулем.

Второй этап называется динамической инициализацией. Этот этап более сложен и имеет больше нюансов, но суть его в том, что инициализируются глобальные переменные с инициализаторами, отличающимися от constexpr.

Вот пример инициализатора, не являющегося constexpr:

int init()
{
    return 5;
}
 
int g_something{ init() }; // инициализация не constexpr

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

#include <iostream>
 
int initx();  // предварительное объявление
int inity();  // предварительное объявление
 
int g_x{ initx() }; // g_x инициализируется первой
int g_y{ inity() };
 
int initx()
{
    return g_y; // g_y при этом вызове не инициализирована
}
 
int inity()
{
  return 5;
}
 
int main()
{
    std::cout << g_x << ' ' << g_y << '\n';
}

Этот код напечатает:

0 5

Гораздо более серьезной проблемой является то, что не определен порядок инициализации для нескольких файлов. При наличии двух файлов, a.cpp и b.cpp, любой из них может первым инициализировать свои глобальные переменные. Это означает, что если переменные в a.cpp зависят от значений в b.cpp, существует 50%-ная вероятность того, что эти переменные будут еще не инициализированы.

Предупреждение


Динамическая инициализация глобальных переменных вызывает в C++ множество проблем. По возможности избегайте ее.

Краткое резюме

// Определения внешних глобальных переменных:
int g_x;                       //  Определяет неинициализированную внешнюю глобальную
                               // переменную (по умолчанию инициализируется нулем)
extern const int g_x{ 1 };     //  Определяет инициализированную константную внешнюю
                               // глобальную переменную
extern constexpr int g_x{ 2 }; //  Определяет инициализированную внешнюю глобальную
                               // переменную constexpr
 
// Перенаправить объявления
extern int g_y;                //  Предварительное объявление для константной глобальной переменной
extern const int g_y;          //  Предварительное объявление для глобальной переменной const
extern constexpr int g_y;      //  Не допускается: переменные constexpr не могут быть
                               // предварительно объявлены

Мы дадим исчерпывающее резюме в уроке «6.11 – Резюме по области видимости, продолжительности и связывании».

Небольшой тест

Вопрос 1

В чем разница между областью видимости, продолжительностью и связыванием переменной? Какие область видимости, продолжительность и связывание имеют глобальные переменные?

Область видимости определяет, где доступна переменная. Продолжительность определяет, где создается и уничтожается переменная. Связывание определяет, можно ли экспортировать переменную в другой файл или нет.

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

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

Глобальные переменные могут получить внутреннее или внешнее связывание с помощью ключевых слов static и extern соответственно.

Теги

C++ / CppexternLearnCppВнешнее связываниеГлобальная переменнаяДля начинающихОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.