C++静态成员变量及其初始化

Posted

技术标签:

【中文标题】C++静态成员变量及其初始化【英文标题】:C++ static member variable and its initialization 【发布时间】:2011-05-31 15:57:54 【问题描述】:

对于 C++ 类中的静态成员变量 - 初始化在类外完成。我想知道为什么?对此有任何逻辑推理/限制吗?或者它是纯粹的遗留实现 - 标准不想更正?

我认为在类中进行初始化更“直观”且不那么混乱。它还给人以变量的静态和全局性的感觉。例如,如果您看到静态 const 成员。

【问题讨论】:

【参考方案1】:

从根本上说,这是因为静态成员必须在一个翻译单元中定义,以免违反One-Definition Rule。如果语言允许类似:

struct Gizmo

  static string name = "Foo";
;

然后name 将在#includes 这个头文件的每个翻译单元中定义。

C++ 允许您在声明中定义 integral 静态成员,但您仍然必须在单个翻译单元中包含定义,但这只是一种快捷方式,或语法糖。所以,这是允许的:

struct Gizmo

  static const int count = 42;
;

只要 a) 表达式是 const 整数或枚举类型,b) 表达式可以在编译时求值,c) 在不违反单一定义规则的某处仍有定义:

文件:gizmo.cpp

#include "gizmo.h"

const int Gizmo::count;

【讨论】:

单一定义规则是:“任何翻译单元不得包含超过一个任何变量、函数、类类型、枚举类型或模板的定义”。如果您的第一个Gizmo 示例是合法的,我认为它不会违反单一定义规则,因为每个翻译单元 有一个Gizmo::name 的单一定义。 @Daniel Trebbien:这不是 ODR 的全部内容。这只是 3.2/1 - ODR 的第一个粗略毯“层”(以处理最明显的违规行为)。完整的 ODR 对每种实体都有一套更详细的要求。对于外部链接对象(以及外部链接函数),ODR 在 3.2/3 中被进一步限制为一个且唯一的定义为整个程序 @Daniel Trebbien:将 3.2/1 的要求与其他要求分开的原因是违反 3.2/1 需要编译器进行诊断,而违反 3.2/3 则不需要诊断. 由于不准确和疯狂的主张而被否决。【参考方案2】:

在 C++ 中,从一开始,initializer 的存在就是对象 definition 的专有属性,即带有初始化器的声明始终是 definition (几乎总是)。

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

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

【讨论】:

【参考方案3】:

这是因为代码的编译方式。如果您要在通常位于标头中的类中对其进行初始化,则每次包含标头时,您都会获得静态变量的实例。这绝对不是本意。在类之外对其进行初始化,您可以在 cpp 文件中对其进行初始化。

【讨论】:

这是现代编译器/链接器组合可以轻松解决的问题,并不足以成为这种繁琐限制的充分理由。 @martona 是对的。 C++ 链接器能够解析成员函数的多个定义,那么为什么不能解析静态成员变量呢?我想这就是 OP 的要求。 我猜只有现代 C++ 链接器可以解析方法的多个定义(成员函数)。 (即我最后一次尝试对一个方法进行多个定义是几年前,链接失败了。)在此之前,头文件中定义的所有方法都需要是内联或静态的,后者导致链接中的多个副本文件。 @Daniel:“为什么不使用静态成员变量”,因为编译器不知道将定义放入哪个翻译单元。 @Daniel:在成员函数有多个定义的情况下这不是问题,因为这些成员函数有多个定义。 Albiet 仍然每个翻译单元有一个定义,但每个翻译单元使用不同的定义。 statics 的一个要求是所有翻译单元使用一个定义。【参考方案4】:

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) 字节组成),而且还有一个调用 std::string 的“ctor 部分”构造函数采用 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

翻看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&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt;::basic_string(char const*, std::allocator&lt;char&gt; const&amp;) 的错位符号。

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

【讨论】:

为什么不允许以下内容:class my_class public: static const double pi = 3.14; ; @John:我认为应该允许使用与const整数或const枚举类型的静态数据成员的值可以在声明中指定的相同原因。我不知道为什么不是。 这向我表明,“非平凡”初始化可能不是非整数类型不允许使用它的唯一原因。 @John:我想我知道为什么 const doubleconst float “不受支持”。如果支持这些类型,则 C++ 编译器必须能够评估“浮点常量表达式”。例如,static const int i = 44 &lt;&lt; 6 ^ 0x63ab9900; 是允许的,因此编译器必须能够计算常量整数表达式。如果static const float f = 24.382f * -999.283f 也被允许,那么 C++ 编译器将必须具有计算浮点运算的函数。这可能被 C++ 委员会视为不必要的复杂情况。【参考方案5】:

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

至少对于成员函数,您确实可以选择在标题中实现它们。对于变量,您必须在 .cpp 文件中进行初始化。我不太同意这种限制,我也不认为有充分的理由。

【讨论】:

以上是关于C++静态成员变量及其初始化的主要内容,如果未能解决你的问题,请参考以下文章

静态成员变量初始化 C++

c++中静态成员变量和静态成员函数(笔试经历)

C++静态成员变量初始化

C++静态成员变量初始化

C++类中的静态成员函数以及静态成员变量

C++类中的静态成员函数以及静态成员变量