第二章 关键字static及其不同用法
# 第二章 关键字static及其不同用法
本章的重点是关键字static
。
# 问题9:C++中的静态成员变量(static member variable)是什么意思?
声明为static
的成员变量会在静态存储区分配存储空间,在程序的生命周期内仅分配一次。由于该变量对于所有对象只有一个副本,因此它也被称为类成员。
当我们在头文件中声明一个静态成员时,只是在告知编译器存在这样一个静态成员变量,但实际上并没有定义它(从某种意义上说,这与前置声明非常相似)。因为静态成员变量不属于类实例(它们的处理方式与全局变量类似,在程序启动时初始化),所以必须在类外部的全局作用域中显式定义静态成员。
class A {
static MyType s_var;
};
MyType A::s_var = value;
2
3
4
5
不过也有一些例外情况。首先,当静态成员是const
整型(包括char
和bool
)或const
枚举类型时,静态成员可以在类定义内部初始化:
class A {
static int s_var{42};
};
2
3
从C++17开始,static constexpr
成员可以在类定义内部初始化(无需在外部进行初始化)。
class A {
// 对于任何支持constexpr初始化的类,这都有效
static constexpr std::array<int , 3> s_array{ 1, 2, 3 };
};
2
3
4
如果在成员函数中调用静态数据成员,那么该成员函数应声明为static
。
前面已经提到过,但值得再次强调的是,静态成员变量在程序启动时创建,在程序结束时销毁,因此即使没有实例化类的任何对象,静态成员依然存在。
# 问题10:C++中的静态成员函数(static member function)是什么意思?
静态成员函数可用于处理类中的静态成员变量,或执行无需类实例的操作。不过,从概念和语义上讲,静态成员函数所执行的操作应与类密切相关。关于静态成员函数,有以下几个要点:
- 静态成员函数没有
this
指针。 - 静态成员函数不能是虚函数(virtual)。
- 静态成员函数不能访问非静态成员。
- 静态成员函数不能使用
const
和volatile
限定符。
在实际应用中,静态成员函数意味着无需实例化类就可以调用它。如果Foo
类中有一个静态函数static void bar()
,你可以这样调用它:Foo::bar()
,而无需实例化Foo
类(顺便说一句,你也可以通过实例调用它:aFoo.bar()
)。
由于this
指针始终保存当前对象的内存地址,而调用静态成员根本不需要对象,所以静态成员函数不能有this
指针。
虚成员与任何特定类没有直接关系,只与实例相关。(根据定义)“虚函数” 是一种动态链接的函数,即在运行时根据给定对象的动态类型选择正确的实现。因此,如果没有对象,就不可能进行虚函数调用。
访问非静态成员函数要求对象已经构造,但对于静态调用,我们不会传递类的任何实例,甚至不能保证已经构造了任何实例。
同样,const
和const volatile
关键字用于修改对象是否可被修改以及如何被修改。由于没有对象……
# 问题11:什么是静态初始化顺序问题(static initialization order fiasco)?
静态初始化顺序问题是C++中一个容易被许多人忽视、误解的微妙方面。由于该错误通常在main()
函数被调用之前就发生了,所以很难察觉。
在一个翻译单元(translation unit)中,静态或全局变量总是按照它们的定义顺序进行初始化。另一方面,对于哪个翻译单元先被初始化,并没有严格的顺序。
假设你有一个翻译单元A
,其中有一个静态变量sA
,它的初始化依赖于翻译单元B
中的静态变量sB
。那么你有50%的失败几率。这就是静态初始化顺序问题。
让我们来看一个相关示例。
// Logger.cpp
#include <string>
std::string theLogger = "aNiceLogger";
// KeyBoard.cpp
#include <iostream>
#include <string>
extern std::string theLogger;
std::string theKeyboard = "The Keyboard with logger: " + theLogger;
int main() {
std::cout << "theKeyboard: " << theKeyboard << '\n';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在这个例子中,我们有一个键盘(驱动)和一个日志记录器。假设键盘驱动需要一个日志记录器,以便在出现错误时进行记录。由于某些原因,我们决定只使用一个键盘和一个日志记录器,所以将它们设为全局变量。显然,这不是一个好的设计决策,但这是一个很常见的场景,并且也是为了便于举例说明。
如果键盘的初始化早于日志记录器,就会出现问题,正如你在下面的示例中看到的:
zhangxf@mydevserver ~/static_fiasco $ g++ -c Logger.cpp
zhangxf@mydevserver ~/static_fiasco $ g++ -c Keyboard.cpp
zhangxf@mydevserver ~/static_fiasco $ g++ Logger.o Keyboard.o -o LoggerThenKeyboard
zhangxf@mydevserver ~/static_fiasco $ g++ Keyboard.o Logger.o -o KeyboardThenLogger
zhangxf@mydevserver ~/static_fiasco $ ./KeyboardThenLogger
theKeyboard: The Keyboard with logger:
zhangxf@mydevserver ~/static_fiasco $ ./LoggerThenKeyboard
theKeyboard: The Keyboard with logger: aNiceLogger
2
3
4
5
6
7
8
这就是静态初始化顺序问题在实际中的体现。
请注意,对不同翻译单元中静态变量的依赖是代码质量不佳的表现,实际上,这应该是进行重构的一个充分理由。
# 问题12:如何解决静态初始化顺序问题?
需要再次提醒的是,在一个翻译单元中,静态或全局变量总是按照它们的定义顺序进行初始化。另一方面,对于哪个翻译单元先被初始化,并没有严格的顺序。
如果翻译单元A
中有一个静态变量sA
,它的初始化依赖于翻译单元B
中的静态变量sB
,那么就有50%的失败几率。这就是静态初始化顺序问题。
对不同翻译单元中静态变量的依赖是代码质量不佳的表现,实际上,这应该是进行重构的一个充分理由。因此,解决这个问题最直接的方法就是消除这种依赖。
这里回顾一下我们之前的示例。
// Logger.cpp
#include <string>
std::string theLogger = "aNiceLogger";
// KeyBoard.cpp
#include <iostream>
#include <string>
extern std::string theLogger;
std::string theKeyboard = "The Keyboard with logger: " + theLogger;
int main() {
std::cout << "theKeyboard: " << theKeyboard << '\n';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这个示例有50%的失败可能性。如果编译单元Logger.cpp
先被初始化,那么没问题。但如果是键盘(对应的编译单元)先被初始化,就会出问题。
可能最简单的解决方案是将Logger.cpp
中的theLogger
变量替换为一个函数,如下所示:
std::string theLogger() {
static std::string aLogger = "aNiceLogger";
return aLogger;
}
2
3
4
然后在Keyboard.cpp
中,我们只需确保对该函数使用extern
,并且之后调用这个函数,而不是引用变量。这样做是可行的,因为局部静态变量std::string aLogger
会在theLogger()
函数第一次被调用时初始化。因此,可以保证在构造theKeyboard
时,theLogger
已经被初始化。
如果在theLogger
被构造之后,程序退出时另一个静态变量使用了theLogger
,你可能会遇到其他问题。同样,对不同翻译单元中静态变量的依赖是代码质量不佳的表现……
从C++20开始,可以使用constinit
来解决静态初始化顺序问题。在这种情况下,静态变量将在编译时、链接之前进行初始化。你可以在这里查看相关解决方案。