如何在C++中初始化私有静态成员

How to initialize private static members in C++?

本文关键字:静态成员 初始化 C++      更新时间:2023-10-16

C++中初始化私有静态数据成员的最佳方法是什么? 我在头文件中尝试过这个,但它给了我奇怪的链接器错误:

class foo
{
    private:
        static int i;
};
int foo::i = 0;

猜这是因为我无法从类外初始化私有成员。 那么最好的方法是什么呢?

类声明应位于头文件中,或者如果类未在其他文件中使用,则应位于源文件中。

// foo.h
class foo
{
    private:
        static int i;
};

但是,初始化应该在源文件中。

// foo.cpp
int foo::i = 0;

如果初始化在头文件中,则包含头文件的每个文件都将具有静态成员的定义。因此,在链接阶段,您将收到链接器错误,因为初始化变量的代码将在多个源文件中定义。static int i的初始化必须在任何函数之外完成。

注意:Matt Curtis:指出,如果静态数据成员是常量整数类型(boolcharchar8_t[自C++20],char16_tchar32_twchar_tshortintlonglong long,或任何实现定义的扩展整数类型,包括任何有符号,无符号,则C++允许简化上述内容 和符合 CV 标准的变体。然后,可以直接在头文件的类声明中声明和初始化数据成员:

class foo
{
    private:
        static int const i = 42;
};

对于变量

傅炯:

class foo
{
private:
    static int i;
};

foo.cpp:

int foo::i = 0;

这是因为程序中只能有一个foo::i实例。这相当于头文件中的extern int i和源文件中的int i

对于常量,您可以将值直接放在类声明中:

class foo
{
private:
    static int i;
    const static int a = 42;
};

从 C++17 开始,可以使用内联关键字在标头中定义静态成员。

http://en.cppreference.com/w/cpp/language/static

"静态数据成员可以内联声明。可以在类定义中定义内联静态数据成员,并且可以指定默认成员初始值设定项。它不需要类外定义:"

struct X
{
    inline static int n = 1;
};

对于这个问题的未来观众,我想指出你应该避免monkey0506的建议。

头文件用于声明。

头文件对于直接或间接#includes它们的每个.cpp文件编译一次,任何函数之外的代码在程序初始化时运行,然后再main()

通过将 : foo::i = VALUE;放入标头中,将为每个.cpp文件分配值VALUE foo:i(无论该值是什么),这些分配将在运行main()之前以不确定的顺序(由链接器确定)进行。

如果我们#define VALUE .cpp文件中的不同数字怎么办?它将编译良好,在运行程序之前,我们将无法知道哪一个获胜。

切勿将执行的代码放入标头中,原因与从不#include .cpp文件相同。

包括保护(我同意您应该始终使用)可以保护您免受不同事物的影响:在编译单个.cpp文件时,同一标头被间接#include多次。

使用Microsoft编译器[1],非int-like的静态变量也可以在头文件中定义,但在类声明之外,使用Microsoft特定的__declspec(selectany)

class A
{
    static B b;
}
__declspec(selectany) A::b;
请注意,我

并不是说这很好,我只是说可以做到。

[1] 如今,比 MSC 更多的编译器支持__declspec(selectany) - 至少是 gcc 和 clang。也许更多。

int foo::i = 0; 

是初始化变量的正确语法,但它必须位于源文件 (.cpp) 中,而不是标头中。

因为它是一个静态变量,编译器只需要创建它的一个副本。你必须在代码中的某个地方有一行"int foo:i",告诉编译器把它放在哪里,否则你会得到一个链接错误。如果这是在标头中,您将在包含标头的每个文件中获得一个副本,因此请从链接器获取多重定义的符号错误。

适用于多个对象的 C++11 静态构造函数模式

提出了一个习语: https://stackoverflow.com/a/27088552/895245 但这里有一个更干净的版本,不需要为每个成员创建新方法。

主.cpp

#include <cassert>
#include <vector>
// Normally on the .hpp file.
class MyClass {
public:
    static std::vector<int> v, v2;
    static struct StaticConstructor {
        StaticConstructor() {
            v.push_back(1);
            v.push_back(2);
            v2.push_back(3);
            v2.push_back(4);
        }
    } _staticConstructor;
};
// Normally on the .cpp file.
std::vector<int> MyClass::v;
std::vector<int> MyClass::v2;
// Must come after every static member.
MyClass::StaticConstructor MyClass::_staticConstructor;
int main() {
    assert(MyClass::v[0] == 1);
    assert(MyClass::v[1] == 2);
    assert(MyClass::v2[0] == 3);
    assert(MyClass::v2[1] == 4);
}

GitHub 上游。

编译并运行:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

另请参阅: C++中的静态构造函数?我需要初始化私有静态对象

在 Ubuntu 19.04 上测试。

C++17 内联变量

提到: https://stackoverflow.com/a/45062055/895245 但这里有一个多文件可运行的示例,以使其更加清晰:内联变量如何工作?

这个很棒的 C++17 功能使我们能够:

  • 方便地为每个常量仅使用一个内存地址
  • 将其存储为constexpr:如何声明 constexpr extern?
  • 从一个
  • 标题在一行中完成

主.cpp

#include <cassert>
#include "notmain.hpp"
int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

Notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP
inline constexpr int notmain_i = 42;
const int* notmain_func();
#endif

不是主.cpp

#include "notmain.hpp"
const int* notmain_func() {
    return &notmain_i;
}

编译并运行:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub 上游。

如果你想初始化一些复合类型(例如字符串),你可以做这样的事情:

class SomeClass {
  static std::list<string> _list;
  public:
    static const std::list<string>& getList() {
      struct Initializer {
         Initializer() {
           // Here you may want to put mutex
           _list.push_back("FIRST");
           _list.push_back("SECOND");
           ....
         }
      }
      static Initializer ListInitializationGuard;
      return _list;
    }
};

由于 ListInitializationGuard 是方法SomeClass::getList()静态变量,因此它只会构造一次,这意味着构造函数被调用一次。这将initialize _list变量到您需要的值。任何后续对getList的调用都将只返回已经初始化_list对象。

当然,您必须始终通过调用getList()方法来访问_list对象。

我在这里没有足够的代表来添加它作为评论,但 IMO 无论如何,用 #include 警卫编写标题是一种很好的风格,正如 Paranaix 几个小时前指出的那样,这可以防止多定义错误。除非您已经在使用单独的 CPP 文件,否则不必仅使用一个文件来初始化静态非整数成员。

#ifndef FOO_H
#define FOO_H
#include "bar.h"
class foo
{
private:
    static bar i;
};
bar foo::i = VALUE;
#endif

我认为没有必要为此使用单独的 CPP 文件。当然,你可以,但没有技术上的理由为什么你应该这样做。

您遇到的链接器问题可能是由以下原因引起的:

  • 在头文件中提供类和静态成员定义,
  • 将此标头包含在两个或多个源文件中。

对于那些从C++开始的人来说,这是一个常见问题。静态类成员必须在单个翻译单元中初始化,即在单个源文件中。

遗憾的是,静态类成员必须在类主体外部初始化。这使得编写仅标头代码变得复杂,因此,我使用的是完全不同的方法。您可以通过静态或非静态类函数提供静态对象,例如:

class Foo
{
    // int& getObjectInstance() const {
    static int& getObjectInstance() {
        static int object;
        return object;
    }
    void func() {
        int &object = getValueInstance();
        object += 5;
    }
};

如果使用标头保护,还可以在头文件中包括分配。我已经将这种技术用于我创建的C++库。实现相同结果的另一种方法是使用静态方法。例如。。。

class Foo
   {
   public:
     int GetMyStatic() const
     {
       return *MyStatic();
     }
   private:
     static int* MyStatic()
     {
       static int mStatic = 0;
       return &mStatic;
     }
   }

上面的代码有一个"好处",不需要 CPP/源文件。同样,这是我用于C++库的方法。

我遵循卡尔的想法。我喜欢它,现在我也使用它。我稍微改变了符号并添加了一些功能

#include <stdio.h>
class Foo
{
   public:
     int   GetMyStaticValue () const {  return MyStatic();  }
     int & GetMyStaticVar ()         {  return MyStatic();  }
     static bool isMyStatic (int & num) {  return & num == & MyStatic(); }
   private:
      static int & MyStatic ()
      {
         static int mStatic = 7;
         return mStatic;
      }
};
int main (int, char **)
{
   Foo obj;
   printf ("mystatic value %dn", obj.GetMyStaticValue());
   obj.GetMyStaticVar () = 3;
   printf ("mystatic value %dn", obj.GetMyStaticValue());
   int valMyS = obj.GetMyStaticVar ();
   int & iPtr1 = obj.GetMyStaticVar ();
   int & iPtr2 = valMyS;
   printf ("is my static %d %dn", Foo::isMyStatic(iPtr1), Foo::isMyStatic(iPtr2));
}

此输出

mystatic value 7
mystatic value 3
is my static 1 0

也在privateStatic.cpp文件中工作:

#include <iostream>
using namespace std;
class A
{
private:
  static int v;
};
int A::v = 10; // possible initializing
int main()
{
A a;
//cout << A::v << endl; // no access because of private scope
return 0;
}
// g++ privateStatic.cpp -o privateStatic && ./privateStatic

set_default()方法呢?

class foo
{
    public:
        static void set_default(int);
    private:
        static int i;
};
void foo::set_default(int x) {
    i = x;
}

我们只需要使用 set_default(int x) 方法,我们的 static 变量将被初始化。

这不会与其他注释相矛盾,实际上它遵循在全局范围内初始化变量的相同原则,但是通过使用这种方法,我们使其明确(并且易于查看-理解),而不是将变量的定义挂在那里。

定义

常量的一种"老派"方法是用enum替换它们:

class foo
{
    private:
        enum {i = 0}; // default type = int
        enum: int64_t {HUGE = 1000000000000}; // may specify another type
};

这种方式不需要提供定义,并且避免了常量左值,这可以为您省去一些麻烦,例如,当您不小心使用它时。

这里有一个简单的例子中的所有可能性和错误......

#ifndef Foo_h
#define Foo_h
class Foo
{
  static const int a = 42; // OK
  static const int b {7};  // OK
  //static int x = 42; // ISO C++ forbids in-class initialization of non-const static member 'Foo::x'
  //static int y {7};  // ISO C++ forbids in-class initialization of non-const static member 'Foo::x'
  static int x;
  static int y;
  int m = 42;
  int n {7};
};
// Foo::x = 42;  // error: 'int Foo::x' is private
int Foo::x = 42; // OK in Foo.h if included in only one  *.cpp -> *.o file!
int Foo::y {7};  // OK
// int Foo::y {7};  // error: redefinition of 'int Foo::y'
   // ONLY if the compiler can see both declarations at the same time it, 
   // OTHERWISE you get a linker error
#endif // Foo_h

但最好把它放在Foo.cpp。这样,您可以单独编译每个文件并在以后链接它们,否则 Foo:x 将出现在多个目标文件中并导致链接器错误。...

// Foo::x = 42;  // error: 'int Foo::x' is private, bad if Foo::X is public!
int Foo::x = 42; // OK in Foo.h if included in only one  *.cpp -> *.o file!
int Foo::y {7};  // OK

这符合您的目的吗?

//header file
struct MyStruct {
public:
    const std::unordered_map<std::string, uint32_t> str_to_int{
        { "a", 1 },
        { "b", 2 },
        ...
        { "z", 26 }
    };
    const std::unordered_map<int , std::string> int_to_str{
        { 1, "a" },
        { 2, "b" },
        ...
        { 26, "z" }
    };
    std::string some_string = "justanotherstring";  
    uint32_t some_int = 42;
    static MyStruct & Singleton() {
        static MyStruct instance;
        return instance;
    }
private:
    MyStruct() {};
};
//Usage in cpp file
int main(){
    std::cout<<MyStruct::Singleton().some_string<<std::endl;
    std::cout<<MyStruct::Singleton().some_int<<std::endl;
    return 0;
}

当我第一次遇到这个问题时,我只是想提一些对我来说有点奇怪的事情。

我需要在模板类中初始化一个私有静态数据成员。

在 .h 或 .hpp 中,初始化模板类的静态数据成员如下所示:

template<typename T>
Type ClassName<T>::dataMemberName = initialValue;