15.6 — 静态成员变量

在课程 7.4 — 全局变量简介 中,我们介绍了全局变量,在课程 7.11 — 静态局部变量 中,我们介绍了静态局部变量。这两种类型的变量都具有静态持续时间,这意味着它们在程序启动时创建,并在程序结束时销毁。即使它们超出作用域,此类变量也会保留其值。

例如

#include <iostream>

int generateID()
{
    static int s_id{ 0 }; // static local variable
    return ++s_id;
}

int main()
{
    std::cout << generateID() << '\n';
    std::cout << generateID() << '\n';
    std::cout << generateID() << '\n';

    return 0;
}

这个程序打印

1
2
3

请注意,静态局部变量 s_id 在多次函数调用中都保留了其值。

类类型为 static 关键字带来了另外两种用途:静态成员变量和静态成员函数。幸运的是,这些用途相当简单。我们将在本课中讨论静态成员变量,在下一课中讨论静态成员函数。

静态成员变量

在深入探讨应用于成员变量的 static 关键字之前,首先考虑以下类

#include <iostream>

struct Something
{
    int value{ 1 };
};

int main()
{
    Something first{};
    Something second{};
    
    first.value = 2;

    std::cout << first.value << '\n';
    std::cout << second.value << '\n';

    return 0;
}

当我们实例化一个类对象时,每个对象都会获得所有普通成员变量的副本。在这种情况下,因为我们声明了两个 Something 类对象,所以我们最终得到了两个 value 的副本:first.valuesecond.valuefirst.valuesecond.value 是不同的。因此,上面的程序打印

2
1

类的成员变量可以通过使用 static 关键字来设置为静态。与普通成员变量不同,静态成员变量由类的所有对象共享。考虑以下程序,与上面类似

#include <iostream>

struct Something
{
    static int s_value; // declare s_value as static (initializer moved below)
};

int Something::s_value{ 1 }; // define and initialize s_value to 1 (we'll discuss this section below)

int main()
{
    Something first{};
    Something second{};

    first.s_value = 2;

    std::cout << first.s_value << '\n';
    std::cout << second.s_value << '\n';
    return 0;
}

此程序生成以下输出:

2
2

因为 s_value 是一个静态成员变量,所以 s_value 在类的所有对象之间共享。因此,first.s_valuesecond.s_value 是同一个变量。上面的程序表明,我们使用 first 设置的值可以使用 second 访问!

静态成员不与类对象关联

尽管您可以通过类对象访问静态成员(如上例中所示的 first.s_valuesecond.s_value),但即使没有实例化任何类对象,静态成员也存在!这很合理:它们在程序开始时创建,在程序结束时销毁,因此它们的生命周期不像普通成员那样与类对象绑定。

本质上,静态成员是存在于类作用域区域内的全局变量。类的静态成员与命名空间中的普通变量之间几乎没有区别。

关键见解

静态成员是存在于类作用域区域内的全局变量。

