【问题标题】:C++ Initialization of static variables (Once again)静态变量的 C++ 初始化(再一次)
【发布时间】:2010-12-07 16:57:51
【问题描述】:

如果我在不同的编译单元中有两个静态变量,那么它们的初始化顺序是没有定义的。这一课很好学。

我的问题是:当第一个变量被初始化时,所有静态变量是否都已分配。换句话说:

static A global_a; // in compilation unit 1
static B global_b; // in compilation unit 2

struct A {
    A() { b_ptr = &global_b; }
    B *b_ptr;

    void f() { b_ptr->do_something(); }
}

int main() {
    global_a.f();
}

b_ptr 是否会指向一个有效的内存,B 是在主函数执行时分配和初始化的?在所有平台上?

更长的故事:

编译单元1是Qt库。 另一个是我的申请。我有几个 QObject 派生类,我需要能够通过类名字符串进行实例化。为此,我想出了一个模板工厂类:

class AbstractFactory {
public:
    virtual QObject *create() = 0;
    static QMap<const QMetaObject *, AbstractFactory *> m_Map;
}
QMap<const QMetaObject *, AbstractFactory *> AbstractFactory::m_Map; //in .cpp

template <class T>
class ConcreteFactory: public AbstractFactory {
public:   
    ConcreteFactory() { AbstractFactory::m_Map[&T::staticMetaObject] = this; }
    QObject *create() { return new T(); }
}

#define FACTORY(TYPE) static ConcreteFactory < TYPE > m_Factory;

然后我在每个 QObject 子类定义中添加这个宏:

class Subclass : public QObject {
   Q_OBJECT;
   FACTORY(Subclass);
}

终于可以通过类型名来实例化一个类了:

QObject *create(const QString &type) {
    foreach (const QMetaObect *meta, AbstractFactory::m_Map.keys() {
        if (meta->className() == type) {
           return AbstractFactory::m_Map[meta]->create();
        }
    }
    return 0;
}

因此,该类从 Qt 库中获取了一个静态的 QMetaObject 实例:Subclass::staticMetaObject - 我认为它是在 Q_OBJECT 宏中自动生成的。然后FACTORY 宏创建一个静态ConcreteFactory&lt; Subclass &gt; 实例。 ConcreteFactory 在其构造函数中尝试引用 Subclass::staticMetaObject。

我对 linux (gcc) 上的这个实现非常满意,直到我用 Visual Studio 2008 编译它。由于某种原因,AbstractFactory::m_Map 在运行时为空,调试器不会在工厂构造函数中中断。

这就是引用其他静态变量的静态变量的气味的来源。

如何优化此代码以避免所有这些陷阱?

【问题讨论】:

  • 好吧,这段代码与你原来的问题完全不同......在你原来的问题中,你存储了一个全局变量的地址,但直到main 才访问它,到那时全球已全面建成。但是现在您尝试从全局 ConcreteFactory 对象的构造函数内部使用它,这比 main 早得多。
  • 在详细的例子中我存储了静态变量staticMetaObject的地址。但它是在运行时通过create 函数从main 访问的——正如我在简化示例中描述的那样。我不关心static QMap&lt;...&gt; m_Map - 正如@Martin York 所建议的那样,我的真实代码实际上被包装到一个静态函数中。所以详细的例子是关于从ConcreteFactory构造函数中引用staticMetaObject

标签: c++ variables static initialization


【解决方案1】:

是的,标准允许这样做。

[basic.life] 部分中有许多段落以开头

在对象的生命周期结束之前 开始但在存储之后 将占用的对象是 分配,或者,在一个生命周期之后 对象已经结束并且在 对象占用的存储空间是 重用或释放,任何指针 指存储位置 该对象将被或曾经被定位 可以使用,但只能以有限的方式使用。

并且有一个脚注表明这特别适用于您的情况

例如在构造非POD类类型的全局对象之前

【讨论】:

  • 很高兴有节号。但是很好找。
  • 我很确定各个版本的标准中的部分名称非常一致,但我对编号并没有那么满意。例如,它是 C++0x FCD 的第 3.8 节。
  • 哎呀。错过了那个明显的名字。 :-) 对我来说还早。
