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;
}

该程序输出:

image

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

类类型为 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.value和second.value。first.value与second.value相互独立。因此,上述程序将输出:

image

类的成员变量可通过static关键字声明为静态变量。与普通成员变量不同,静态成员变量static member variables由该类的所有对象共享。请看以下与上述类似的程序:

#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;
}

该程序产生以下输出:

image

由于 s_value 是静态成员变量,该变量在类的所有对象间共享。因此 first.s_value 与 second.s_value 是同一个变量。上述程序表明:通过 first 设置的值,也可以通过 second 访问!


静态成员不与类对象相关联

尽管可以通过类对象访问静态成员(如上例中的first.s_value和second.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;
}

image

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

最佳实践:
使用类名和作用域解析运算符 (:😃 访问静态成员。


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

当我们在类类型内部声明静态成员变量时,只是告知编译器该变量的存在,并未真正定义它(类似于前向声明)。由于静态成员变量本质上是全局变量,必须在类外部的全局作用域中显式定义(并可选地初始化)该静态成员。

在上例中,我们通过以下代码实现:

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

该语句具有双重作用:它实例化了静态成员变量(如同全局变量),并对其进行初始化。在此示例中,我们为其赋予初始值1。若未提供初始化表达式,静态成员变量默认将被初始化为零。

需注意此静态成员定义不受访问控制限制:即使在类中声明为私有(或受保护),仍可定义并初始化其值(因定义不被视为访问形式)。

对于非模板类,若类定义在头文件(.h)中,静态成员定义通常置于对应的类代码文件(如Something.cpp)中。另一种方式是将成员定义为inline,并置于头文件的类定义下方(这对纯头文件库尤为实用)。若类定义在源文件(.cpp)中,静态成员定义通常直接置于类定义下方。切勿将静态成员定义放在头文件中(类似全局变量,若该头文件被多次包含,将导致多重定义引发链接器错误)。

对于模板类,其静态成员定义通常直接置于模板类定义下方(此处不会违反ODR规则,因这类定义默认具有内联属性)。


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

针对上述情况存在若干简化方案。首先,当静态成员为常量整数类型(包括 char 和 bool)或 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
};

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

由于常量表达式成员默认具有内联特性(自C++17起),静态常量表达式成员也可在类定义内部初始化,无需显式使用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
};

最佳实践:
将静态成员声明为inline或constexpr,以便它们能在类定义内部初始化。


静态成员变量的示例

为何要在类内部使用静态变量?其用途之一是为类的每个实例分配唯一标识符。以下是一个示例:

#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;
}

该程序输出:

image

由于 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;
}

image

image

posted @ 2026-01-02 18:25  游翔  阅读(16)  评论(0)    收藏  举报