我认为,在类内进行初始化更具“直观性”和少些混淆。这也给人以变量既是静态的又是全局的感觉。例如,如果你查看静态常量成员。
基本上这是因为静态成员必须在一个翻译单元中定义,以避免违反C++的One-Definition Rule规则。如果语言允许如下代码:
struct Gizmo
{
static string name = "Foo";
};
然后 name
会在每个包含该头文件的翻译单元中被定义。
C++确实允许您在声明中定义整数静态成员,但仍然需要在一个翻译单元中包含定义,但这只是一种快捷方式或语法糖。所以,这是允许的:
struct Gizmo
{
static const int count = 42;
};
只要a)表达式是“const”整数或枚举类型,b)表达式可以在编译时评估,并且c)仍然有某个定义不违反单一定义规则:
文件:gizmo.cpp
#include "gizmo.h"
const int Gizmo::count;
自C++诞生以来,初始化器的存在一直是对象定义的专属属性,也就是说,带有初始化器的声明始终是一个定义(几乎总是如此)。
正如你所知,在C++程序中使用的每个外部对象都必须在一个翻译单元中定义一次且仅一次。允许为静态对象使用类内初始化器将立即违反此约定:初始化器将进入头文件(通常存放类定义的位置),从而生成同一静态对象的多个定义(对于包含头文件的每个翻译单元都会生成一个)。这当然是不可接受的。因此,对于静态类成员,声明方法保持完全“传统”:只需在头文件中声明它(即不允许初始化器),然后在您选择的翻译单元中定义它(可能带有初始化器)。
唯一的例外是针对整数或枚举类型的const静态类成员,因为这样的条目可以用于整数常量表达式(ICE)。 ICE的主要思想是它们在编译时进行评估,因此不依赖于涉及的对象的定义。这就是为什么对于整数或枚举类型可以做出此异常处理,但对于其他类型而言,这将违反C++的基本声明/定义原则。
这是由于代码编译的方式导致的。如果你在类中初始化它(通常在头文件中),每次包含头文件时都会得到静态变量的一个实例。这绝对不是意图。将其在类外初始化,可以让你有可能在cpp文件中初始化它。
static
的要求是所有翻译单元都使用一个定义。 - John DiblingC++标准的第9.4.2节“静态数据成员”规定:
如果一个
static
数据成员是const
整数或枚举类型,它在类定义中的声明可以指定一个const-initializer,该initializer必须是一个整数常量表达式。
因此,静态数据成员的值可以被包含在“类内部”(我想你的意思是在类的声明内部)。然而,静态数据成员的类型必须是一个const
整数或枚举类型。其他类型的静态数据成员的值不能在类声明中指定,因为可能需要进行非平凡的初始化(即需要运行构造函数)。
请想象以下情况是否合法:
// my_class.hpp
#include <string>
class my_class
{
public:
static std::string str = "static std::string";
//...
my_class::str
的空间副本(由sizeof(std::string)
字节组成),还会有一个“ctor section”,调用接受C字符串的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
SO4547660.s
文件,您会发现对于如此小的源文件,有很多代码。
__ZN8my_class3strE
是存储空间my_class::str
的标签。还有一个名为__Z41__static_initialization_and_destruction_0ii
的汇编源代码__static_initialization_and_destruction_0(int, int)
函数。该函数对于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文件对应的对象文件都将具有标签__ZN8my_class3strE
,大小为sizeof(std::string)
字节,并且包含汇编代码以调用__ZNSsC1EPKcRKSaIcE
在其实现中的__static_initialization_and_destruction_0(int, int)
函数。链接器可以轻松合并__ZN8my_class3strE
的所有出现,但它不可能隔离调用__ZNSsC1EPKcRKSaIcE
的代码在对象文件实现的__static_initialization_and_destruction_0(int, int)
函数中。class my_class { public: static const double pi = 3.14; };
? - John Diblingconst double
和const float
。如果支持这些类型,那么C++编译器必须能够评估“浮点常量表达式”。例如,static const int i = 44 << 6 ^ 0x63ab9900;
是允许的,因此编译器必须能够评估常量整数表达式。如果也允许static const float f = 24.382f * -999.283f
,那么C++编译器就必须有计算浮点运算的函数。这可能被C++委员会视为不必要的复杂性。 - Daniel Trebbien我认为将初始化操作放在class
块之外的主要原因是允许使用其他类成员函数的返回值进行初始化。如果你想用b::some_static_fn()
初始化a::var
,你需要确保每个包含a.h
的.cpp
文件先包含b.h
。这样会很麻烦,特别是当你(早晚)遇到只能通过一个不必要的interface
来解决的循环引用问题时。同样的问题也是将类成员函数实现放在.cpp
文件中而不是把所有代码都放在主类的.h
中的主要原因。
至少对于成员函数,你可以选择在头文件中实现它们。但对于变量,你必须在.cpp
文件中进行初始化。我不太同意这种限制,也不认为有什么好理由这么做。
Gizmo
示例是合法的,我认为它不会违反一个定义规则,因为每个翻译单元将具有Gizmo::name
的单个定义。 - Daniel Trebbien