CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++语言面试问题集锦 目录与说明
  • 第一章 auto与类型推导
  • 第二章 关键字static及其不同用法
    • 问题9:C++中的静态成员变量(static member variable)是什么意思?
    • 问题10:C++中的静态成员函数(static member function)是什么意思?
    • 问题11:什么是静态初始化顺序问题(static initialization order fiasco)?
    • 问题12:如何解决静态初始化顺序问题?
  • 第三章 多态、继承和虚函数
  • 第四章 Lambda函数
  • 第五章 C++中如何使用 const 限定符
  • 第六章 Modern C++的一些最佳实践
  • 第七章 智能指针
  • 第八章 引用、万能引用等
  • 第九章 C++20相关问题
  • 第十章 特殊函数及数量规则
  • 第十一章 C++面向对象设计
  • 第十二章 程序质量
  • 第十三章 标准模板库
  • 第十四章 杂项
  • cppinterviewmostaskedquestions
zhangxf
2025-03-27
目录

第二章 关键字static及其不同用法

# 第二章 关键字static及其不同用法

本章的重点是关键字static。

# 问题9:C++中的静态成员变量(static member variable)是什么意思?

声明为static的成员变量会在静态存储区分配存储空间,在程序的生命周期内仅分配一次。由于该变量对于所有对象只有一个副本,因此它也被称为类成员。

当我们在头文件中声明一个静态成员时,只是在告知编译器存在这样一个静态成员变量,但实际上并没有定义它(从某种意义上说,这与前置声明非常相似)。因为静态成员变量不属于类实例(它们的处理方式与全局变量类似,在程序启动时初始化),所以必须在类外部的全局作用域中显式定义静态成员。

class A {
    static MyType s_var;
};

MyType A::s_var = value;
1
2
3
4
5

不过也有一些例外情况。首先,当静态成员是const整型(包括char和bool)或const枚举类型时,静态成员可以在类定义内部初始化:

class A {
    static int  s_var{42};
};
1
2
3

从C++17开始,static constexpr成员可以在类定义内部初始化(无需在外部进行初始化)。

class A {
    // 对于任何支持constexpr初始化的类,这都有效
    static constexpr std::array<int , 3> s_array{ 1, 2, 3 };
};
1
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';
}
1
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
1
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';
}
1
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;
}
1
2
3
4

然后在Keyboard.cpp中,我们只需确保对该函数使用extern,并且之后调用这个函数,而不是引用变量。这样做是可行的,因为局部静态变量std::string aLogger会在theLogger()函数第一次被调用时初始化。因此,可以保证在构造theKeyboard时,theLogger已经被初始化。

如果在theLogger被构造之后,程序退出时另一个静态变量使用了theLogger,你可能会遇到其他问题。同样,对不同翻译单元中静态变量的依赖是代码质量不佳的表现……

从C++20开始,可以使用constinit来解决静态初始化顺序问题。在这种情况下,静态变量将在编译时、链接之前进行初始化。你可以在这里查看相关解决方案。

上次更新: 2025/03/27, 20:29:48
第一章 auto与类型推导
第三章 多态、继承和虚函数

← 第一章 auto与类型推导 第三章 多态、继承和虚函数→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第五章 C++中如何使用 const 限定符
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式