C++中的静态构造函数?我需要初始化私有的静态对象。

193
我想在一个类中拥有一个私有静态数据成员(包含所有小写字母a-z的向量)。在Java或C#中,我可以创建一个“静态构造函数”,它会在创建类的任何实例之前运行,并设置类的静态数据成员。它只会运行一次(因为变量是只读的,且只需要设置一次),而且由于它是类的函数,所以可以访问其私有成员。我可以在构造函数中添加代码来检查向量是否初始化,如果没有则进行初始化,但这引入了许多必要的检查,看起来并不像是解决问题的最佳方案。
我认为既然变量是只读的,它们可以被声明为公共静态常量,这样我就可以在类外部设置它们一次。但再次强调,这似乎仍是一种丑陋的hack方法。
如果我不想在实例构造函数中初始化它们,那么有可能创建一个私有静态数据成员吗?
23个回答

192

要获得静态构造函数的等效功能,您需要编写一个单独的普通类来保存静态数据,然后创建该普通类的静态实例。

class StaticStuff
{
     std::vector<char> letters_;

public:
     StaticStuff()
     {
         for (char c = 'a'; c <= 'z'; c++)
             letters_.push_back(c);
     }

     // provide some way to get at letters_
};

class Elsewhere
{
    static StaticStuff staticStuff; // constructor runs once, single instance

};

