ChatGPT解决这个技术问题 Extra ChatGPT

为什么我不能在类中初始化非常量静态成员或静态数组?

为什么我不能在类中初始化非常量 static 成员或 static 数组?

class A
{
    static const int a = 3;
    static int b = 3;
    static const int c[2] = { 1, 2 };
    static int d[2] = { 1, 2 };
};

int main()
{
    A a;

    return 0;
}

编译器发出以下错误:

g++ main.cpp
main.cpp:4:17: error: ISO C++ forbids in-class initialization of non-const static member ‘b’
main.cpp:5:26: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:5:33: error: invalid in-class initialization of static data member of non-integral type ‘const int [2]’
main.cpp:6:20: error: a brace-enclosed initializer is not allowed here before ‘{’ token
main.cpp:6:27: error: invalid in-class initialization of static data member of non-integral type ‘int [2]’

我有两个问题:

为什么我不能在类中初始化静态数据成员?为什么我不能在类中初始化静态数组,甚至是 const 数组?

我认为主要原因是做对很棘手。原则上,你可能会做你所说的,但会有一些奇怪的副作用。就像您的数组示例被允许一样,那么您可能能够获取 A::c[0] 的值,但不能将 A::c 传递给函数,因为这需要地址和编译时间常量没有地址。 C++11 通过使用 constexpr 实现了其中的一些功能。
很好的问题和完美的答案。对我有帮助的链接:msdn.microsoft.com/en-us/library/0e5kx78b.aspx

J
Joseph Mansfield

为什么我不能在类中初始化静态数据成员?

C++ 标准只允许在类中初始化静态常量整数或枚举类型。这就是允许初始化 a 而其他不允许初始化的原因。

参考:C++03 9.4.2 静态数据成员§4

如果静态数据成员是 const 整数或 const 枚举类型,它在类定义中的声明可以指定一个常量初始化器,它应该是一个整数常量表达式 (5.19)。在这种情况下,成员可以出现在整型常量表达式中。如果在程序中使用该成员,则该成员仍应在名称空间范围内定义,并且名称空间范围定义不应包含初始值设定项。

什么是整数类型?

C++03 3.9.1 基本类型§7

bool、char、wchar_t 类型以及有符号和无符号整数类型统称为整数类型。43) 整数类型的同义词是整数类型。

脚注:

43)因此,枚举(7.2)不是整数;但是,枚举可以提升为 int、unsigned int、long 或 unsigned long,如 4.5 中所述。

解决方法:

您可以使用枚举技巧在类定义中初始化一个数组。

class A 
{
    static const int a = 3;
    enum { arrsize = 2 };

    static const int c[arrsize] = { 1, 2 };

};

为什么标准不允许这样做?

Bjarne 恰当地解释了这一点 here

一个类通常在头文件中声明,并且头文件通常包含在许多翻译单元中。但是,为了避免复杂的链接器规则,C++ 要求每个对象都有唯一的定义。如果 C++ 允许在类内定义需要作为对象存储在内存中的实体,则该规则将被打破。

为什么只允许静态常量整数类型和枚举类内初始化?

答案隐藏在 Bjarne 的引文中仔细阅读,“C++ 要求每个对象都有一个唯一的定义。如果 C++ 允许在类内定义需要作为对象存储在内存中的实体,则该规则将被打破。”

请注意,只有 static const 整数可以被视为编译时常量。编译器知道整数值不会随时更改,因此它可以应用自己的魔法并应用优化,编译器只是内联此类成员,即它们不再存储在内存中,因为不需要存储在内存中,它为这些变量提供了 Bjarne 提到的规则的例外。

这里值得注意的是,即使 static const 整数值可以进行类内初始化,也不允许获取此类变量的地址。当(且仅当)静态成员具有类外定义时,可以获取静态成员的地址。这进一步验证了上述推理。

枚举是允许的,因为枚举类型的值可以在需要整数的地方使用。参见上面的引用

这在 C++11 中有何变化?

C++11在一定程度上放宽了限制。

C++11 9.4.2 静态数据成员 §3

如果静态数据成员是 const 字面量类型,则它在类定义中的声明可以指定一个大括号或等号初始化器,其中作为赋值表达式的每个初始化器子句都是一个常量表达式。可以在类定义中使用 constexpr 说明符声明文字类型的静态数据成员;如果是这样,它的声明应指定一个大括号或等式初始化器,其中作为赋值表达式的每个初始化器子句都是一个常量表达式。 [ 注意:在这两种情况下,成员都可能出现在常量表达式中。 —尾注]如果在程序中使用该成员,则该成员仍应在名称空间范围内定义,并且名称空间范围定义不应包含初始化程序。

此外,C++11 将允许(第 12.6.2.8 节)一个非静态数据成员在它被声明的地方(在其类中)进行初始化。这将意味着更简单的用户语义。

请注意,这些功能尚未在最新的 gcc 4.7 中实现,因此您可能仍会遇到编译错误。


