Статические переменные-члены. Цель применения. Синтаксис. Особенности компоновки
Обычные переменные-члены размещаются в памяти объектов класса, и каждый созданный объект содержит собственный набор переменных-членов, объявленных в теле класса. Область видимости таких переменных-членов ограничивается телом класса, включая выносные реализации функций-членов. Время жизни обычной переменной-члена определяется временем жизни содержащего их объекта.
На практике встречается необходимость в переменных-членах иного рода — относящихся к классу в целом, а не к конкретному объекту. Такие переменные-члены называются СТАТИЧЕСКИМИ. Соответственно, обычные переменные-члены, относящиеся к объектам, иногда называют НЕСТАТИЧЕСКИМИ. Статические и нестатические переменные-члены объединяет общая область видимости, одинаковое влияние спецификаторов доступа (public, private,..).
Однако время жизни статических переменных-членов принципиально отличается от нестатических. Местом хранения статических переменных-членов в памяти является сегмент данных, что соответствует глобальным переменным. Отсюда вытекает идентичное глобальным переменным время жизни — они создаются до начала выполнения функции main() и уничтожаются после ее завершения.
Сколько бы ни было создано объектов класса, статическая переменная-член существует в памяти строго в единственном экземпляре. Этот экземпляр существует даже если не было фактически создано ни одного объекта класса. Очевидно, наличие статических переменных-членов в классе ни каким образом не увеличивает размеры объектов.
Единственным синтаксическим различием объявления статических и нестатических переменных-членов класса является наличие ключевого слова static. В приведенном ниже простейшем примере, статическая переменная-член ms_ObjCounter используется для подсчета количества созданных за все время выполнения программы объектов некоторого класса. В свою очередь, обычная нестатическая переменная-член m_Data содержит некоторое полезное данное, относящееся к каждому из объектов:
classTest
{
// Статическая переменная-член (храним независимо от объектов в сегменте данных)
static intms_ObjCounter;
// Обычная (нестатическая) переменная-член (храним в объекте)
intm_Data;
public:
// Конструктор
Test ( int_data )
: m_Data( _data )
{
// Увеличиваем счетчик, хранящийся в статической переменной-члене
ms_ObjCounter++;
}
// Конструктор копий
Test ( constTest & _test )
: m_Data( _test.m_Data )
{
// Также увеличиваем счетчик
ms_ObjCounter++;
}
};
Из внутренних функций-членов класса обращение к статическим переменным-членам непосредственно по имени ничем не отличается от обращения к обычным переменным-членам.
При условии, что статическая переменная-член была объявлена в зоне спецификатора доступа public, возможно обращение из функций, определенных за пределами рассматриваемого класса. Обращение возможно записать двумя способами:
● подобно нестатическим членам, через любой из объектов класса + точка, либо через указатель на объект класса + стрелка:
Test t;
std::cout << t.ms_ObjCounter;
Test * pTest = & t;
std::cout << pTest->ms_objCounter;
● через имя класса (только для статических членов!):
std::cout << Test::ms_objCounter;
Второй способ является предпочтительным, поскольку подчеркивает отношение статической переменной-члена к классу, а не к объекту, к тому же не требует наличия готового объекта или создания временного лишь для обращения к статическому члену. Первый способ с технической точки зрения допустим, однако сбивает с толку при чтении кода, поскольку визуально неразличим от обращения к нестатическим членам.
Объявление статической переменной-члена в классе не является определением, соответственно где-либо в программе обязательно должно существовать ее определение. Из хранения статических переменных-членов в сегменте данных вытекает обязательное требование дополнительного определения таких переменных в глобальной области в одной из единиц трансляции. В любом компилируемом С++ файле следует разместить следующее определение:
intTest::ms_ObjCounter;
Ключевое слово static в определении повторно использовать не нужно.
Как и для глобальных переменных, статическим членам присваивается нулевое значение по умолчанию. Если тип статического члена сам является структурой или классом, вызывается конструктор по умолчанию. Переменные встроенных типов автоматически получат нулевое значение. При необходимости, определение может определять собственное значение, если оно должно отличаться от автоматически присваиваемого:
intTest::ms_ObjCounter = 5;
Если в программе нигде не будет размещено подобное определение для статической переменной-члена, компиляция по-прежнему пройдет успешно, однако возникнет ошибка на этапе компоновки:
error LNK2001: unresolved external symbol "private: static int Test::ms_ObjCounter" (?ms_ObjCounter@Test@@0HA)
Для компилятора не имеет никакого значения в какой единице трансляции будет размещено определение статической переменной-члена. Типичная конвенция разбиения определений классов на заголовочный файл и файл реализации предполагает размещение определения статических членов в файле реализации того же класса. Однако, конвенции, в отличие от правил, соблюдаются программистами добровольно, по сути, являются общепринятой условностью. Соответственно, разбиение программы на файлы объявления, реализации классов и другие файлы в принципе не имеет для компилятора никакого значения. Отсюда следует, что решение в каком именно файле разместить определение статического члена определяется исключительно программистом. И разместить его в файле реализации того же класса является ожидаемым и логичным.
Ни в коем случае не следует размещать определение статических членов в заголовочных файлах. Такие файлы могут включаться сразу в несколько единиц трансляции при помощи директив #include, и размещение в них определений статических членов приведет к другой ошибке этапа компоновки — к множественному определению одной и той же переменной в нескольких единицах трансляции.
Особый случай статических переменных-членов представляют собой константы целочисленных типов (char, short, int, long, а также перечисления). Подобно глобальным константам, такие статические константные члены подпадают под правила внутренней компоновки (internal linkage). Соответственно, их можно инициализировать сразу в заголовочной части класса, поскольку при таком типе компоновки допускается наличие нескольких копий одного и того же определения в различных единицах трансляции. При помещении в заголовочную часть класса, предполагается, что инициализирующее выражение будет возможно вычислить во время компиляции. В приведенном ниже примере константа целочисленного типа получает значение сразу при объявлении, и используется для задания размера массива в объектах:
classQueue
{
static const intQUEUE_LENGTH = 100;
intm_QueueData[ QUEUE_LENGTH ];
};
Стандарты языка С++, предшествовавшие недавно принятому стандарту С++'11, в явном виде запрещали инициализацию таким образом для констант неинтегральных типов - действительных чисел, константных массивов и константных объектов классов. Новая версия стандарта снимает это ограничение, при условии, что все инициализирующие выражения представляется возможным вычислить во время компиляции (к сожалению, это поддерживается пока не всеми компиляторами).