因为静态成员 s_value 独立于任何类对象而存在,所以它可以直接使用类名和作用域解析运算符访问(在本例中为 Something::s_value

class Something
{
public:
    static int s_value; // declare s_value as static
};

int Something::s_value{ 1 }; // define and initialize s_value to 1 (we'll discuss this section below)

int main()
{
    // note: we're not instantiating any objects of type Something

    Something::s_value = 2;
    std::cout << Something::s_value << '\n';
    return 0;
}

在上面的代码片段中,s_value 通过类名 Something 而不是通过对象引用。请注意,我们甚至没有实例化 Something 类型的对象,但我们仍然能够访问和使用 Something::s_value。这是访问静态成员的首选方法。

最佳实践

使用类名和作用域解析运算符 (::) 访问静态成员。

定义和初始化静态成员变量

当我们在类类型中声明一个静态成员变量时,我们只是告诉编译器存在一个静态成员变量,但实际上并没有定义它(很像前向声明)。由于静态成员变量本质上是全局变量,因此您必须在类外部的全局作用域中显式定义(并可选地初始化)静态成员。

在上面的示例中,我们通过此行实现这一点

int Something::s_value{ 1 }; // define and initialize s_value to 1

这行代码有两个目的:它实例化静态成员变量(就像全局变量一样),并初始化它。在这种情况下,我们提供了初始化值 1。如果没有提供初始化器,静态成员变量默认会进行零初始化。

请注意,此静态成员定义不受访问控制:即使它在类中声明为私有(或受保护),您也可以定义和初始化该值(因为定义不被视为一种访问形式)。

对于非模板类,如果类在头文件 (.h) 中定义,则静态成员定义通常放置在类的相关代码文件(例如 Something.cpp)中。或者,成员也可以定义为 inline 并放置在头文件中的类定义下方(这对于仅头文件库很有用)。如果类在源 (.cpp) 文件中定义,则静态成员定义通常直接放置在类下方。不要将静态成员定义放在头文件中(很像全局变量,如果该头文件被多次包含,您最终会得到多个定义,这将导致链接器错误)。

对于模板类,(模板化的)静态成员定义通常直接放置在头文件中的模板类定义下方(这不会违反 ODR,因为此类定义是隐式内联的)。

在类定义中初始化静态成员变量

上述方法有一些捷径。首先,当静态成员是常量整数类型(包括 charbool)或 const 枚举时,静态成员可以在类定义中初始化

class Whatever
{
public:
    static const int s_value{ 4 }; // a static const int can be defined and initialized directly
};

在上面的示例中,因为静态成员变量是 const int,所以不需要显式定义行。允许使用此快捷方式,因为这些特定的 const 类型是编译时常量。

在课程 7.10 — 在多个文件之间共享全局常量(使用内联变量) 中,我们介绍了内联变量,这些变量允许有多个定义。C++17 允许静态成员成为内联变量

class Whatever
{
public:
    static inline int s_value{ 4 }; // a static inline variable can be defined and initialized directly
};

此类变量可以在类定义内部初始化,无论它们是否为常量。这是定义和初始化静态成员的首选方法。

由于 constexpr 成员在 C++17 中是隐式内联的,因此静态 constexpr 成员也可以在类定义中初始化,而无需显式使用 inline 关键字

#include <string_view>

class Whatever
{
public:
    static constexpr double s_value{ 2.2 }; // ok
    static constexpr std::string_view s_view{ "Hello" }; // this even works for classes that support constexpr initialization
};

最佳实践

将静态成员声明为 inlineconstexpr,以便它们可以在类定义中初始化。

静态成员变量的示例

为什么要在类中使用静态变量?一种用途是为类的每个实例分配唯一的 ID。这是一个示例

#include <iostream>

class Something
{
private:
    static inline int s_idGenerator { 1 };
    int m_id {};

public:
    // grab the next value from the id generator
    Something() : m_id { s_idGenerator++ } 
    {    
    }

    int getID() const { return m_id; }
};

int main()
{
    Something first{};
    Something second{};
    Something third{};

    std::cout << first.getID() << '\n';
    std::cout << second.getID() << '\n';
    std::cout << third.getID() << '\n';
    return 0;
}

这个程序打印

1
2
3

因为 s_idGenerator 由所有 Something 对象共享,所以当创建新的 Something 对象时,构造函数使用 s_idGenerator 的当前值初始化 m_id,然后为下一个对象递增该值。这保证了每个实例化的 Something 对象都接收到一个唯一的 ID(按创建顺序递增)。

为每个对象赋予唯一的 ID 有助于调试,因为它可以用来区分具有相同数据的对象。在处理数据数组时尤其如此。

当类需要使用查找表(例如,用于存储一组预计算值的数组)时,静态成员变量也很有用。通过使查找表静态化,所有对象只存在一个副本,而不是为每个实例化的对象创建副本。这可以节省大量的内存。

只有静态成员才能使用类型推导(auto 和 CTAD)

静态成员可以使用 auto 从其初始化器推导其类型,或使用类模板参数推导 (CTAD) 从初始化器推导模板类型参数。

非静态成员不能使用 auto 或 CTAD。

造成这种区别的原因非常复杂,但归结为非静态成员可能出现某些情况,导致歧义或非直观结果。静态成员不会出现这种情况。因此,非静态成员被限制使用这些功能,而静态成员则不受限制。

#include <utility> // for std::pair<T, U>

class Foo
{
private:
    auto m_x { 5 };           // auto not allowed for non-static members
    std::pair m_v { 1, 2.3 }; // CTAD not allowed for non-static members

    static inline auto s_x { 5 };           // auto allowed for static members
    static inline std::pair s_v { 1, 2.3 }; // CTAD allowed for static members

public:
    Foo() {};
};

int main()
{
    Foo foo{};
    
    return 0;
}
guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
306 条评论
最新
最早 最多投票
内联反馈
查看所有评论