在 c++11 中情况有所不同。答案可以使用更新。
这似乎不是真的:“请注意,只有静态 const 整数可以被视为编译时常量。编译器知道整数值不会随时更改,因此它可以应用自己的魔法并应用优化,编译器只是内联此类成员,即它们不再存储在内存中,"您确定它们必然不存储在内存中吗?如果我为成员提供定义怎么办? &member 会返回什么?
@Als:是的。这就是我的问题。那么为什么 C++ 只允许对整数类型进行类内初始化,您的答案没有正确回答。想想为什么它不允许对 static const char* 成员进行初始化?
@Nawaz:因为 C++03 只允许静态和 const 整数和 const 枚举类型的常量初始化器,而没有其他类型,所以 C++11 将其扩展为 const 文字类型,放宽了类内初始化的规范。限制在 C++03 中可能是一个疏忽,需要进行更改,因此在 C++11 中得到纠正,如果更改有任何传统的战术原因,我不知道他们。如果您知道任何可以随意分享他们。
您提到的 is not working 与 g++ 的“解决方法”标准。
n
not-a-user

这似乎是过去简单链接器的遗留物。您可以在静态方法中使用静态变量作为解决方法:

// header.hxx
#include <vector>

class Class {
public:
    static std::vector<int> & replacement_for_initialized_static_non_const_variable() {
        static std::vector<int> Static {42, 0, 1900, 1998};
        return Static;
    }
};

int compilation_unit_a();

// compilation_unit_a.cxx
#include "header.hxx"

int compilation_unit_a() {  
    return Class::replacement_for_initialized_static_non_const_variable()[1]++;
}

// main.cxx
#include "header.hxx"

#include <iostream>

int main() {
    std::cout
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << compilation_unit_a()
    << Class::replacement_for_initialized_static_non_const_variable()[1]++
    << std::endl;
}

建造:

g++ -std=gnu++0x -save-temps=obj -c compilation_unit_a.cxx 
g++ -std=gnu++0x -o main main.cxx compilation_unit_a.o

跑:

./main

这有效的事实(始终如一,即使类定义包含在不同的编译单元中),表明今天的链接器(gcc 4.9.2)实际上足够聪明。

有趣:在 arm 上打印 0123,在 x86 上打印 3210


L
Lewis Kelsey

这是因为所有翻译单元只能使用一个 A::a 定义。

如果您在包含在所有翻译单元中的标头中的类中执行 static int a = 3;,那么您将获得多个定义。因此,静态的非行外定义会强制导致编译器错误。

使用 static inlinestatic const 可以解决此问题。 static inline 只有在翻译单元中使用符号时才将其具体化,并确保链接器仅在由于它位于 comdat 组中而在多个翻译单元中定义时才选择并保留一份副本。文件范围内的 const 使编译器永远不会发出符号,因为它总是在代码中立即替换,除非使用 extern,这在类中是不允许的。

需要注意的一点是,static inline int b; 被视为定义,而 static const int bstatic const A b; 仍被视为声明,如果您未在类中定义它,则必须对其进行离线定义。有趣的是 static constexpr A b; 被视为一个定义,而 static constexpr int b; 是一个错误并且必须有一个初始化程序(这是因为它们现在成为定义,并且像文件范围内的任何 const/constexpr 定义一样,它们需要一个 int 没有的初始化程序'没有,但类类型确实是因为它在定义时具有隐式 = A() - clang 允许这样做,但 gcc 要求您显式初始化,否则这是一个错误。这不是 inline 的问题)。 static const A b = A(); 是不允许的,必须是 constexprinline 才能允许具有类类型的静态对象的初始化程序,即使类类型的静态成员多于声明。所以是的,在某些情况下 A a; 与显式初始化 A a = A(); 不同(前者可以是声明,但如果该类型只允许声明,则后者是错误的。后者只能用于定义。constexpr 使它成为一个定义)。如果您使用 constexpr 并指定默认构造函数,则构造函数将需要为 constexpr

#include<iostream>

struct A
{
    int b =2;
    mutable int c = 3; //if this member is included in the class then const A will have a full .data symbol emitted for it on -O0 and so will B because it contains A.
    static const int a = 3;
};

struct B {
    A b;
    static constexpr A c; //needs to be constexpr or inline and doesn't emit a symbol for A a mutable member on any optimisation level
};

const A a;
const B b;

int main()
{
    std::cout << a.b << b.b.b;
    return 0;
}

静态成员是完全的文件范围声明 extern int A::a;(只能在类中进行,并且行外定义必须引用类中的静态成员,并且必须是定义并且不能包含 extern),而非静态成员是类的完整类型定义的一部分,并且与不带 extern 的文件范围声明具有相同的规则。它们是隐含的定义。所以 int i[]; int i[5]; 是重定义,而 static int i[]; int A::i[5]; 不是,但与 2 extern 不同,如果您在类中执行 static int i[]; static int i[5];,编译器仍会检测到重复的成员。


这是一个非常好的答案。我在哪里可以找到标准中的相关规则,如果它们包含在内?我知道 static inline 是一个定义在 [class.static.general]/4 中,但是像 static constexpr 这样的东西也是一个定义呢?
此外,“文件范围内的 const 使编译器永远不会发出符号 [...]”是 const 还是 static
@RexYuan 我完全忘记了这个答案——我已经写了一个关于这个问题的答案,这对我来说似乎是错误的。我没有正确阅读,但一目了然(请参阅 this 了解我的最新答案)。我会改变它。
u
user541686

我认为这是为了防止您混淆声明和定义。 (想想如果在多个地方包含文件可能会出现的问题。)


佚名

静态变量特定于一个类。构造函数特别为实例初始化属性。