对于C ++类中的静态成员变量 - 初始化在类外部完成。我想知道为什么?对此有任何逻辑推理/约束吗?或者它是纯粹的遗留实现 - 标准不想纠正?
我认为在课堂上进行初始化更加“直观”而且不那么令人困惑。它还给出了变量的静态和全局性。例如,如果您看到静态const成员。
答案 0 :(得分:37)
基本上这是因为静态成员必须只在一个翻译单元中定义,以免违反One-Definition Rule。如果语言允许这样的话:
struct Gizmo
{
static string name = "Foo";
};
然后name
将在#include
此头文件的每个翻译单元中定义。
C ++允许您在声明中定义整数静态成员,但您仍然必须在单个翻译单元中包含一个定义,但这只是一个快捷方式或语法糖。所以,这是允许的:
struct Gizmo
{
static const int count = 42;
};
只要a)表达式为const
整数或枚举类型,b)表达式可以在编译时进行计算,并且c)在某个地方仍然存在一个不违反一个定义的定义规则:
file:gizmo.cpp
#include "gizmo.h"
const int Gizmo::count;
答案 1 :(得分:12)
在C ++中,从一开始就存在初始值设定项是对象 definition 的独占属性,即带初始值设定项的声明始终是定义(几乎总是)。
您必须知道,C ++程序中使用的每个外部对象必须只在一个转换单元中定义一次且仅定义一次。允许静态对象的类内初始化器会立即违反此约定:初始化器将进入头文件(类定义通常驻留在其中),从而生成同一静态对象的多个定义(每个包含头文件的转换单元一个) )。这当然是不可接受的。出于这个原因,静态类成员的声明方法完全是“传统的”:你只在头文件中声明它(即不允许初始化器),然后你定义它在您选择的翻译单元中(可能带有初始化程序)。
此规则的一个例外是针对整数或枚举类型的const静态类成员,因为这些条目可以用于积分常量表达式(ICE)。 ICE的主要思想是它们在编译时进行评估,因此不依赖于所涉及对象的定义。这就是整数或枚举类型可能出现此异常的原因。但对于其他类型,它只会与C ++的基本声明/定义原则相矛盾。
答案 2 :(得分:3)
这是因为编译代码的方式。如果你要在类中初始化它(通常在标题中),每次包含标题时你都会得到一个静态变量的实例。这绝对不是意图。在类外部初始化它使您可以在cpp文件中初始化它。
答案 3 :(得分:2)
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部分”采用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
查看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文件的每个目标文件都会有标签
__ZN8my_class3strE
字节的sizeof(std::string)
以及在__ZNSsC1EPKcRKSaIcE
函数的实现中调用__static_initialization_and_destruction_0(int, int)
的汇编代码。链接器可以轻松地合并所有出现的__ZN8my_class3strE
,但它不可能隔离在对象文件的__ZNSsC1EPKcRKSaIcE
实现中调用__static_initialization_and_destruction_0(int, int)
的代码。
答案 4 :(得分:0)
我认为在class
块之外进行初始化的主要原因是允许使用其他类成员函数的返回值进行初始化。如果您想使用a::var
初始化b::some_static_fn()
,则需要确保包含.cpp
的每个a.h
文件首先包含b.h
。这是一团糟,特别是当你(迟早)遇到循环引用时,你只能通过其他不必要的interface
来解决。同样的问题是在.cpp
文件中实现类成员函数的主要原因,而不是将所有内容放在主类“.h
中。
至少对于成员函数,您可以选择在标题中实现它们。使用变量,您必须在.cpp文件中进行初始化。我不完全同意这个限制,我认为这也不是一个很好的理由。