C ++静态成员variables及其初始化

对于C ++类中的静态成员variables,初始化是在类之外完成的。 我想知道为什么? 任何逻辑推理/约束? 还是纯粹的遗留实现 – 标准不想纠正?

我认为在课堂上进行初始化更“直观”,不易混淆,同时也给出了variables的静态和全局性。 例如,如果你看到了静态const成员。

从根本上说,这是因为静态成员必须在一个翻译单元中定义,以便不违反单一定义规则 。 如果语言允许这样的话:

struct Gizmo { static string name = "Foo"; }; 

那么name将在每个翻译单元中定义#include这个头文件。

C ++确实允许你在声明中定义完整的静态成员,但是你仍然需要在一个单独的翻译单元中包含一个定义,但这只是一个快捷方式或者语法糖。 所以,这是允许的:

 struct Gizmo { static const int count = 42; }; 

只要a)expression式是const整数或枚举types,b)expression式可以在编译时被评估,c)在某个地方仍然有一个定义不违背一个定义规则:

文件:gizmo.cpp

 #include "gizmo.h" const int Gizmo::count; 

从C ++开始的时代, 初始化器的存在是对象定义的唯一属性,即具有初始化器的声明总是定义 (几乎总是)。

正如您必须知道的那样,C ++程序中使用的每个外部对象都只能在一个翻译单元中定义一次,只能定义一次。 允许静态对象的类初始化器会立即违背这个约定:初始化器将进入头文件(类定义通常驻留),从而生成同一个静态对象的多个定义(每个包含头文件的翻译单元)。 这当然是不可接受的。 由于这个原因,静态类成员的声明方法是完全“传统”的:你只在头文件中声明它(即不允许初始化),然后在你select的翻译单元中定义它(可能使用初始化器)。

此规则的一个例外是整型或枚举types的常量静态类成员,因为这些条目可以用于整型常量expression式(ICE)。 ICE的主要思想是在编译时进行评估,因此不依赖于涉及的对象的定义。 这就是为什么对于整型或枚举types这种exception是可能的。 但是对于其他types,它只是与C ++的基本声明/定义原则相矛盾。

这是因为代码编译的方式。 如果要在类中进行初始化,通常在头文件中,每当包含头文件时,都会得到一个静态variables的实例。 这绝对不是意图。 让它在类外初始化,可以在cpp文件中初始化它。

C ++标准的第9.4.2节静态数据成员声明:

如果一个static数据成员是const整型或者const枚举types,那么它在类定义中的声明可以指定一个const-initializer ,它应该是一个整型常量expression式。

因此,静态数据成员的价值可能被包含在“类内”(我假定你是指在类的声明中)。 但是,静态数据成员的types必须是const整型或const枚举types。 其他types的静态数据成员的值不能在类声明中指定的原因是可能需要非平凡的初始化(即构造函数需要运行)。

想象一下,如果以下是合法的:

 // my_class.hpp #include <string> class my_class { public: static std::string str = "static std::string"; //... 

与包含这个头文件的CPP文件相对应的每个目标文件将不仅具有my_class::str (由sizeof(std::string)字节组成)的存储空间的副本,而且还具有调用std::string采用Cstring的std::string构造函数 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"; 

编辑看看下面的代码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 

通过g ++生成的SO4547660.s文件,你可以看到这样一个小的源文件有很多代码。

__ZN8my_class3strEmy_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&)的破坏符号。

回到上面的假设示例并使用这些详细信息,对应于包含my_class.hpp的CPP文件的每个对象文件将具有sizeof(std::string)字节的标签__ZN8my_class3strE以及在其实现中调用__ZNSsC1EPKcRKSaIcE汇编代码__static_initialization_and_destruction_0(int, int)函数。 链接器可以轻松地合并所有出现的__ZN8my_class3strE ,但是它不可能在__static_initialization_and_destruction_0(int, int)的目标文件实现中隔离调用__ZNSsC1EPKcRKSaIcE的代码。

我认为在class块之外进行初始化的主要原因是允许使用其他类成员函数的返回值进行初始化。 如果你想用b::some_static_fn()初始化a::var ,你需要确保包含ah每个.cpp文件都包含bh 。 这将是一个混乱,特别是(迟早)你遇到一个循环的引用,你只能解决一个否则不必要的interface 。 同样的问题是在.cpp文件中使用类成员函数实现的主要原因,而不是将所有内容都放在主类“ .h

至less在成员函数中,你可以select在头文件中实现它们。 对于variables,您必须在.cpp文件中进行初始化。 我并不完全同意这个限制,我也不认为这是有充分理由的。