【解决方案2】:

简短回答:它应该可以按照您的编码工作。见 Ben Voigt 答案

长答案:

做这样的事情:
与其让编译器决定何时创建全局变量,不如通过静态方法(使用静态函数变量)创建它们。这意味着它们将在首次使用时确定地创建(并按创建的相反顺序销毁)。

即使一个全局变量在其构建过程中使用此方法使用另一个全局变量,也保证它们将按所需顺序创建,因此可供另一个全局变量使用(注意循环)。

struct A
{
    // Rather than an explicit global use
    // a static method thus creation of the value is on first use
    // and not at all if you don't want it.
    static A& getGlobalA()
    {
        static A instance;  // created on first use (destroyed on application exit)
     // ^^^^^^ Note the use of static here.
        return instance;    // return a reference.
    }
  private:
    A() 
        :b_ref(B::getGlobalB())     // Do the same for B
    {}                              // If B has not been created it will be
                                    // created by this call, thus guaranteeing
                                    // it is available for use by this object
    }
    B&  b_ref;
  public:
    void f() { b_ref.do_something(); }
};

int main() {
    a::getGlobalA().f();
}

虽然是一句警告。

  • 全局变量是不良设计的指示
  • 依赖于其他全局变量的全局变量是另一种代码异味(尤其是在构造/销毁期间)。

【讨论】:

  • 你的意思是编译器实现?
  • OP 询问了它们的分配,而不是初始化。比较 static int istatic int* i
【解决方案3】:

是的。所有都位于.data 部分,即一次分配(而且不是堆)。

换一种说法:如果你能拿到它的地址,那就没关系,因为它肯定不会改变。

【讨论】:

  • 该标准没有 .data 部分的概念。
【解决方案4】:

如果 B 有一个构造函数,就像 A 一样,那么调用它们的顺序是不确定的。因此,除非您很幸运,否则您的代码将无法工作。但是如果 B 不需要任何代码来初始化它,那么你的代码就可以工作。它不是实现定义的。

【讨论】:

  • 这不是被问到的问题。被问到的问题是变量 global_b 的地址是否可供 global_a 的构造函数使用(即使 global_b 尚未被其构造函数初始化)。
  • 是的,你说的很对。对不起。忽略我的回答,去阅读 Ben Voigt 的。
【解决方案5】:

啊,但是静态变量“未初始化”的想法是完全错误的。它们总是被初始化,只是不一定用你的初始化器。特别是,在任何其他初始化之前,所有静态变量都以零值创建。对于类对象,成员为零。因此,上面的 global_a.b_ptr 将始终是一个有效的指针,最初为 NULL,然后是 &global_b。这样做的效果是非指针的使用是未指定的,不是未定义的,特别是这段代码是很好定义的(在 C 中):

// unit 1
int a = b + 1;

// unit 2
int b = a + 1;

main ... printf("%d\n", a + b); // 3 for sure

零初始化保证与此模式一起使用:

int get_x() {
  static int init;
  static int x;
  if(init) return x;
  else { x = some_calc(); init = 1; return x; }
}

确保由于无限递归而不返回,或者正确初始化值。

【讨论】:

  • 我经常遇到一个问题,即成员变量的自动初始化在 debud 和 release 中的工作方式不同。因此,如果我忘记将 0(或 NULL)分配给成员指针,它可能不会在调试中初始化(即包含垃圾值)。静态变量的工作方式是否不同?
猜你喜欢
  • 1970-01-01
  • 2020-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-26
  • 1970-01-01
  • 2010-10-15
  • 2021-10-23
相关资源
最近更新 更多