Инициализация static в классе
Может кто-нибудь дать техническое объяснение, почему нельзя инициализировать статические переменные внутри класса, а в функциях можно? Да, я знаю про const static и инициализацию static вне класса, но меня интересует техническое объяснение этого процесса.
class A < public: static int a = 10; // нельзя >;
int main() < static int b = 10; // можно >
Отслеживать
задан 25 янв 2017 в 21:53
476 3 3 серебряных знака 11 11 бронзовых знаков
one point of definition
25 янв 2017 в 22:00
Не любите struct ?
26 янв 2017 в 19:48
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Это существенно разные вещи с точки зрения языка. В первом случае вы имеете просто объявление статического члена класса с внешним связыванием (external linkage), а во втором — определение статической переменной вообще без связывания (no linkage).
Традиционно в С++ определение сущностей c (для которых требуются определения) — это задача пользователя. И инициализатор традиционно (за редкими исключениями) указывается именно в определении.
Причина этого заключается в том, что выбор единицы трансляции, в которой будет располагаться определение c внешним связыванием в классическом С++ — это часть пользовательского замысла. Т.е. пользователь выбирает, в какой объектный файл попадет это определение.
Также точное место расположения определения статического члена класса определяет его порядок инициализации (и деструкции) в рамках одной единицы трансляции. Это, с точки зрения компилятора, тоже часть пользовательского замысла.
Поэтому компилятор ждет этого решения от вас, а не пытается принимать его сам.
В С++17 появятся inline-переменные, т.е. фактически возможность при помощи ключевого слова inline сказать компилятору о том, что вас не интересует, где именно будет определен ваш статический член класса. Вот пользуясь эти синтаксисом вы и сможете наконец обойтись без явного указания места определения статического члена и, как следствие, также сможете указывать инициализатор прямо в определении класса
class a < public: static inline int a = 10; >;
Отслеживать
ответ дан 25 янв 2017 в 22:01
AnT stands with Russia AnT stands with Russia
69k 3 3 золотых знака 62 62 серебряных знака 139 139 бронзовых знаков
Но статические переменные ведь создаются и инициализируются в статической памяти перед запуском функции main, мы ведь можем создать и инициализировать глобальные переменные, почему то-же самое нельзя сделать и со static?
25 янв 2017 в 22:10
@nammidd: Определения классов в С++ обычно располагаются в заголовочных файла и включаются в несколько единиц трансляции. В такой ситуации трактовка объявления статического члена класса как определения ведет к множественным определениям этого члена. А это уже проблема. Попробуйте определить обычную глобальную переменную в заголовочном файле и включить его в несколько единиц трансляции — вы сразу получите ошибку множественного определения. Вот именно в избежание этой проблемы ваше объявление трактуется как просто объявление, не как определение. Определение надо делать отдельно.
25 янв 2017 в 22:13
Внутри определения класса статические члены класса лишь объявляются, а не определяются. Поэтому внутри объявления класса статические члены класса могут иметь неполный тип. Например,
#include struct A < static int a[]; >; int A::a[10]; int main()
В этой демонстрационной программе в определении класса A объявляется статический член класса — массив a , который имеет неполный тип, то есть количество элементов массива не задано.
Имейте в виду, что объявление класса может быть включено во множество единиц трансляции, в то время как определение объекта, включая статические члены класса, должно быть только в одной единице трансляции, если только объект не имеет внутреннее связывание.
Кроме того инициализация статических объектов зависит от того, как он расположен относительно других статических объектов. Например, если инициализация некоторого статического объекта зависит от значения другого объекта, то этот статический объект должен быть расположен после того объекта, от которого его инициализация зависит.
В функциях вы имеете дело с определениями объектов, если только они не объявлены со спецификатором extern . Но в последнем случае они не могут иметь инициализаторов. А в определениях классов вы имеете дело с объявлениями статических членов, а не с их определениями как объектов. С константными статическими членами классов проблем нет, так как они инициализируются констатными значениями, которые вычисляются на этапе компиляции.
Статические члены класса
Класс – это тип, а не объект данных, и в каждом объекте класса имеется своя собственная копия данных – членов этого класса. Однако некоторые типы требуется реализовать так, что все объекты этого типа могут совместно использовать некоторые данные. Такие совместно используемые данные должны быть описаны как часть класса.
Статические данные относятся ко всем объектам класса. Такие данные используются, если
- требуется контроль общего количества объектов класса;
- требуется одновременный доступ ко всем объектам или части их;
- требуется разделение объектами общих ресурсов.
В этом случае в определение класса могут быть введены статические члены.
Статические члены описываются с помощью ключевого слова static , которое может использоваться при объявлении член-данных и член-функций.
Такие члены классов называются статическими, и независимо от количества объектов данного класса, существует только одна копия статического элемента.
Обращение к статическому элементу осуществляется с помощью оператора разрешения контекста и имени класса:
ИмяКласса :: ИмяЭлемента
Если x – статическое член-данное класса cl, то к нему можно обращаться как
cl::x
При этом не имеет значения количество объектов класса cl.
Аналогично можно обращаться к статической член-функции:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Статические член-данные (или поля) класса можно рассматривать как глобальную переменную класса. Но в отличие от обычных глобальных переменных на статические члены распространяются правила видимости private и public . Поместив статическую переменную в часть private , можно ограничить ее использование.
Объявление статического члена в объявлении класса не является определением, то есть это объявление статического члена не обеспечивает распределения памяти и инициализацию.
Инициализация статических полей
Статические поля нельзя инициализировать в теле класса, а также в методах. Статические поля должны инициализироваться аналогично глобальным переменным в области видимости файла:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include
using namespace std;
class X
static int n;
static char ClassName[30];
public :
static int getN() < return n; >
static char * getClass() < return ClassName; >
X() < n++; >// конструктор
>;
int X::n = 0;
char X::ClassName[] = «My Class» ;
int main()
X a, b, c; // описываем 3 объекта класса X
cout cin.get();
return 0;
>

