背景回顾
在《全局变量简介》中,我们介绍了全局变量;在《静态局部变量》中,又介绍了静态局部变量。这两类变量均具有静态存储期:它们在程序启动时创建,在程序结束时销毁,即使离开作用域也保留其值。
示例:
#include <iostream>
int generateID()
{
static int s_id{ 0 }; // 静态局部变量
return ++s_id;
}
int main()
{
std::cout << generateID() << '\n'; // 1
std::cout << generateID() << '\n'; // 2
std::cout << generateID() << '\n'; // 3
return 0;
}
类中的两种新 static
用法
类类型为 static
关键字带来了两种新用途:
- 静态成员变量(本节讨论);
- 静态成员函数(下节讨论)。
静态成员变量
与普通成员变量的区别
先观察一个普通类:
#include <iostream>
struct Something
{
int value{ 1 };
};
int main()
{
Something first{};
Something second{};
first.value = 2;
std::cout << first.value << '\n'; // 2
std::cout << second.value << '\n'; // 1
}
每个对象各自拥有普通成员变量的副本。
若将成员变量声明为 static
,则该变量被所有对象共享:
#include <iostream>
struct Something
{
static int s_value; // 声明为静态
};
int Something::s_value{ 1 }; // 定义并初始化
int main()
{
Something first{};
Something second{};
first.s_value = 2;
std::cout << first.s_value << '\n'; // 2
std::cout << second.s_value << '\n'; // 2
}
静态成员不属于任何对象
尽管可以通过对象访问静态成员(obj.s_value
),但即使未创建任何对象,静态成员也依然存在。它们的生命周期与程序相同,本质上是位于类作用域内的全局变量。
关键洞见
静态成员是位于类作用域中的全局变量。
因此,推荐使用类名加作用域解析运算符访问:
Something::s_value = 2;
最佳实践
使用 类名::静态成员
的形式访问静态成员。
定义与初始化静态成员变量
在类内声明静态成员变量时,仅向编译器告知其存在(类似前向声明)。
必须在类外全局作用域显式定义(并可初始化):
int Something::s_value{ 1 }; // 定义并赋初值
- 若未显式初始化,静态成员将零初始化。
- 该定义不受访问控制限制;即使变量在类中为
private
,也可在类外定义。 - 非模板类:
- 若类定义在头文件,静态成员定义通常放在对应
.cpp
文件; - 也可声明为
inline
并放在头文件中,以支持仅头文件库。
- 若类定义在头文件,静态成员定义通常放在对应
- 模板类:
- 模板静态成员定义通常紧跟类模板定义放在头文件中(隐式
inline
,不违 ODR)。
- 模板静态成员定义通常紧跟类模板定义放在头文件中(隐式
- 切勿把非
inline
静态成员定义放在头文件,否则多重包含会导致链接错误。
类内初始化静态成员
若静态成员为:
const
整型(含char
、bool
);const
枚举类型;
则可在类内直接初始化:
class Whatever
{
public:
static const int s_value{ 4 }; // 无需额外定义行
};
此外,C++17 起可声明 inline
静态成员:
class Whatever
{
public:
static inline int s_value{ 4 }; // 类内定义并初始化
};
constexpr
静态成员自 C++17 起也隐式 inline
,可直接初始化:
#include <string_view>
class Whatever
{
public:
static constexpr double s_value{ 2.2 };
static constexpr std::string_view s_view{ "Hello" };
};
最佳实践
将静态成员声明为 inline
或 constexpr
,以便在类内完成定义与初始化。
静态成员变量示例:生成唯一 ID
#include <iostream>
class Something
{
private:
static inline int s_idGenerator{ 1 }; // 共享计数器
int m_id{};
public:
Something() : m_id{ s_idGenerator++ } {}
int getID() const { return m_id; }
};
int main()
{
Something first, second, third;
std::cout << first.getID() << '\n'; // 1
std::cout << second.getID() << '\n'; // 2
std::cout << third.getID() << '\n'; // 3
}
每个新对象获得递增的唯一 ID,便于调试区分。
静态成员的其他用途
- 共享查找表:如预计算数组。设为
static
后,所有对象共用一份,节省内存。 - 类型推导:
- 静态成员可使用
auto
或 CTAD 进行类型推导; - 非静态成员不允许,以避免潜在歧义。
- 静态成员可使用
示例:
#include <utility> // std::pair
class Foo
{
private:
// 非静态成员禁止 auto / CTAD
// auto m_x{ 5 };
// std::pair m_v{ 1, 2.3 };
// 静态成员允许
static inline auto s_x{ 5 };
static inline std::pair s_v{ 1, 2.3 };
};
小结
- 静态成员变量被所有对象共享,生命周期贯穿整个程序。
- 推荐通过
类名::成员
访问。 - 必须在类外(或
inline
/constexpr
内)显式定义,避免多重定义。 - 可用于唯一 ID、全局计数器、共享查找表等场景,并可利用
auto
/ CTAD 进行类型推导。