Статические члены класса в C++: пошаговая инструкция
За полчаса вы объявите статические поля и методы, правильно их определите вне класса и избежите ошибки линковщика undefined reference. Примеры для C++17 и новее.

К концу этой инструкции у вас будет класс со статическим счётчиком объектов и статическим методом, который читает этот счётчик без создания экземпляра. Заодно вы увидите две ошибки, на которых спотыкаются почти все: undefined reference при попытке использовать статическое поле и невозможность обратиться к обычному полю из статического метода. Всё компилируется и запускается, на любом свежем компиляторе. Времени — около 30 минут.
Что нужно иметь до начала
- Компилятор с поддержкой C++17 или новее: GCC 9+, Clang 10+ или MSVC 2019+. Проверьте версию командой
g++ --versionилиclang++ --version. - Любой текстовый редактор и терминал.
- Базовое понимание, что такое класс, поле и метод — про синтаксис
classи создание объекта здесь не рассказываем.
Почему именно C++17: с этой версии появились inline-переменные, которые убирают самую частую боль со статическими полями. Ниже покажу оба способа — классический и через inline, — и объясню, когда какой брать.
Шаг 1. Объявите статическое поле внутри класса
Статический член принадлежит классу целиком, а не отдельному объекту. Один экземпляр на всю программу, сколько бы объектов вы ни создали. Начнём со счётчика созданных объектов.
// file: counter.cpp (C++17)
#include <iostream>
class Widget {
public:
// объявление статического поля — только объявление, не определение
static int instanceCount;
Widget() { ++instanceCount; }
~Widget() { --instanceCount; }
};
Ключевое слово static внутри класса — это объявление. Оно говорит компилятору: «такое поле существует», но память под него ещё не выделена. Если остановиться здесь и попытаться собрать программу, использующую Widget::instanceCount, линковщик выдаст ошибку. Это нормально — исправим на следующем шаге.
Шаг 2. Определите поле вне класса
Классическому статическому полю нужно ровно одно определение в каком-то .cpp-файле. Именно оно выделяет память и задаёт начальное значение.
// продолжение counter.cpp
// определение вне класса: тип, полное имя через ::, начальное значение
int Widget::instanceCount = 0;
int main() {
std::cout << Widget::instanceCount << "\n"; // 0
Widget a;
Widget b;
std::cout << Widget::instanceCount << "\n"; // 2
{
Widget c;
std::cout << Widget::instanceCount << "\n"; // 3
} // здесь c уничтожается
std::cout << Widget::instanceCount << "\n"; // 2
return 0;
}
Обратите внимание: в определении static уже не пишется. Ключевое слово нужно только в объявлении внутри класса. Соберите и запустите:
g++ -std=c++17 counter.cpp -o counter
./counter
Как понять, что получилось: в терминале должны появиться четыре строки — 0, 2, 3, 2. Счётчик растёт при создании объекта и падает при уничтожении, а обращаемся мы к нему через имя класса, а не через объект.
Шаг 3. Добавьте статический метод
Статический метод тоже принадлежит классу и вызывается без объекта. У него нет указателя this, поэтому он видит только статические члены. Добавим геттер.
class Widget {
public:
static int instanceCount;
Widget() { ++instanceCount; }
~Widget() { --instanceCount; }
// статический метод: обращается только к статическим членам
static int howMany() {
return instanceCount;
}
};
int Widget::instanceCount = 0;
int main() {
Widget a, b;
// вызов без объекта, через имя класса
std::cout << Widget::howMany() << "\n"; // 2
}
Проверка: вывод — 2. Метод howMany() можно вызвать и через объект (a.howMany()), но так писать не стоит — это путает читателя, будто метод зависит от конкретного экземпляра.
Статический метод не имеет this. Если внутри него обратиться к обычному (нестатическому) полю, компилятор откажется собирать код. Это не баг, а сама суть: метод не знает, о каком объекте речь.
Шаг 4. Способ для C++17 — inline-переменная
Отдельное определение в .cpp раздражает: поле объявлено в заголовке, а значение живёт в другом файле. С C++17 можно определить статическое поле прямо в объявлении, добавив inline.
// header-only, C++17
class Widget {
public:
// inline-переменная: и объявление, и определение сразу
inline static int instanceCount = 0;
Widget() { ++instanceCount; }
};
Теперь строка int Widget::instanceCount = 0; в .cpp не нужна. Это особенно ценно для header-only библиотек: заголовок можно включить в несколько единиц трансляции, и линковщик не пожалуется на дублирование. Именно inline обеспечивает, что определение по всей программе одно.
Отдельный частый случай — константа. Целочисленную static constexpr можно инициализировать прямо в классе и без inline:
class Config {
public:
static constexpr int maxRetries = 3; // ок с C++11 для целочисленных
};
С C++17 constexpr-члены и так неявно inline, поэтому отдельное определение вне класса им не требуется вообще.
Когда какой способ брать
| Способ | Когда | Минимальная версия |
|---|---|---|
Определение вне класса (int X::field = 0;) | Классический код, есть отдельный .cpp, нужна совместимость со старыми компиляторами | C++98 |
inline static в объявлении | Header-only, хочется всё держать в одном месте | C++17 |
static constexpr в объявлении | Константа времени компиляции, известна сразу | C++11 (целые), C++17 без внешнего определения |
Если не получилось
Три ошибки, которые вы почти наверняка увидите.
undefined reference to `Widget::instanceCount'(GCC/Clang) илиunresolved external symbol(MSVC). Вы объявили поле в классе, но забыли определить его вне класса. Добавьтеint Widget::instanceCount = 0;в один.cpp-файл — или используйтеinline staticна C++17.invalid use of member 'Widget::name' in static member function. Вы обратились к обычному полю из статического метода. У статического метода нет объекта, поэтому доступ к нестатическим членам невозможен. Либо сделайте поле статическим, либо передайте объект в метод аргументом.multiple definition of `Widget::instanceCount'. Вы поместили определениеint Widget::instanceCount = 0;в заголовок, который включается в несколько.cpp. Так делать нельзя: определение должно быть одно. Перенесите его в единственный.cppили используйтеinline static.
Если поле не хочет собираться под старым стандартом, проверьте флаг компиляции: -std=c++17 обязателен для inline static. Без него компилятор ругнётся на синтаксис.
Что делать дальше
Вы умеете объявлять, определять и использовать статические поля и методы. Дальше стоит посмотреть на порядок инициализации статических объектов из разных единиц трансляции — это отдельная тема (так называемая static initialization order fiasco), и там свои грабли, которые к базовому синтаксису отношения не имеют. Для констант, известных на этапе компиляции, привыкайте к static constexpr — он даёт больше гарантий, чем обычное статическое поле. Правила и точные формулировки по inline-переменным сверяйте на cppreference.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: cppreference — static members, cppreference — inline specifier
Частые вопросы
А если у меня компилятор со старым стандартом, без C++17?
Почему я получаю undefined reference, хотя поле объявлено?
Можно ли вызвать статический метод через объект?
Почему статический метод не видит обычные поля?
В чём разница между static constexpr и inline static?
Куда класть определение статического поля, если класс в заголовке?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.