对于 C++ 类中的静态成员变量 - 初始化在类外完成。我想知道为什么?对此有任何逻辑推理/限制吗?或者它是纯粹的遗留实现 - 标准不想更正?
我认为在类中进行初始化更“直观”且不那么混乱。它还给出了变量的静态和全局性的感觉。例如,如果您看到静态 const 成员。
对于 C++ 类中的静态成员变量 - 初始化在类外完成。我想知道为什么?对此有任何逻辑推理/限制吗?或者它是纯粹的遗留实现 - 标准不想更正?
我认为在类中进行初始化更“直观”且不那么混乱。它还给出了变量的静态和全局性的感觉。例如,如果您看到静态 const 成员。
从根本上说,这是因为静态成员必须在一个翻译单元中定义,以免违反One-Definition Rule。如果语言允许类似:
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 静态类成员,因为此类条目可以用于整型常量表达式 (ICE)。ICE 的主要思想是它们在编译时被评估,因此不依赖于所涉及对象的定义。这就是为什么整数或枚举类型可能出现此异常的原因。但对于其他类型,它只会与 C++ 的基本声明/定义原则相矛盾。
这是因为代码的编译方式。如果您要在通常位于标头中的类中对其进行初始化,则每次包含标头时,您都会获得静态变量的实例。这绝对不是本意。在类之外对其进行初始化使您可以在 cpp 文件中对其进行初始化。
C++ 标准的第 9.4.2 节,静态数据成员指出:
如果
static
数据成员是const
整数或const
枚举类型,它在类定义中的声明可以指定一个const-initializer,它应该是一个整数常量表达式。
因此,静态数据成员的值可能包含在“类中”(我认为您的意思是在类的声明中)。但是,静态数据成员的类型必须是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";
编辑查看以下代码的 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
。
至少对于成员函数,您确实可以选择在标题中实现它们。对于变量,您必须在 .cpp 文件中进行初始化。我不太同意这种限制,我也不认为有充分的理由。