Статическая переменная-член C++ и ее инициализация
Для статических переменных-членов в классе C++ - инициализация выполняется вне класса. Интересно, почему? Любые логические рассуждения / ограничения для этого? Или это чисто унаследованная реализация, которую стандарт не хочет исправлять?
Я думаю, что инициализация в классе более "интуитивна" и менее confusing.It также дает ощущение как статичности, так и глобальности переменной. Например, если вы видите статический член const.
5 ответов:
В основном это происходит потому, что статические члены должны быть определены точно в одной единице перевода, чтобы не нарушать правило одного определения. Если бы язык позволял что-то вроде:
struct Gizmo { static string name = "Foo"; };
Тогда
name
будет определено в каждой единице перевода, которая#include
является этим заголовочным файлом.C++ позволяет определить интегральные статические члены в объявлении, но вы все равно должны включить определение в одну единицу перевода, но это просто короткий путь, или синтаксический сахар. Итак, это разрешено:
struct Gizmo { static const int count = 42; };
До тех пор, пока a) выражение является
const
интегральным или перечислительным типом, b) выражение может быть вычислено во время компиляции, и c) где-то еще есть определение, которое не нарушает правило одного определения:Файл: gizmo.cpp
#include "gizmo.h" const int Gizmo::count;
В C++ с начала времен наличие инициализатора было исключительным атрибутом определения объекта , т. е. объявление с инициализатором всегда является определением (почти всегда).
Как вы должны знать, каждый внешний объект, используемый в программе C++, должен быть определен один раз и только один раз только в одной единице перевода. Разрешение инициализаторов в классе для статических объектов немедленно противоречило бы этому соглашению: инициализаторы входили бы в заголовочные файлы (где обычно находятся определения классов) и таким образом генерируют несколько определений одного и того же статического объекта (по одному для каждой единицы перевода, включающей файл заголовка). Это, конечно, недопустимо. По этой причине подход к объявлению для статических членов класса остается совершенно "традиционным": вы только объявляете его в заголовочном файле (т. е. инициализатор не разрешен), а затем вы определяете его в выбранной вами единице перевода (возможно, с помощью инициализатора). инициализатор).
Одно исключение из этого правила было сделано для членов статического класса const типов integral или enum, поскольку такие записи могут быть использованы для интегральных постоянных выражений (ICEs). Основная идея ICEs заключается в том, что они оцениваются во время компиляции и, таким образом, не зависят от определений участвующих объектов. Который является, почему это исключение было возможности интегрального или перечислимых типов. Но для других типов это просто противоречило бы основным принципам декларации / определения C++.
Это из - за способа компиляции кода. Если бы вы инициализировали его в классе, который часто находится в заголовке, каждый раз, когда заголовок включен, вы бы получили экземпляр статической переменной. Это определенно не является намерением. Его инициализация вне класса дает вам возможность инициализировать его в файле cpp.
Раздел 9.4.2, статические элементы данных, стандарта C++ гласит:
Таким образом, возможно, что значение статического члена данных будет включено "в класс" (под которым я предполагаю, что вы подразумеваете объявление класса). Однако, тип статического элемента данных должен бытьЕсли элемент данных
static
имеет тип перечисленияconst
integral илиconst
, его объявление в определении класса может указывать const-инициализатор, который должен быть интегральным константным выражением.const
целочисленным илиconst
перечислительным типом. Причина, по которой значения статических элементов данных других типов не могут быть указаны в объявлении класса, заключается в том, что, скорее всего, потребуется нетривиальная инициализация (то есть конструктор должен быть запущен).Представьте себе, если бы следующее было законным:
// my_class.hpp #include <string> class my_class { public: static std::string str = "static std::string"; //...
Каждый объектный файл, соответствующий CPP-файлам, содержащим этот заголовок, будет иметь не только копию пространства хранения для
my_class::str
(состоящий изsizeof(std::string)
байт), но и "ctor-раздел", который вызывает конструкторstd::string
, принимающий C-строку. Каждая копия пространства хранения дляmy_class::str
будет идентифицирована общей меткой, поэтому компоновщик теоретически может объединить все копии пространства хранения в одну. Однако компоновщик не сможет изолировать все копии кода конструктора в разделах ctor объектных файлов. Это было бы похоже на просьбу компоновщика удалить весь код для инициализацииstr
в компиляция следующего содержания:std::map<std::string, std::string> map; std::vector<int> vec; std::string str = "test"; int c = 99; my_class mc; std::string str2 = "test2";
EDIT полезно посмотреть на ассемблерный вывод g++ для следующего кода:
// SO4547660.cpp #include <string> class my_class { public: static std::string str; }; std::string my_class::str = "static std::string";
Ассемблерный код можно получить, выполнив:
g++ -S SO4547660.cpp
Просматривая файл
SO4547660.s
, который генерирует g++, вы можете увидеть, что существует много кода для такого небольшого исходного файла.
__ZN8my_class3strE
является меткой места хранения дляmy_class::str
. Существует также источник сборки функции__static_initialization_and_destruction_0(int, int)
, которая имеет метку__Z41__static_initialization_and_destruction_0ii
. Эта функция является особенной для g++ , но просто знайте, что g++ позаботится о том, чтобы она была вызвана до того, как будет выполнен любой неинициализирующий код. Обратите внимание, что реализация этой функции вызывает__ZNSsC1EPKcRKSaIcE
. Это искореженный символ дляstd::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)
.Возвращаясь к гипотетическому примеру выше и используя эти детали, каждый объектный файл, соответствующий CPP-файлу, который включает
my_class.hpp
, будет иметь метку__ZN8my_class3strE
дляsizeof(std::string)
байт, а также ассемблерный код для вызова__ZNSsC1EPKcRKSaIcE
в пределах его реализация функции__static_initialization_and_destruction_0(int, int)
. Компоновщик может легко объединить все вхождения__ZN8my_class3strE
, но он не может изолировать код, который вызывает__ZNSsC1EPKcRKSaIcE
в реализации объектного файла__static_initialization_and_destruction_0(int, int)
.
Я думаю, что основная причина, по которой инициализация выполняется вне блока
class
, заключается в том, чтобы разрешить инициализацию с возвращаемыми значениями других функций-членов класса. Если вы хотите инициализироватьa::var
с помощьюb::some_static_fn()
, вам нужно убедиться, что каждый файл.cpp
, который включаетa.h
, включаетb.h
в первую очередь. Это будет беспорядок, особенно когда (рано или поздно) вы столкнетесь с круговой ссылкой, которую вы могли бы решить только с помощью ненужного в противном случаеinterface
. Та же проблема является основной причиной наличия класса реализации функций-членов в файле.cpp
вместо того, чтобы помещать все в свой основной класс'.h
.По крайней мере, с функциями-членами у вас есть возможность реализовать их в заголовке. С переменными вы должны сделать инициализацию в a .cpp-файл. Я не совсем согласен с этим ограничением, и я не думаю, что для этого есть веская причина.