12.13 – Статические переменные-члены
Обзор использования ключевого слова static
Из урока, посвященного области видимости файла и ключевому слову static
, вы узнали, что статические переменные сохраняют свои значения и не уничтожаются даже после того, как выходят за пределы области видимости. Например:
#include <iostream>
int generateID()
{
static int s_id{ 0 };
return ++s_id;
}
int main()
{
std::cout << generateID() << '\n';
std::cout << generateID() << '\n';
std::cout << generateID() << '\n';
return 0;
}
Эта программа печатает:
1
2
3
Обратите внимание, что переменная s_id
сохраняла свое значение на протяжении нескольких вызовов функции.
При применении к глобальным переменным ключевое слово static
имеет другое значение – оно дает им внутреннее связывание (что ограничивает их видимость/использование вне файла, в котором они определены). Поскольку глобальных переменных обычно избегают, ключевое слово static
в этом качестве используется не часто.
Статические переменные-члены
В C++ представлены еще два использования ключевого слова static
при применении к классам: статические переменные-члены и статические функции-члены. К счастью, эти способы использования довольно просты. В этом уроке мы поговорим о статических переменных-членах, а в следующем – о статических функциях-членах.
Прежде чем мы перейдем к ключевому слову static
применительно к переменным-членам, рассмотрим сначала следующий класс:
class Something
{
public:
int m_value{ 1 };
};
int main()
{
Something first;
Something second;
first.m_value = 2;
std::cout << first.m_value << '\n';
std::cout << second.m_value << '\n';
return 0;
}
Когда мы создаем экземпляр объекта класса, каждый объект получает свою собственную копию всех обычных переменных-членов. В этом случае, поскольку мы объявили два объекта класса Something
, мы получаем две копии m_value
: first.m_value
и second.m_value
. first.m_value
отличается от second.m_value
. Следовательно, приведенная выше программа печатает:
2
1
Статическими переменные-члены класса можно сделать с помощью ключевого слова static
. В отличие от обычных переменных-членов, статические переменные-члены используются всеми объектами класса. Рассмотрим следующую программу, похожую на приведенную выше:
class Something
{
public:
static int s_value;
};
int Something::s_value{ 1 };
int main()
{
Something first;
Something second;
first.s_value = 2;
std::cout << first.s_value << '\n';
std::cout << second.s_value << '\n';
return 0;
}
Эта программа создает следующий вывод:
2
2
Поскольку s_value
является статической переменной-членом, то она используется совместно всеми объектами класса. Следовательно, first.s_value
– это та же переменная, что и second.s_value
. Приведенная выше программа показывает, что значение, которое мы установили, используя объект first
, можно получить, используя объект second
!
Статические члены не связаны с объектами класса
Хотя вы можете получить доступ к статическим членам через объекты класса (как показано с помощью first.s_value
и second.s_value
в приведенном выше примере), оказывается, что статические члены существуют, даже если объекты класса не были созданы! Как и глобальные переменные, они создаются при запуске программы и уничтожаются при завершении программы.
Следовательно, лучше думать о статических членах как о принадлежащих самому классу, а не его объектам. Поскольку s_value
существует независимо от каких-либо объектов класса, к нему можно получить доступ напрямую, используя имя класса и оператор разрешения области видимости (в данном случае Something::s_value
):
class Something
{
public:
// объявляет статическую переменную-член
static int s_value;
};
// определяет статическую переменную-член
// (мы обсудим это в разделе ниже)
int Something::s_value{ 1 };
int main()
{
// обратите внимание: мы не создаем какие-либо
// экземпляры объектов типа Something
Something::s_value = 2;
std::cout << Something::s_value << '\n';
return 0;
}
В приведенном выше фрагменте s_value
указывается через имя класса, а не через объект. Обратите внимание, что мы даже не создали экземпляр объекта типа Something
, но мы всё же можем получить доступ и использовать Something::s_value
. Этот метод предпочтителен для доступа к статическим членам.
Определение и инициализация статических переменных-членов
Когда мы объявляем статическую переменную-член внутри класса, мы сообщаем компилятору о существовании статической переменной-члена, но на самом деле не определяем ее (это очень похоже на предварительное объявление). Поскольку статические переменные-члены не являются частью отдельных объектов класса (они обрабатываются аналогично глобальным переменным и инициализируются при запуске программы), вы должны явно определить статический член вне класса в глобальной области видимости.
В приведенном выше примере мы делаем это с помощью этой строки:
int Something::s_value{ 1 }; // определяет статическую переменную-член
Эта строка служит двум целям: она создает экземпляр статической переменной-члена (аналогично случаю с глобальной переменной) и, при необходимости, инициализирует ее. В этом случае мы предоставляем инициализирующее значение 1. Если инициализатор не указан, C++ инициализирует значением 0.
Обратите внимание, что это определение статического члена не подлежит контролю доступа: вы можете определить и инициализировать переменную, даже если она объявлена в классе как закрытая (или защищенная).
Если класс определен в файле .h, определение статического члена обычно помещается в связанный с ним файл исходного кода для класса (например, Something.cpp). Если класс определен в файле .cpp, определение статического члена обычно помещается непосредственно под классом. Не помещайте определение статического члена в заголовочный файл (подобно случаю с глобальной переменной, если этот заголовочный файл будет включен более одного раза, вы получите несколько определений, что приведет к ошибке компиляции.
Встраиваемая инициализация статических переменных-членов
Для вышесказанного есть несколько сокращений. Во-первых, когда статический член является константным и принадлежит целочисленному типу (включает в себя char
и bool
) или перечислению, этот статический член может быть инициализирован внутри определения класса:
class Whatever
{
public:
// переменная static const int может быть
// объявлена и инициализирована напрямую
static const int s_value{ 4 };
};
В приведенном выше примере, поскольку статическая переменная-член является const int
, строки явного определения не требуется.
Во-вторых, статические члены constexpr
могут быть инициализированы внутри определения класса:
#include <array>
class Whatever
{
public:
static constexpr double s_value{ 2.2 }; // ok
// это работает даже для классов, поддерживающих инициализацию constexpr
static constexpr std::array<int, 3> s_array{ 1, 2, 3 };
};
Пример статических переменных-членов
Зачем использовать статические переменные внутри классов? Отличный пример – присвоение уникального идентификатора каждому экземпляру класса. Например:
class Something
{
private:
static int s_idGenerator;
int m_id;
public:
// получаем следующее значение из генератора id
Something() { m_id = s_idGenerator++; }
int getID() const { return m_id; }
};
// Обратите внимание, что мы определяем и инициализируем s_idGenerator,
// хотя выше он объявлен как закрытый.
// Это нормально, поскольку определение не подлежит контролю доступа.
int Something::s_idGenerator { 1 }; // запускаем наш генератор id со значения 1
int main()
{
Something first;
Something second;
Something third;
std::cout << first.getID() << '\n';
std::cout << second.getID() << '\n';
std::cout << third.getID() << '\n';
return 0;
}
Эта программа печатает:
1
2
3
Поскольку s_idGenerator
используется всеми объектами Something
, при создании нового объекта Something
конструктор берет текущее значение из s_idGenerator
и затем увеличивает его для следующего объекта. Это гарантирует, что каждый экземпляр объекта Something
получает уникальный идентификатор (увеличивающийся по мере создания объектов). Это может помочь при отладке нескольких элементов в массиве, так как позволяет различать несколько объектов одного типа класса!
Статические переменные-члены также могут быть полезны, когда классу необходимо использовать внутреннюю таблицу поиска (например, массив, используемый для хранения набора предварительно рассчитанных значений). Если сделать таблицу поиска статической, то будет существовать только одна копия для всех объектов, вместо того, чтобы делать копию для каждого созданного объекта. Это может сэкономить значительный объем памяти.