[英]C++ static member variable and its initialization
对于 C++ 类中的静态成员变量 - 初始化在类之外完成。 我想知道为什么? 对此有何逻辑推理/约束? 还是纯粹的遗留实现——标准不想纠正?
我认为在类中进行初始化更“直观”并且不那么令人困惑。它还提供了变量的静态和全局性的意义。 例如,如果您看到静态常量成员。
从根本上说,这是因为必须在一个翻译单元中定义静态成员,以免违反单一定义规则。 如果语言允许以下内容:
struct Gizmo
{
static string name = "Foo";
};
然后name
将在每个翻译单元中定义#include
s这个头文件。
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
整型或const
枚举类型。 不能在类声明中指定其他类型的静态数据成员的值的原因是可能需要非平凡的初始化(即需要运行构造函数)。
想象一下,如果以下是合法的:
// 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
的“ctor section” 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++生成的SO4547660.s
文件,可以看到这么小的源文件代码量很大。
__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&)
的重整符号。
回到上面的假设示例并使用这些细节,与包含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
中的主要原因。
至少对于成员函数,您可以选择在标头中实现它们。 对于变量,您必须在 .cpp 文件中进行初始化。 我不太同意这种限制,而且我认为也没有充分的理由。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.