Результат выполнения
Комментариев к записи: 2
Инициализация статических переменных в С++. Неоднозначности и возможные решения

Занимаясь разработкой на языке С++, мы рано или поздно приходим к вопросу об инициализации статических переменных. Более сложный случай, когда проект большой и над ним трудится много людей. Статические переменные, как правило, объявляются в глобальной области имён, то есть используются многими участниками проекта, что часто вызывает споры и раздоры.
Есть мнение о полном избегании объявления глобальных переменных в угоду обеспечения низкой связанности и манипуляции взаимодействия классов. Давайте последовательно посмотрим, что же происходит.
1. Статическая инициализация
При создании статической переменной, возникает вопрос, когда эта переменная будет инициализирована, сколько времени она проживёт и когда будет уничтожена? Статическая инициализация позволяет нам создать переменную, которая будет инициализирована до запуска программы со временем жизни в течение всей программы и уничтожении после завершения. Такие константные переменные не зависят от исполнения — они всегда существуют, создаются во время компиляции и располагаются в исполнимом файле (в бинарнике). Как результат: нулевые накладные расходы, ранняя диагностика проблем и безопасность. Вопрос об использовании статической инициализации напрямую зависит от предметной области разрабатываемого проекта — чем ниже уровень, тем соблазн использования выше, а иногда и критичен, тут скорость решает всё.
const std :: size_t tabsize = 64 ; int tab [ tabsize ] ;2. Нулевая инициализация
Она возникает тогда, когда начальное значение не может быть оценено во время компиляции, в этом случае все статические переменные либо инициализируются константами, либо нулём, что является большой проблемой, так как однозначно неизвестен вид инициализации. Такая переменная попадёт в динамическую память, но с константным выражением обычно инициализированной нулём, либо она будет в бинарнике с заданным константным выражением. Как следствие, это ведёт к появлению трудно выявляемых ошибок, так как исчезает возможность контроля корректности инициализированных значений.
Для того чтобы этого избежать и принудительно создать статическую переменную, используется спецификатор constexpr, который вычисляет выражение (или результат работы функции) на этапе компиляции при условии, что оно может быть вычислено. Например:
constexpr auto const getLog(std::size_t n)< std :: size_t k = 0; while(n>>= 1) k++; return k; > constexpr std :: size_t n = 64 ; constexpr std :: size_t sz = getLog(tabsize) ; int tab2 [ sz ] ;Но здесь есть свои подводные камни, о которых необходимо упомянуть. Мы все любим выражения подобно auto VER = std::string( "3.4.1" ) , так вот, выражения подобного вида с constexpr, например:
constexpr auto VER = std::string( "3.4.1" );работать не будут, что и логично, так как класс std::string выделяет некоторый ресурс, который должен быть освобождён при уничтожении, в данном случае памяти. Следовательно, std::string( "3.4.1" ) не может быть константным выражением, вычисляемым во время компиляции. В замен мы вынуждены использовать const и за это платим перемещением из времени компиляции во время выполнения, т. е. переходим из статической в динамическую инициализацию.
const auto VER = std::string( "3.4.1" );3. Static-проблема порядка инициализации
Так как порядок инициализации статических переменных чётко не определён, возникает серьёзная проблема правильной инициализации, если значения находятся в разных модулях. Короче говоря, предположим, что у нас есть два static-объекта x и y, которые существуют в отдельных исходных файлах, скажем, x.cpp и y.cpp. Предположим далее, что инициализация для y объекта (обычно y — конструктор объекта) вызывает некоторый метод x объекта. Вот и все. Мы получили 50%-ную вероятность испортить программу.
Как правило, подобная проблема возникает из-за плохого проектирования проекта. Лучший способ её решить — это рефакторинг кода, чтобы разорвать зависимость инициализации глобальных переменных от единиц компиляции. Необходимо сделать модули автономными и стремиться к постоянной инициализации.
Если рефакторинг кода не подходит, возможно использовать идиому Construct On First Use. Основная идея состоит в том, чтобы спроектировать статические переменные, которые не являются константными выражениями (то есть теми, которые должны быть инициализированы во время выполнения) таким образом, чтобы они создавались при первом обращении к ним. Подобный подход часто называется синглтоном Мейера. Как пример:
// a.cpp int duplicate ( int n ) < return n * 2 ; >auto & A () < static auto a = duplicate ( 7 ); return a; >// b.cpp #include #include "a.h" auto B = A (); int main ()
B всегда будет инициализироваться в значение 14.
В заключении хотелось бы отметить, что в общем смысле вопрос правильной инициализации достаточно сложный. Этой теме посвящено множество публикаций и выступлений, особенно на фоне неоднозначного изменения в стандартах языка С++. Так, C++11 принёс концепцию «универсальной инициализации», которая привнесла ещё более сложные правила, и, в свою очередь, их перекрыли в C++14, C++17 и снова поменяют в C++20.
Пока смерть не разлучит нас или всё о static в C++
Всем привет. На одном из код-ревью я столкнулся с мыслью, что многие, а чего скрывать и я сам, не то чтобы хорошо понимаем когда нужно использовать ключевое слова static. В данной статье я хотел бы поделиться своими знаниями и информацией по поводу ключевого слова static. Статья будет полезна как начинающим программистам, так и людям, работающим с языком С++. Для понимания статьи у вас должны быть знания о процессе сборки проектов и владение языком С/С++ на базовом уровне. Кстати, static используется не только в С++, но и в С. В этой статье я буду говорить о С++, но имейте в виду, что всё то, что не связано с объектами и классами, в основном применимо и к языку С.
Что такое static?
Static - это ключевое слово в C++, используемое для придания элементу особых характеристик. Для статических элементов выделение памяти происходит только один раз и существуют эти элементы до завершения программы. Хранятся все эти элементы не в heap и не на stack, а в специальных сегментах памяти, которые называются .data и .bss (зависит от того инициализированы статические данные или нет). На картинке ниже показан типичный макет программной памяти.
Где используется?
Ниже приведена схема, как и где используется static в программе.
А теперь я постараюсь детально описать все то, что изображено на схеме. Поехали!
Статические переменные внутри функции
Статические переменные при использовании внутри функции инициализируются только один раз, а затем они сохраняют свое значение. Эти статические переменные хранятся в статической области памяти (.data или .bss), а не в стеке, что позволяет хранить и использовать значение переменной на протяжении всей жизни программы. Давайте рассмотрим две почти одинаковые программы и их поведение. Отличие в них только в том, что одна использует статическую переменную, а вторая нет.
#include void counter() < static int count = 0; // строка 4 std::cout int main() < for (int i = 0; i < 10; ++i) < counter(); >return 0; >Вывод программы:
0123456789
#include void counter() < int count = 0; // строка 4 std::cout int main() < for (int i = 0; i < 10; ++i) < counter(); >return 0; >Вывод программы:
Если не использовать static в строке 4, выделение памяти и инициализация переменной count происходит при каждом вызове функции counter(), и уничтожается каждый раз, когда функция завершается. Но если мы сделаем переменную статической, после инициализации (при первом вызове функции counter()) область видимости count будет до конца функции main(), и переменная будет хранить свое значение между вызовами функции counter().
Статические объекты класса
Статический объект класса имеет такие же свойства как и обычная статическая переменная, описанная выше, т.е. хранится в .data или .bss сегменте памяти, создается на старте и уничтожается при завершении программы, и инициализируется только один раз. Инициализация объекта происходит, как и обычно — через конструктор класса. Рассмотрим пример со статическим объектом класса.
#include class Base < // строка 3 public: Base() < // строка 5 std::cout ~Base() < // строка 8 std::cout >; void foo() < static Base obj; // строка 14 >// строка 15 int main() < foo(); // строка 18 std::coutВывод программы:
Constructor
End of main()
DestructorВ строке 3 мы создаем класс Base с конструктором (строка 5) и деструктором (строка 8). При вызове конструктора либо деструктора мы выводим название метода класса в консоль. В строке 14 мы создаем статический объект obj класса Base. Создание этого статического объекта будет происходить только при первом вызове функции foo() в строке 18.
Из-за того, что объект статический, деструктор вызывается не при выходе из функции foo() в строке 15, а только при завершении программы, т.к. статический объект разрушается при завершении программы. Ниже приведен пример той же программы, за исключением того, что наш объект нестатический.
#include class Base < public: Base() < std::cout ~Base() < std::cout >; void foo() < Base obj; >// строка 15 int main()Если мы уберем static при создании переменной в функции foo(), то разрушение объекта будет происходить в строке 15 при каждом вызове функции. В таком случае вывод программы будет вполне ожидаемый для локальной переменной с выделенной памятью на стеке:
Constructor
Destructor
End of main()Статические члены класса
В сравнении с предыдущими вариантами использования, статические члены класса немного сложнее для понимания. Давайте разберемся, почему. Предположим, у нас есть следующая программа:
#include class A < // строка 3 public: A() < std::cout ~A() < std::cout >; class B < // строка 9 public: B() < std::cout ~B() < std::cout private: static A a; // строка 15 (объявление) >; int main() < B b; // строка 19 return 0; >В нашем примере мы создали класс А (строка 3) и класс В (строка 9) со статическими членами класса (строка 15). Мы предполагаем, что при создании объекта b в строке 19 будет создан объект a в строке 15. Так бы и произошло, если бы мы использовали нестатические члены класса. Но вывод программы будет следующим:
Constructor B
Destructor BПричиной такого поведения является то, что статические члены класса не инициализируются с помощью конструктора, поскольку они не зависят от инициализации объекта. Т.е. в строке 15 мы только объявляем объект, а не определяем его, так как определение должно происходить вне класса с помощью оператора разрешения области видимости (::). Давайте определим члены класса B.
#include class A < public: A() < std::cout ~A() < std::cout >; class B < public: B() < std::cout ~B() < std::cout private: static A a; // строка 15 (объявление) >; A B::a; // строка 18 (определение) int main()Теперь, после того как мы определили наш статический член класса в строке 18, мы можем увидеть следующий результат программы:
Constructor A
Constructor B
Destructor B
Destructor AНужно помнить, что член класса будет один для всех экземпляров класса B, т.е. если мы создали три объекта класса B, то конструктор статического члена класса будет вызван только один раз. Вот пример того, о чем я говорю:
#include class A < public: A() < std::cout ~A() < std::cout >; class B < public: B() < std::cout ~B() < std::cout private: static A a; // объявление static int count; // объявление >; A B::a; // определение int B::count = 1; // определение int main()Вывод программы:
Constructor A
Constructor B1
Constructor B2
Constructor B3
Destructor B3
Destructor B2
Destructor B1
Destructor AСтатические функции
Статические функции пришли в С++ из С. По умолчанию все функции в С глобальные и, если вы захотите создать две функции с одинаковым именем в двух разных .c(.cpp) файлах одного проекта, то получите ошибку о том, что данная функция уже определена (fatal error LNK1169: one or more multiply defined symbols found). Ниже приведен листинг трех файлов одной программы.
// extend_math.cpp int sum(int a, int b)// math.cpp int sum(int a, int b)// main.cpp int sum(int, int); // declaration int main()Для того чтобы исправить данную проблему, одну из функций мы объявим статической. Например эту:
// extend_math.cpp static int sum(int a, int b)В этом случае вы говорите компилятору, что доступ к статическим функциям ограничен файлом, в котором они объявлены. И он имеет доступ только к функции sum() из math.cpp файла. Таким образом, используя static для функции, мы можем ограничить область видимости этой функции, и данная функция не будет видна в других файлах, если, конечно, это не заголовочный файл (.h).
Как известно, мы не можем определить функцию в заголовочном файле не сделав ее inline или static, потому что при повторном включении этого заголовочного файла мы получим такую же ошибку, как и при использовании двух функций с одинаковым именем. При определении статической функции в заголовочном файле мы даем возможность каждому файлу (.cpp), который сделает #include нашего заголовочного файла, иметь свое собственное определение этой функции. Это решает проблему, но влечет за собой увеличение размера выполняемого файла, т.к. директива include просто копирует содержимое заголовочного файла в .cpp файл.
Статические функции-члены класса (методы)
Статическую функцию-член вы можете использовать без создания объекта класса. Доступ к статическим функциям осуществляется с использованием имени класса и оператора разрешения области видимости (::). При использовании статической функции-члена есть ограничения, такие как:
- Внутри функции обращаться можно только к статическим членам данных, другим статическим функциям-членам и любым другим функциям извне класса.
- Статические функции-члены имеют область видимости класса, в котором они находятся.
- Вы не имеете доступа к указателю this класса, потому что мы не создаем никакого объекта для вызова этой функции.
#include class A < public: A() < std::cout ~A() < std::cout static void foo() < // строка 8 std::cout >; int main() < A::foo(); // строка 14 return 0; >В классе A в строке 8 у нас есть статическая функция-член foo(). В строке 14, мы вызываем функцию используя имя класса и оператор разрешения области видимости и получаем следующий результат программы:
static foo()
Из вывода видно, что никакого создания объекта нет и конструктор/деструктор не вызывается.
Если бы метод foo() был бы нестатическим, то компилятор выдал бы ошибку на выражение в строке 14, т.к. нужно создать объект для того, чтобы получить доступ к его нестатическим методам.
Заключение
В одной статье в интернете я нашел совет от автора – «Используйте static везде, где только можно». Я хотел бы написать, почему так делать не стоит, а стоит использовать только в случае необходимости.
- Статические переменные медленнее, чем нестатические переменные. Для того, чтобы обратиться к статической переменной, нам нужно сделать несколько дополнительных действий, таких как переход в другой сегмент памяти и проверка инициализации переменной. Чаще всего, быстрее выделить локальную переменную на стеке, чем делать дополнительные действия по использованию статической переменной.
- Если вы используете многопоточность, то здесь вы должны быть крайне осторожными, т.к. возможна ситуация, когда два и более потока захотят писать в одну статическую переменную. Если вы будете использовать нестатические переменные в функциях, то избежите подобного, т.к. для каждого потока будет создана собственная нестатическая переменная.
- Ключевое слово static является неотъемлемой частью порождающего шаблона проектирования Singleton, который гарантирует, что будет создан только один экземпляр этого класса. В реализации этого паттерна используется и статический объект, и статическая функция-член. На практике вы можете использовать Singleton для создания объекта трейсера, логгера или любого другого объекта, который должен быть один на всё ваше приложение.
- Иногда для того, чтобы функция отработала только один раз без хранения предыдущего состояния где-то в объекте, используют статические переменные. Пример вы можете посмотреть в разделе «Статические переменные внутри функции». Но это не очень хороший подход, и может привести к долгим часам поиска ошибки, если вы используете многопоточность.
- На практике, программисты C++ часто используют статические функции-члены как альтернативу обычным функциям, которые не требуют создания объекта для выполнения ее.


