Введение
Статья 513 разобрала static для локальных переменных и переменных уровня файла — в этой статье разберём третье, отдельное значение того же ключевого слова применительно к членам класса: статические поля (общие для всех экземпляров класса) и статические методы (не привязанные к конкретному экземпляру).
Концепция
Статическое поле класса (static int count;) существует в единственном экземпляре, общем для ВСЕХ объектов этого класса (а не отдельно для каждого объекта, как обычные, «нестатические» поля) — изменение статического поля через один объект класса видно при последующем доступе через любой другой объект того же класса, поскольку физически это одна и та же переменная. Статический метод класса (static void someMethod();) не имеет доступа к нестатическим (обычным) полям конкретного объекта и не получает скрытый параметр this (указатель на текущий объект, неявно передаваемый всем обычным методам), что означает, что статический метод можно вызвать без какого-либо реально существующего объекта класса вовсе, обращаясь к нему через имя класса (ClassName::staticMethod()), а не через конкретный экземпляр.
Пример кода: Статические члены класса
#include <iostream>
class Counter {
public:
Counter() { ++s_instanceCount; } // КАЖДЫЙ конструктор увеличивает ОБЩИЙ, единый счётчик
~Counter() { --s_instanceCount; }
static int getInstanceCount() { return s_instanceCount; } // статический метод — доступен БЕЗ объекта
private:
static int s_instanceCount; // ОБЪЯВЛЕНИЕ статического поля внутри класса
};
int Counter::s_instanceCount = 0; // ОПРЕДЕЛЕНИЕ статического поля — ОБЯЗАТЕЛЬНО ВНЕ класса, в .cpp файле!
int main()
{
std::cout << Counter::getInstanceCount() << std::endl; // 0 — вызов через ИМЯ КЛАССА, без объекта
Counter a, b, c;
std::cout << Counter::getInstanceCount() << std::endl; // 3 — общий счётчик увидел ВСЕ три созданных объекта
{ Counter d; std::cout << Counter::getInstanceCount() << std::endl; } // 4
std::cout << Counter::getInstanceCount() << std::endl; // 3 — d уничтожен при выходе из блока
return 0;
}
// static constexpr — статическая константа, инициализируемая прямо в объявлении класса (современный C++)
class MathConstants {
public:
static constexpr double PI = 3.14159265358979; // допустимо инициализировать ПРЯМО здесь для constexpr
};
int main()
{
std::cout << MathConstants::PI << std::endl; // доступ через имя класса, без создания объекта
return 0;
}
Пояснения к коду
Counter::s_instanceCount, увеличиваемый в каждом конструкторе и уменьшаемый в деструкторе, показывает практичное применение статического поля — единственная, общая переменная отслеживает суммарное количество живых объектов класса вне зависимости от того, через какой конкретный объект (или вовсе без объекта, через Counter::getInstanceCount()) к ней обращаются, что иллюстрируется явным изменением значения при создании и уничтожении объектов a, b, c, d в разных областях видимости. Важная синтаксическая деталь — int Counter::s_instanceCount = 0; вне самого класса показывает обязательное отдельное ОПРЕДЕЛЕНИЕ статического поля (объявление внутри класса само по себе не выделяет память для него), что является классической ловушкой синтаксиса, разбираемой в подводных камнях. static constexpr double PI показывает современное, более удобное исключение из этого правила — для static constexpr полей (констант, известных на этапе компиляции) допустима (и обычно достаточна) инициализация прямо в объявлении внутри класса, без необходимости в отдельном определении вне класса, что устраняет соответствующую ловушку именно для этого, всё более распространённого в современном коде случая.
Подводные камни
- Забытое отдельное определение статического поля вне класса (для обычных, не-
constexprстатических полей) — объявлениеstatic int s_instanceCount;внутри класса лишь сообщает компилятору о существовании этого поля, но не выделяет для него реальную память, и без соответствующего определения (int Counter::s_instanceCount = 0;) в одном из.cpp-файлов проекта возникнет ошибка линковки о неразрешённой внешней ссылке при попытке реально использовать это статическое поле — это частая, классическая ошибка при первом знакомстве со статическими членами класса. - Попытка обратиться к нестатическому (обычному) полю объекта изнутри статического метода — поскольку статический метод не получает скрытый параметр
this(указатель на конкретный объект), он физически не имеет доступа ни к какому конкретному экземпляру класса, и попытка использовать обычное, нестатическое поле внутри статического метода является ошибкой компиляции («an object reference is required»), что является прямым следствием самой природы статических методов как не привязанных к конкретному объекту. - Использование статических полей для хранения состояния, которое логически должно быть отдельным для каждого экземпляра класса, по ошибке или недостаточному пониманию различия между статическими и обычными полями — это приводит к неожиданному, нежелательному совместному использованию одного значения между всеми объектами класса там, где разработчик ожидал отдельное, независимое значение для каждого конкретного объекта, что является противоположной по характеру, но не менее серьёзной ошибкой по сравнению с противоположным случаем (использование обычного поля там, где реально нужно общее, статическое значение).
- Чрезмерное использование статических методов и полей как замены полноценного, объектно-ориентированного дизайна (фактическое превращение класса в простой «контейнер» для набора связанных, но статических функций и данных, аналогично модулю процедурного программирования) — хотя статические члены легитимны и полезны для действительно общих, не зависящих от конкретного экземпляра данных и операций (как фабричные методы, общие счётчики, константы), злоупотребление этим подходом для логики, которая концептуально должна оперировать конкретным состоянием отдельных объектов, противоречит самой идее объектно-ориентированного проектирования и часто говорит о том, что класс на самом деле не нужен в его текущей форме, и было бы понятнее организовать тот же код как набор обычных свободных функций.