Перейти к содержимому

Где можно выполнять инициализацию статистической переменной класса

  • автор:

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

otus_Posts_25may_VK_1000x700_2-20219-86ca43.jpg

Занимаясь разработкой на языке С++, мы рано или поздно приходим к вопросу об инициализации статических переменных. Более сложный случай, когда проект большой и над ним трудится много людей. Статические переменные, как правило, объявляются в глобальной области имён, то есть используются многими участниками проекта, что часто вызывает споры и раздоры.

Есть мнение о полном избегании объявления глобальных переменных в угоду обеспечения низкой связанности и манипуляции взаимодействия классов. Давайте последовательно посмотрим, что же происходит.

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 в классе

Может кто-нибудь дать техническое объяснение, почему нельзя инициализировать статические переменные внутри класса, а в функциях можно? Да, я знаю про const static и инициализацию static вне класса, но меня интересует техническое объяснение этого процесса.

class A < public: static int a = 10; // нельзя >; 
int main() < static int b = 10; // можно >

Отслеживать
задан 25 янв 2017 в 21:53
476 4 4 серебряных знака 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
69.2k 3 3 золотых знака 62 62 серебряных знака 140 140 бронзовых знаков

Но статические переменные ведь создаются и инициализируются в статической памяти перед запуском функции 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 . Но в последнем случае они не могут иметь инициализаторов. А в определениях классов вы имеете дело с объявлениями статических членов, а не с их определениями как объектов. С константными статическими членами классов проблем нет, так как они инициализируются констатными значениями, которые вычисляются на этапе компиляции.

Урок №124. Статические переменные-члены класса

На этом уроке мы рассмотрим использование статических переменных-членов класса в языке С++.

Оглавление:

  1. Статические переменные-члены класса
  2. Статические члены не связаны с объектами класса
  3. Определение и инициализация статических переменных-членов класса
  4. Инициализация статических переменных-членов внутри тела класса
  5. Использование статических переменных-членов класса

Статические переменные-члены класса

Из урока №51 мы узнали, что статические переменные сохраняют свои значения и не уничтожаются даже после выхода из блока, в котором они объявлены, например:

int generateID ( )
static int s_id = 0 ;
return ++ s_id ;
std :: cout << generateID ( ) << '\n' ; std :: cout << generateID ( ) << '\n' ; std :: cout << generateID ( ) << '\n' ;

Результат выполнения программы:

Обратите внимание, s_id сохраняет свое значение после каждого вызова функции generateID().

Ключевое слово static имеет другое значение, когда речь идет о глобальных переменных — оно предоставляет им внутреннюю связь (что ограничивает их видимость/использование за пределами файла, в котором они определены). Поскольку использование глобальных переменных — это зло, то ключевое слово static в этом контексте используется не очень часто.

В языке C++ ключевое слово static можно использовать в классах: статические переменные-члены и статические методы. Мы поговорим о статических переменных-членах на этом уроке, а о статических методах на следующем.

Прежде чем мы перейдем к ключевому слову static с переменными-членами класса, давайте сначала рассмотрим следующий класс:

class Anything
int m_value = 3 ;
Anything first ;
Anything second ;
first . m_value = 4 ;
std :: cout << first . m_value << '\n' ; std :: cout << second . m_value << '\n' ;

При создании объекта класса, каждый объект получает свою собственную копию всех переменных-членов класса. В этом случае, поскольку мы объявили два объекта класса Anything, у нас будет две копии m_value : first.m_value и second.m_value . Это разные значения, следовательно, результат выполнения программы:

Переменные-члены класса можно сделать статическими, используя ключевое слово static. В отличие от обычных переменных-членов, статические переменные-члены являются общими для всех объектов класса. Рассмотрим следующую программу:

class Anything
static int s_value ;
int Anything :: s_value = 3 ;
Anything first ;
Anything second ;
first . s_value = 4 ;
std :: cout << first . s_value << '\n' ; std :: cout << second . s_value << '\n' ;

Результат выполнения программы:

Поскольку s_value является статической переменной-членом, то она является общей для всех объектов класса Anything. Следовательно, first.s_value — это та же переменная, что и second.s_value . Вышеприведенная программа показывает, что к значению, которое мы установили через первый объект, можно получить доступ и через второй объект.

Статические члены не связаны с объектами класса

Хотя вы можете получить доступ к статическим членам через разные объекты класса (как в примере, приведенном выше), но, оказывается, статические члены существуют, даже если объекты класса не созданы! Подобно глобальным переменным, они создаются при запуске программы и уничтожаются, когда программа завершает свое выполнение.

Следовательно, статические члены принадлежат классу, а не объектам этого класса. Поскольку s_value существует независимо от любых объектов класса, то доступ к нему осуществляется напрямую через имя класса и оператор разрешения области видимости (в данном случае, через Anything::s_value ):

class Anything
static int s_value ; // объявляем статическую переменную-член
int Anything :: s_value = 3 ; // определяем статическую переменную-член
// Примечание: Мы не создаем здесь никаких объектов класса Anything
Anything :: s_value = 4 ;
std :: cout << Anything :: s_value << '\n' ;

В вышеприведенном фрагменте доступ к s_value осуществляется через имя класса, а не через объект этого класса. Обратите внимание, мы даже не создавали объект класса Anything, но мы все равно имеем доступ к Anything::s_value и можем использовать эту переменную-член.

Определение и инициализация статических переменных-членов класса

Когда мы объявляем статическую переменную-член внутри тела класса, то мы сообщаем компилятору о существовании статической переменной-члене, но не о её определении (аналогией является предварительное объявление). Поскольку статические переменные-члены не являются частью отдельных объектов класса (они обрабатываются аналогично глобальным переменным и инициализируются при запуске программы), то вы должны явно определить статический член вне тела класса — в глобальной области видимости.

В программе, приведенной выше, это делается следующей строкой кода:

int Anything :: s_value = 3 ; // определяем статическую переменную-член

Здесь мы определили статическую переменную-член класса и инициализировали её значением 3 . Если же инициализатор не предоставлен, то C++ инициализирует s_value значением 0 .

Обратите внимание, данное определение статического члена не подпадает под действия спецификаторов доступа: вы можете определить и инициализировать s_value , даже если он будет private (или protected).

Если класс определен в заголовочном файле, то определение статического члена обычно помещается в файл с кодом класса (например, в Anything.cpp). Если класс определен в файле .cpp, то определение статического члена обычно пишется непосредственно под классом. Не пишите определение статического члена класса в заголовочном файле (подобно глобальным переменным). Если этот заголовочный файл подключают больше одного раза, то вы получите несколько определений одного члена, что приведет к ошибке компиляции.

Инициализация статических переменных-членов внутри тела класса

Есть несколько обходных путей определения статических членов внутри тела класса. Во-первых, если статический член является константным интегральным типом (к которому относятся и char, и bool) или константным перечислением, то статический член может быть инициализирован внутри тела класса:

Статические члены класса

Класс – это тип, а не объект данных, и в каждом объекте класса имеется своя собственная копия данных – членов этого класса. Однако некоторые типы требуется реализовать так, что все объекты этого типа могут совместно использовать некоторые данные. Такие совместно используемые данные должны быть описаны как часть класса.

Статические данные относятся ко всем объектам класса. Такие данные используются, если

  • требуется контроль общего количества объектов класса;
  • требуется одновременный доступ ко всем объектам или части их;
  • требуется разделение объектами общих ресурсов.

В этом случае в определение класса могут быть введены статические члены.

Статические члены описываются с помощью ключевого слова 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;
>

Статические члены класса

Результат выполнения

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *