如何创建一个静态类?

313
如何在C++中创建一个静态类?我应该能够做类似这样的事情:
cout << "bit 5 is " << BitParser::getBitAt(buffer, 5) << endl;

假设我创建了一个名为BitParser的类。那么BitParser类的定义会是什么样子?

9
@Vagrant 命名空间内的函数仍然是函数。属于类的函数称为方法。如果它是静态方法,您可以像调用命名空间内的函数一样调用它。 - user1598585
据我所知,“静态”类有一个很好的用途:专门化重载模板函数 - 请参见这里的“道德原则#2”(http://www.gotw.ca/publications/mill17.htm)。@superjoe30 - bcrist
3
在某些情况下,只具有静态方法的IMO类似容器类很有用。 - AarCee
静态类模板可用于消除多个类模板中的冗余类型声明。 - anni
15个回答

332
如果你想要在类中应用像C#中那样的static关键字,那么除非使用Managed C++,否则你将无法实现。
但根据你的示例,你只需要在BitParser对象上创建一个公共的静态方法。就像这样:

BitParser.h

class BitParser
{
public:
  static bool getBitAt(int buffer, int bitIndex);

  // ...

  // Disallow creating an instance of this object
  // (Making all constructors private also works but is not ideal and does not
  // convey your intent as well)
  BitParser() = delete;
};

BitParser.cpp

bool BitParser::getBitAt(int buffer, int bitIndex)
{
  bool isBitSet = false;
  // .. determine if bit is set
  return isBitSet;
}

你可以使用这段代码以与你的示例代码相同的方式调用方法。

5
OJ,你有一个语法错误。应该只在类定义中使用static关键字,而不是在方法定义中使用。请注意修改。 - andrewrk
111
为了让这种方法的意图清晰,您可以使用私有构造函数进行补充。private: BitParser() {} 这将防止任何人创建实例。 - Danvil
8
@MoatazElmasry 线程安全是在共享状态时会出现的问题。在上述实现中,没有共享状态,因此不存在任何线程安全问题… 除非你愚蠢到在这些函数内部使用静态变量。因此,是的,上面的代码是线程安全的,只要将持久状态保持在函数外部就可以了。 - OJ.
@MoatazElmasry 错误。两个线程无法修改静态函数中的非静态局部变量。 - OJ.
1
如果你的目的是让类具有类似于C#静态类的行为,那么应该使用final关键字(从C++11开始)将该类标记为密封类,以防止其他类继承。 - Jämes
39
如果是C++11,我会建议使用 BitParser() = delete; 来明确表达移除构造函数的意图(而不仅仅将其隐藏为 private)。 - phoenix

286

考虑 Matt Price的解决方案

  1. 在C++中,“静态类”没有意义。最接近的概念是一个只有静态方法和成员的类。
  2. 使用静态方法将限制你。

你想要的是,在C++语义中,将你的函数(因为它一个函数)放入命名空间中。

编辑 2011-11-11

C++中没有“静态类”的概念。最接近的概念是一个只有静态方法的类。例如:

// header
class MyClass
{
   public :
      static void myMethod() ;
} ;

// source
void MyClass::myMethod()
{
   // etc.
}

但是你必须记住,“静态类”在类似Java的语言(例如C#)中属于一种破解方式,这些语言无法拥有非成员函数,因此它们将这些函数作为静态方法放置在类内部。

在C++中,你真正想要的是一个声明在命名空间中的非成员函数:

// header
namespace MyNamespace
{
   void myMethod() ;
}

// source
namespace MyNamespace
{
   void myMethod()
   {
      // etc.
   }
}

为什么会这样呢?
在C++中,命名空间比类更强大,适用于“Java静态方法”模式,原因如下:
- 静态方法可以访问类的私有符号 - 私有静态方法对所有人仍然可见(尽管无法访问),这在一定程度上破坏了封装性 - 静态方法无法进行前向声明 - 类用户无法重载静态方法,除非修改库头文件 - 静态方法无法做到的事情,一个(可能是友元)非成员函数在相同命名空间中可以更好地实现 - 命名空间具有自己的语义(可以组合,可以匿名等) - 等等
结论:不要将Java/C#的模式直接复制粘贴到C++中。在Java/C#中,该模式是必需的。但在C++中,这是不好的风格。
2010-06-10编辑:
有人提出了支持静态方法的论点,因为有时候需要使用静态私有数据成员。
我有些不同意见,如下所示:
“静态私有成员”解决方案
// HPP

class Foo
{
   public :
      void barA() ;
   private :
      void barB() ;
      static std::string myGlobal ;
} ;

首先,myGlobal之所以被称为myGlobal,是因为它仍然是一个全局私有变量。查看CPP源代码将会澄清这一点:

// CPP
std::string Foo::myGlobal ; // You MUST declare it in a CPP

void Foo::barA()
{
   // I can access Foo::myGlobal
}

void Foo::barB()
{
   // I can access Foo::myGlobal, too
}

void barC()
{
   // I CAN'T access Foo::myGlobal !!!
}

乍一看,free函数barC无法访问Foo::myGlobal这个事实从封装的角度来看是件好事... 这很酷,因为除非采取破坏行为,否则在查看HPP时无法访问Foo::myGlobal。
但是如果你仔细观察,你会发现这是一个巨大的错误:不仅你的私有变量仍然必须在HPP中声明(因此,尽管是私有的,对所有人可见),而且你必须在同一个HPP中声明所有(全部)被授权访问它的函数!!!
所以使用私有静态成员就像赤身裸体地走在外面,身上刺着你情人的名单:没有人被授权触碰,但每个人都能窥视。还有额外的奖励:每个人都可以知道那些被授权玩弄你的隐私的人的名字。
确实是"private"... :-D

"匿名命名空间"解决方案

匿名命名空间将具有使事物真正私有的优点。
首先,HPP头文件
// HPP

namespace Foo
{
   void barA() ;
}

只是为了确保你注意到:barB和myGlobal都没有无用的声明。这意味着读取头文件的人不知道barA背后隐藏着什么。
然后,CPP文件:
// CPP
namespace Foo
{
   namespace
   {
      std::string myGlobal ;

      void Foo::barB()
      {
         // I can access Foo::myGlobal
      }
   }

   void barA()
   {
      // I can access myGlobal, too
   }
}

void barC()
{
   // I STILL CAN'T access myGlobal !!!
}

如你所见,就像所谓的“静态类”声明一样,fooA和fooB仍然可以访问myGlobal。但是其他人无法访问。而且,在这个CPP之外,没有人知道fooB和myGlobal的存在!
与“静态类”赤身裸体地走在大街上,她的通讯录纹在皮肤上不同,“匿名”命名空间则完全穿着衣服,这似乎更好地封装了。
这真的重要吗?
除非你的代码使用者是破坏者(我会让你自己找出如何通过肮脏的行为未定义的黑客手段来访问公共类的私有部分...),否则“private”就是“private”,即使它在头文件中的“private”部分可见。
然而,如果你需要添加另一个具有访问私有成员的“私有函数”,你仍然必须通过修改头文件向全世界声明它,这在我看来是个悖论:如果我改变我的代码实现(CPP部分),那么接口(HPP部分)不应该改变。引用Leonidas的话:“这就是封装!”

编辑 2014-09-20

在什么情况下,类的静态方法比非成员函数的命名空间更好?

当您需要将函数分组并将该组提供给模板时。

namespace alpha
{
   void foo() ;
   void bar() ;
}

struct Beta
{
   static void foo() ;
   static void bar() ;
};

template <typename T>
struct Gamma
{
   void foobar()
   {
      T::foo() ;
      T::bar() ;
   }
};

Gamma<alpha> ga ; // compilation error
Gamma<Beta> gb ;  // ok
gb.foobar() ;     // ok !!!

因为,如果一个类可以作为模板参数,那么命名空间就不能。

4
GCC支持-fno-access-control选项,可以在白盒单元测试中访问原本私有的类成员。这可能是使用类成员而不是匿名/静态全局变量实现的唯一合理理由。 - Tom
8
@Tom:一个跨平台的解决方案是在头文件中添加以下代码 #define private public ... ^_^ ... - paercebal
1
@Tom:无论如何,在我看来,即使考虑到单元测试,"显示太多东西"的缺点也超过了优点。我猜另一种解决方案是将要测试的代码放在一个函数中,该函数接受所需参数(而不是更多)并位于utilities命名空间中。这样,该函数可以进行单元测试,仍然没有特殊访问私有成员的权限(因为它们在函数调用时作为参数给出)... - paercebal
@paercebal 我即将加入你的队伍,但我还有一个最后的顾虑。如果有人进入你的“命名空间”,他们不会获得访问你的“全局”成员吗?尽管这些成员是隐藏的,但显然他们必须猜测,除非你有意混淆你的代码,否则变量名很容易被猜到。 - Zak
@Zak:确实,他们可以这样做,但只能尝试在声明myGlobal变量的CPP文件中执行。重点是可见性而不是可访问性。在静态类中,myGlobal变量是私有的,但仍然可见。这并不像看起来那么重要,但在DLL中,在导出的头文件中显示应该是DLL私有的符号可能会很尴尬...在命名空间中,myGlobal仅存在于CPP文件中(甚至可以更进一步,使其成为静态的)。该变量不会出现在公共头文件中。 - paercebal
显示剩余5条评论

71

您也可以在命名空间中创建一个免费函数:

在BitParser.h文件中:

namespace BitParser
{
    bool getBitAt(int buffer, int bitIndex);
}

在 BitParser.cpp 文件中

namespace BitParser
{
    bool getBitAt(int buffer, int bitIndex)
    {
        //get the bit :)
    }
}

一般情况下,这将是编写代码的首选方式。当没有必要使用对象时,请不要使用类。


2
在某些情况下,即使类大多是“静态”的,您可能仍希望具有数据封装。静态私有类成员将为您提供此功能。命名空间成员始终是公共的,无法提供数据封装。 - Torleif
如果“member”变量仅在.cpp文件中声明和访问,则比在.h文件中声明的私有变量更私有。但我不建议使用这种技术。 - jmucchiello
4
@Torleif:你错了。 命名空间比静态私有成员更好地实现了封装性。 请参见我的答案以进行演示。 - paercebal
1
是的,但在命名空间中,您必须保持函数顺序,与具有静态成员的类相反,例如void a(){b();} b(){}将在命名空间中产生错误,但不会在具有静态成员的类中产生错误。 - Moataz Elmasry

13
如果您想将 "static" 关键字应用于类,就像在 C# 中一样, 那么静态类只是编译器在手把手地指导您,并阻止您编写任何实例方法/变量。
如果您只编写一个没有任何实例方法/变量的普通类,这与使用C++时的情况相同。

不是要抱怨(特别是针对你),但有一些编译器的辅助措施可以防止我写或者复制/粘贴200遍“static”这个单词,那会是一件好事。 - 3Dave
同意 - 但是在C#中的静态类也不会这样做。当您忘记在其中粘贴static时,它只会编译失败 :-) - Orion Edwards
好的 - 没错。我的宏正在显示。老实说,如果我将类声明为静态的,编译器只应在我尝试实例化它时才抛出错误。要求我重复自己的规则很讨厌,当革命来临时,这些规则应该是第一个被推翻的。 - 3Dave

12

我能写类似于static class这样的语法吗?

不行,根据C++11 N3337标准草案附录C 7.1.1:

更改: 在 C++ 中,static 或 extern 修饰符只能用于对象或函数名。在 C++ 中,将这些修饰符与类型声明一起使用是非法的。在 C 中,当在类型声明上使用这些修饰符时会被忽略。例如:

static struct S {    // valid C, invalid in C++
  int i;
};

解释:当与类型相关联时,存储类别说明符没有任何意义。在C++中,类的成员可以使用static存储类别说明符声明。允许在类型声明上使用存储类别说明符可能会使用户的代码变得混乱。

struct一样,class也是一个类型声明。

通过在附录A中遍历语法树,可以得出相同的结论。

有趣的是,在C中,static struct是合法的,但没有效果:Why and when to use static structures in C programming?


11

C++中你想创建一个类的静态函数(而不是静态类)。

class BitParser {
public:
  ...
  static ... getBitAt(...) {
  }
};

你应该能够使用 BitParser::getBitAt() 调用函数,而无需实例化一个对象,这也是我假定的预期结果。


10

正如在这里所指出的,C++中实现此功能更好的方法可能是使用命名空间。但由于没有人在这里提到关键字final,因此我会发布一个直接相当于C#中static class的C++11或更高版本代码:

class BitParser final
{
public:
  BitParser() = delete;

  static bool GetBitAt(int buffer, int pos);
};

bool BitParser::GetBitAt(int buffer, int pos)
{
  // your code
}

5

如前所述,您可以在C++中使用静态类。 静态类是没有实例化对象的类。 在C ++中,这可以通过将构造函数/析构函数声明为私有来实现。 最终结果是相同的。


你所建议的可能会创建一个单例类,但它并不同于静态类。 - ksinkar

4
在托管C++中,静态类的语法是:-
public ref class BitParser abstract sealed
{
    public:
        static bool GetBitAt(...)
        {
            ...
        }
}

迟到总比不到好。


4
与其他托管编程语言不同,C++中的“static class”没有意义。您可以使用静态成员函数。

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