13.13 - 静态成员变量
重温 static 关键字
在 6.10 - 静态局部变量 中我们学习了 static
关键字,它可以定义一个变量并确保它不会在离开作用域后被销毁,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
打印结果:
1 2 3 |
|
注意,s_id
的值在多次函数调用间得以保留。
static
关键字在用于全局变量时,还有另外的含义—— 它会赋予该变量内部链接属性(不能在定义它们的文件外使用)。因为全局变量应该被杜绝,所以static
的这方面应用并不常见。
静态成员变量
static
关键字在应用于类成员时,有两个额外的用途:静态成员变量和静态成员函数。不过,这两种static
用法都非常简单直接。我们会在本章介绍静态成员变量,然后在下一章介绍静态成员函数。
在开始学习为成员变量添加static
关键字之前,请先考虑下面的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
当我们实例化对象的时候,对象各自创建其成员函数的一份拷贝。在这个例子中,因为我们创建了两个 Something
类型的对象,最终我们会得到两份 m_value
: first.m_value
和second.m_value
。它们两个是各自独立的,因此上述程序的输出结果为:
1 2 |
|
使用 static
关键字可以创建静态成员变量。和普通成员变量不同的是,静态成员变量在同一个类的对象间是共享的。考虑下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
输出结果如下:
1 2 |
|
因为 s_value
是静态成员变量,所以 s_value
在各个对象间是共享的。其结果就是,first.s_value
is 与 second.s_value
实际上是同一个。因此在上面的代码中我们可以通过first
设置s_value
的值,并通过second
访问它。
静态成员变量并不和类对象关联
尽管你可以通过对象来访问静态成员(例如:first.s_value
和 second.s_value
),但实际上这些静态成员在对象被实例化前就存在了。它们更像是全局变量,会在程序启动时创建,在程序退出时销毁。
因此,最好认为静态成员是属于类本身的,而不是类的某个实例对象。因为 s_value
独立于任何类对象而存在,所以可以直接使用类名和作用域解析运算符(在本例中为Something::s_value
)访问它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
在上面的例子中,s_value
是使用类名进行访问的,而没有通过任何该类的变量去访问。我们甚至都还没有实例化任何该类的对象,但是仍然可以通过Something::s_value
来访问它。这种方式是更为推荐的使用静态成员的方法。
最佳实践
通过类名(和作用域解析运算符)来访问静态成员变量而不是通过对象来访问(使用成员选择运算符)。
定义和初始化静态成员变量
当我们在类内部声明静态成员变量时,我们是在告诉编译器静态成员变量的存在,而不是真正定义它(类似前向声明)。因为静态成员变量不是单个类对象的一部分(它们与全局变量处理相似,并在程序启动时初始化),所以必须在类外部的全局作用域中显式地定义静态成员。
在上面的例子中,我们通过这一行来实现静态成员的定义
1 |
|
这一行有两个目的:实例化静态成员变量(就像全局变量一样),并可选地初始化它。在本例中,我们提供了初始化值1。如果没有提供初始化值,C++将该值初始化为0。
注意,静态成员的定义不受成员访问修饰符的限制,您可以定义和初始化变量,即使它在类中声明为private(或protected)。
如果类定义在头文件中,静态成员定义通常放在类的相关代码文件中(例如Something.cpp
)。如果类定义在.cpp
文件中,则静态成员定义通常直接放在类的下面。不要将静态成员定义放在头文件中(很像全局变量,如果头文件被包含不止一次,最终将得到多个定义,这将导致链接器错误)。
静态成员变量的内联初始化
有一些捷径可以实现上述目标。首先,当静态成员是const整型类型(包括char和bool)或const enum时,可以在类定义中初始化静态成员:
1 2 3 4 5 |
|
在上面的例子中,因为静态成员变量是 const 的,所以不需要显式地定义它。
其次,static constexpr
成员可以在类定义中初始化:
1 2 3 4 5 6 7 8 |
|
最后,对于 C++17 来说,非 const 静态成员也可以在类定义中内联地初始化:
1 2 3 4 5 |
|
静态成员变量案例
为什么我们要在类中使用静态变量呢?一个比较有用的例子是为类的每个实例设置一个唯一的ID,请看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
程序输出结果:
1 2 3 |
|
因为 s_idGenerator
由所有 Something
对象共享,所以当创建一个新的 Something
对象时,构造函数从 s_idGenerator
中获取当前值,将其递增后作为下一个对象的ID。这保证了每个实例化的 Something
对象可以获取唯一的id(按创建顺序递增)。这在调试数组中的多个项时非常有帮助,因为它提供了一种方法来区分具有相同类类型的多个对象!
当类需要创建类内部的查找表(例如,用于存储一组预计算值的数组)时,静态成员变量也很有用。通过将查找表设置为静态的,所有对象只存在一个副本,而不是为每个实例化的对象都创建一个副本。这可以节省大量的内存。