6.7 – Внешнее связывание
В предыдущем уроке (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
соответственно.