13
谢谢!虽然这样做很烦人。这是C#和Java学到的许多“错误”之一。 - Gordon Gustafson
119
是的。我经常向人们指出,如果C++没有犯那些“错误”,其他语言也将不得不犯这些错误。即使C++犯了错误,但它所涵盖的范围非常广泛,这对于随后的语言来说是很有好处的。 - quark
12
只有一个小的细微差别,由于构造函数的作用,没有人保证静态对象的构造函数何时执行。一个众所周知的更安全的方法是:class Elsewhere { StaticStuff& get_staticStuff() { static StaticStuff staticStuff; // 只有当需要时才会运行构造函数一次 return staticStuff; }};我想知道在C#和Java中的静态构造函数是否能够提供与上述代码相同的保证... - Oleg Zhylin
13
@Oleg: 是的,它们确实会执行。标准保证在进入main函数之前会执行所有非本地变量的构造函数。它还保证在编译单元内,构建顺序是明确定义的,并且与编译单元内的声明顺序相同。不幸的是,它们没有定义多个编译单元之间的顺序。 - Martin York
13
这实际上是一个例子,说明使用“friend”关键字非常合理,这样“Elsewhere”类就可以轻松访问“StaticStuff”的内部(我必须指出,这并不会以任何危险的方式破坏封装性)。 - Konrad Rudolph
显示剩余8条评论

86

好的,你可以拥有

class MyClass
{
    public:
        static vector<char> a;

        static class _init
        {
          public:
            _init() { for(char i='a'; i<='z'; i++) a.push_back(i); }
        } _initializer;
};

不要忘记(在 .cpp 文件中)加入以下内容:

vector<char> MyClass::a;
MyClass::_init MyClass::_initializer;

即使没有第二行,该程序仍将链接,但初始化程序将不会被执行。


3
你好,我可以帮您查找有关"initializer"魔法的更多信息。 - Karel Bílek
应该是 MyClass::a.push_back(i) 而不是 a.push_back(i) 吧? - Neel Basu
4
_initializerMyClass 的一个子对象。子对象按照以下顺序初始化:虚拟基类子对象按深度优先,从左往右的顺序(但每个不同的子对象只初始化一次);然后是普通基类子对象,按深度优先,从左往右的顺序;最后是成员子对象,按声明顺序排列。因此,如果 _initialiser 中的代码只涉及到在它之前声明的成员,则使用 EFraim 的策略是安全的。 - j_random_hacker
FYI:我添加了必要的静态定义,将_init()变成了私有方法,并测试代码仍然可以正常工作。 - Blaisorblade
尝试在Visual C++ 2010上运行,但是如果不将_init构造函数声明为public(或将_init声明为结构体),则无法正常工作。您的代码是否应该这样工作? - OregonGhost
显示剩余2条评论

49

C++11更新

自C++11以来,您可以简单地使用lambda表达式来初始化静态类成员。您不再需要使用任何辅助类或解决方法。

头文件:

class MyClass {
    static const vector<char> letters;
};

源文件:

// Initialize MyClass::letters with all letters from 'a' to 'z'.
const vector<char> MyClass::letters = [] {
    vector<char> letters;
    for (char c = 'a'; c <= 'z'; c++)
        letters.push_back(c);
    return letters;
}();

关于静态初始化顺序的注意事项:

如果多个静态类成员必须按照某个特定顺序初始化,这种方法适用。由于静态成员始终按照源文件中定义的完全相同的顺序初始化,因此您只需确保在源文件中以正确的顺序编写初始化即可。


有趣的解决方案。在这种情况下,如果我抛出异常,谁可以捕获它? - rafi wiener
7
静态程序初始化代码绝不能抛出任何异常,否则程序将崩溃。如果可能会抛出异常,则必须将初始化逻辑包装在 try catch 块中。 - emkey08

21
在 .h 文件中:
class MyClass {
private:
    static int myValue;
};
在 .cpp 文件中:
#include "myclass.h"

int MyClass::myValue = 0;

5
对于单个静态成员(无论其类型),这很好用。与静态构造函数相比的不足之处在于,你不能在各个静态成员之间强制执行顺序。如果你需要这样做,请参照Earwicker的回答。 - quark
我正在做这件事,但它仍然无法编译。并且它指出这是问题区域(在构造函数中,而不是头文件)。 - Flotolk

17

这里提供一种与Daniel Earwicker的方法类似的另一种方式,同样使用了Konrad Rudolph的友元类建议。我们在此处使用一个内部的私有友元实用类来初始化您的主类的静态成员。例如:

头文件:

class ToBeInitialized
{
    // Inner friend utility class to initialize whatever you need

    class Initializer
    {
    public:
        Initializer();
    };

    friend class Initializer;

    // Static member variables of ToBeInitialized class

    static const int numberOfFloats;
    static float *theFloats;

    // Static instance of Initializer
    //   When this is created, its constructor initializes
    //   the ToBeInitialized class' static variables

    static Initializer initializer;
};

实现文件:

// Normal static scalar initializer
const int ToBeInitialized::numberOfFloats = 17;

// Constructor of Initializer class.
//    Here is where you can initialize any static members
//    of the enclosing ToBeInitialized class since this inner
//    class is a friend of it.

ToBeInitialized::Initializer::Initializer()
{
    ToBeInitialized::theFloats =
        (float *)malloc(ToBeInitialized::numberOfFloats * sizeof(float));

    for (int i = 0; i < ToBeInitialized::numberOfFloats; ++i)
        ToBeInitialized::theFloats[i] = calculateSomeFancyValue(i);
}

采用这种方法的优点是完全隐藏了初始化器类,使一切都包含在要初始化的类内部。


2
此外,您必须确保调用ToBeInitialized::Initializer::Initializer(),因此需要在实现文件中添加ToBeInitialized::Initializer ToBeInitialized::initializer;。我从您的想法和EFraim的想法中借鉴了一些东西,它完全按照我的要求工作并且看起来很干净。谢谢,伙计。 - Andrew Larsson

12

Test::StaticTest() 在全局静态初始化期间仅被调用一次。

调用者只需在他们的静态构造函数中添加一行即可。

static_constructor<&Test::StaticTest>::c; 强制在全局静态初始化期间初始化 c

template<void(*ctor)()>
struct static_constructor
{
    struct constructor { constructor() { ctor(); } };
    static constructor c;
};

template<void(*ctor)()>
typename static_constructor<ctor>::constructor static_constructor<ctor>::c;

/////////////////////////////

struct Test
{
    static int number;

    static void StaticTest()
    {
        static_constructor<&Test::StaticTest>::c;

        number = 123;
        cout << "static ctor" << endl;
    }
};

int Test::number;

int main(int argc, char *argv[])
{
    cout << Test::number << endl;
    return 0;
}

9

不需要 init() 函数,std::vector 可以从范围创建:

// h file:
class MyClass {
    static std::vector<char> alphabet;
// ...
};

// cpp file:
#include <boost/range.hpp>
static const char alphabet[] = "abcdefghijklmnopqrstuvwxyz";
std::vector<char> MyClass::alphabet( boost::begin( ::alphabet ), boost::end( ::alphabet ) );

请注意,类类型的静态成员在库中容易出现问题,因此应该避免使用。

C++11更新

从C++11开始,您可以使用以下方法:

// cpp file:
std::vector<char> MyClass::alphabet = { 'a', 'b', 'c', ..., 'z' };

这个方法在语义上与原始答案中的C++98解决方案等效,但是你不能在右侧使用字符串字面量,因此它并不完全优越。然而,如果你有一个除了charwchar_tchar16_tchar32_t之外的任何其他类型的向量(数组可以写成字符串字面量),与C++98版本相比,C++11版本将严格消除样板代码而不引入其他语法。


我喜欢它。虽然如果我们可以在一行内完成而不使用无用的字母那就好了。 - Martin York
对于库的问题,静态类是私有的还是公共的是否重要?此外,静态库(.a)和动态库(.so)是否重要? - Zachary Kraus
@ZacharyKraus:什么是公共/私有 _类_?虽然问题不同,但重叠部分并不影响库是静态链接还是动态链接。 - Marc Mutz - mmutz
@MarcMutz-mmutz 很抱歉我使用了不正确的C++术语,应该是公共/私有类。我所指的是EFraim上面提出的解决方案。在我的版本中,我将静态类成员设为私有。我试图理解在库开发和可用性方面,将静态类成员设置为公共或私有是否会有影响。我的直觉告诉我这不应该影响库,因为用户永远不会访问静态类成员或其构建的对象,但我很想听取一些大师对这个话题的智慧。 - Zachary Kraus
@ZacharyKraus:需要动态初始化的静态变量([basic.start.init]/2)的主要问题在于它们会运行代码。在库中,当析构函数运行时,库代码可能已经被卸载。如果您想了解更多信息,请发表相关问题。 - Marc Mutz - mmutz
谢谢,我会在论坛上发一个问题,因为我在动态库中使用了相当数量的单例和其他静态结构。 - Zachary Kraus

7
在Java中,静态构造函数的概念是在从C ++中学到问题之后引入的。因此我们没有直接的等价物。
最好的解决方案是使用可以明确初始化的POD类型。
或者将您的静态成员作为具有自己的构造函数的特定类型,该构造函数将正确地初始化它。
//header

class A
{
    // Make sure this is private so that nobody can missues the fact that
    // you are overriding std::vector. Just doing it here as a quicky example
    // don't take it as a recomendation for deriving from vector.
    class MyInitedVar: public std::vector<char>
    {
        public:
        MyInitedVar()
        {
           // Pre-Initialize the vector.
           for(char c = 'a';c <= 'z';++c)
           {
               push_back(c);
           }
        }
    };
    static int          count;
    static MyInitedVar  var1;

};


//source
int            A::count = 0;
A::MyInitedVar A::var1;

4
尝试编译并使用Earwicker's answer中的 Elsewhere 类时,我遇到了以下问题:
error LNK2001: unresolved external symbol "private: static class StaticStuff Elsewhere::staticStuff" (?staticStuff@Elsewhere@@0VStaticStuff@@A)

似乎不可能在类定义中初始化非整数类型的静态属性,除非在类定义之外放置一些代码(CPP)。

为了使其编译,您可以使用"带有内部静态局部变量的静态方法"。像这样:

class Elsewhere
{
public:
    static StaticStuff& GetStaticStuff()
    {
        static StaticStuff staticStuff; // constructor runs once, single instance
        return staticStuff;
    }
};

你也可以向构造函数传递参数或使用特定值进行初始化,它非常灵活、强大且易于实现……唯一的问题是你有一个包含静态变量而不是静态属性的静态方法……语法有些变化,但仍然很有用。希望对某人有所帮助。——Hugo González Castro。

虽然要小心使用线程。我相信在GCC中,静态局部变量的构造受到并发执行的保护,但在Visual C++中则不是这样。 - Daniel Earwicker
1
从C++11开始,在POSIX中,它必须是线程安全的。 - Marc Mutz - mmutz
我非常喜欢上面的两个解决方案(这个这个),但是你的是唯一一个确保在跨库需要时按顺序初始化静态变量的方法。我只是像你上面那样有一个私有的静态实例方法,并将对其他值的访问包装在公共的静态访问器中,这些访问器使用该实例方法而不是直接引用。谢谢。 - FlintZA

4
我想,这个问题的简单解决方案是:

我猜,简单的解决方案如下:

    //X.h
    #pragma once
    class X
    {
    public:
            X(void);
            ~X(void);
    private:
            static bool IsInit;
            static bool Init();
    };

    //X.cpp
    #include "X.h"
    #include <iostream>

    X::X(void)
    {
    }


    X::~X(void)
    {
    }

    bool X::IsInit(Init());
    bool X::Init()
    {
            std::cout<< "ddddd";
            return true;
    }

    // main.cpp
    #include "X.h"
    int main ()
    {
            return 0;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接