当前位置: 首页 > news >正文

【C++】全局变量/静态变量的初始化时机

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 一、全局变量下断点调试
    • 1. `int a = 10;` —— 不能卡住断点
    • 2. `static int b;` —— 不能卡住断点
    • 3. `MyClass c;` —— 可以卡住断点
    • 4. `static MyClass d;` —— 可以卡住断点
    • 总结
  • 二、静态变量初始化时机
    • 1. 局部静态变量
    • 2. 与全局变量对比
      • 1. 全局变量的初始化时机
        • (1)静态初始化阶段(编译期/加载期)
        • (2)动态初始化阶段(运行期,main函数执行前)
      • 2. 局部静态变量的初始化时机
        • (1)初始化触发条件
        • (2)初始化阶段(运行期,函数首次调用时)
      • 3. 关键差异对比
      • 4. 示例验证
      • 总结
    • 3.同为静态存储区为什么初始化时机不同
  • 后续


一、全局变量下断点调试

现有如下代码

#include <iostream>int a = 10;
static int b;
MyClass c;
static MyClass d;int main() 
{return 0;
}

首先,全局变量的初始化分为零初始化和动态初始化。对于基本类型,比如 int a=10,这是常量初始化,属于静态初始化的一部分,通常在编译时就完成,可能不会生成对应的执行代码,所以调试器可能无法在定义处下断点,因为那里没有可执行的指令。

然后,static int b,全局静态变量,默认初始化(零初始化),也是在程序加载时完成,同样可能没有对应的执行代码,所以断点可能无效。

接下来是自定义类的全局对象 c 和 d。类对象的初始化需要执行构造函数,这属于动态初始化,发生在程序启动阶段(main 函数之前),由运行时库调用构造函数。这时候会有对应的执行代码(调用构造函数的指令),所以在定义处下断点,调试器可能能捕捉到这个时机,因为构造函数的调用是有执行步骤的。

不过需要注意,调试器能否在全局变量定义处下断点,取决于是否有对应的代码位置。对于基本类型的初始化,因为是编译期或加载期完成,没有指令执行,所以断点无效。而类对象因为需要执行构造函数,会有代码执行,所以断点可能有效。

另外,静态全局变量(b 和 d)和非静态全局变量(a 和 c)的区别主要在作用域(文件内还是全局),但初始化时机类似,基本类型的静态初始化没有执行代码,类对象的动态初始化有执行代码。

所以总结:int a 和 static int b 是基本类型,初始化在编译或加载时,无执行代码,断点不能卡住;MyClass c 和 static MyClass d 需要执行构造函数,有执行代码,断点可以卡住。

在全局变量定义处设置断点能否“卡住”调试,取决于变量初始化过程是否会生成可被调试器捕获的执行代码。具体分析如下:

1. int a = 10; —— 不能卡住断点

  • 原因
    基本类型全局变量的初始化(如int a=10)属于静态初始化(编译期即可确定初始值),由编译器在编译阶段直接将初始值写入数据段,程序加载时直接从二进制文件中读取该值,不会生成单独的执行代码(没有指令来“执行”这个初始化)。
    调试器需要有对应的机器指令才能设置断点,而这里没有可执行的代码,因此断点无效。

2. static int b; —— 不能卡住断点

  • 原因
    全局静态变量(static修饰的全局变量)的初始化规则与普通全局变量一致。未显式初始化的基本类型(如int b)会进行零初始化(值为0),同样在编译期/程序加载阶段完成,无执行代码
    调试器无法在没有指令的位置断住,因此断点无效。

3. MyClass c; —— 可以卡住断点

  • 原因
    自定义类的全局对象初始化需要执行构造函数,这属于动态初始化(运行时执行代码)。程序启动阶段(main函数之前),运行时库会自动调用MyClass的构造函数来初始化c,这个过程会生成对应的执行代码(调用构造函数的指令)。
    调试器可以在构造函数执行的对应位置断住,因此断点有效。

4. static MyClass d; —— 可以卡住断点

  • 原因
    全局静态类对象(static MyClass d)的初始化逻辑与普通全局类对象一致:需要执行构造函数,属于动态初始化,在main函数之前由运行时库触发,会生成执行代码
    虽然static限定了其作用域(仅当前文件可见),但初始化过程仍有可被调试器捕获的指令,因此断点有效。

总结

  • 不能卡住断点int a = 10;static int b;(基本类型,静态初始化,无执行代码)。
  • 可以卡住断点MyClass c;static MyClass d;(类对象,动态初始化,需执行构造函数,有可调试的执行代码)。

核心区别在于:基本类型的全局变量初始化在编译/加载阶段完成,无执行代码;而类对象的全局变量需要运行时执行构造函数,存在可被调试器捕获的代码指令。

二、静态变量初始化时机

1. 局部静态变量

在C++中,局部静态变量(包括自定义类的静态局部对象)的初始化时机和特性如下:

  1. 初始化时机
    局部静态变量在函数第一次被调用时进行初始化,且仅初始化一次。
    对于自定义类的静态局部对象,其构造函数会在这个时机执行。

  2. 未调用函数的情况
    如果从未调用该函数,其中的静态局部对象不会被初始化,其构造函数也不会执行

  3. 生命周期
    一旦初始化完成,局部静态变量的生命周期会延续到整个程序结束(与全局变量类似),在main函数退出后才会执行析构函数(如果是类对象)。

示例代码说明:

#include <iostream>class MyClass {
public:MyClass() {std::cout << "MyClass构造函数执行" << std::endl;}~MyClass() {std::cout << "MyClass析构函数执行" << std::endl;}
};void func() {static MyClass obj; // 静态局部对象std::cout << "func()被调用" << std::endl;
}int main() {std::cout << "main()开始" << std::endl;func(); // 第一次调用func(),触发obj的构造func(); // 第二次调用,不会再次构造std::cout << "main()结束" << std::endl;return 0;
}

输出结果:

main()开始
MyClass构造函数执行
func()被调用
func()被调用
main()结束
MyClass析构函数执行

如果注释掉main()中对func()的所有调用,那么obj的构造函数永远不会执行。

这种特性使得局部静态变量非常适合实现"懒汉式"单例模式,确保对象在第一次使用时才被创建。

2. 与全局变量对比

在C++中,全局变量和局部静态变量虽然都存储在静态存储区(生命周期贯穿整个程序),但它们的初始化时机存在显著差异,主要体现在初始化的触发条件和具体阶段上。以下是详细说明:

1. 全局变量的初始化时机

全局变量(包括全局作用域的变量、命名空间作用域的变量、类的静态成员变量)的初始化发生在程序执行之前,具体分为两个阶段:

(1)静态初始化阶段(编译期/加载期)
  • 对于内置类型(如intdouble)或常量表达式初始化的变量,编译器在编译期即可确定初始值,直接将值写入目标文件的静态存储区。
    例如:int g_a = 10;(常量初始化)、int g_b;(零初始化,默认值为0)。
(2)动态初始化阶段(运行期,main函数执行前)
  • 对于需要运行时计算初始值的全局变量(如自定义类的对象、依赖其他变量的表达式),初始化发生在main函数执行之前,由程序的启动代码(CRT初始化代码)触发。
    例如:
    class MyClass { public: MyClass() { /* 构造函数 */ } };
    MyClass g_obj; // 动态初始化,构造函数在main前执行
    int g_c = g_a + 5; // 依赖其他变量,动态初始化
    


int g_c = g_a + 5;
这句话下断点是可以卡住的

核心特点

  • 全局变量的初始化不依赖任何函数调用,无论是否被使用,都会在程序启动阶段完成初始化。
  • 所有全局变量的初始化在main函数执行前全部完成。

2. 局部静态变量的初始化时机

局部静态变量(函数内部定义的static变量)的初始化时机与全局变量完全不同:

(1)初始化触发条件

局部静态变量的初始化仅在函数第一次被调用时触发,且只初始化一次。
例如:

void func() {static MyClass obj; // 仅在func第一次被调用时初始化static int num = 100; // 仅在func第一次被调用时赋值
}
(2)初始化阶段(运行期,函数首次调用时)
  • 对于内置类型,第一次进入函数时执行初始化(如static int num = 100;)。
  • 对于自定义类的静态局部对象,其构造函数在函数第一次被调用时执行
  • 如果函数从未被调用,局部静态变量不会被初始化(构造函数也不会执行)。

核心特点

  • 初始化依赖函数的首次调用,是“按需初始化”。
  • C++11及以后标准保证局部静态变量的初始化是线程安全的(多线程下首次调用不会导致重复初始化)。

3. 关键差异对比

特性全局变量局部静态变量
初始化触发条件程序启动时(main前),不依赖函数调用函数第一次被调用时,依赖函数调用
初始化次数仅一次(程序启动阶段)仅一次(首次调用时)
未使用时是否初始化会初始化(无论是否被使用)不会初始化(函数未调用则不初始化)
构造函数执行时机main函数执行前函数第一次被调用时

4. 示例验证

#include <iostream>class Test {
public:Test(const char* name) : m_name(name) {std::cout << m_name << " 构造函数执行" << std::endl;}~Test() {std::cout << m_name << " 析构函数执行" << std::endl;}
private:const char* m_name;
};// 全局变量
Test g_test("全局变量");void func() {// 局部静态变量static Test l_test("局部静态变量");std::cout << "func() 被调用" << std::endl;
}int main() {std::cout << "main() 开始" << std::endl;func(); // 首次调用func,触发局部静态变量初始化func(); // 再次调用,不触发初始化std::cout << "main() 结束" << std::endl;return 0;
}

输出结果

全局变量 构造函数执行  // 全局变量在main前初始化
main() 开始
局部静态变量 构造函数执行  // 首次调用func时初始化
func() 被调用
func() 被调用
main() 结束
局部静态变量 析构函数执行  // 程序结束时销毁
全局变量 析构函数执行      // 程序结束时销毁

结论

  • 全局变量的构造函数在main开始前执行,局部静态变量的构造函数在函数首次调用时执行。
  • 若注释掉main中对func()的调用,局部静态变量的构造函数不会执行,而全局变量的构造函数仍会执行。

总结

全局变量和局部静态变量的核心差异在于初始化是否依赖函数调用

  • 全局变量是“主动初始化”,程序启动即完成,与是否被使用无关;
  • 局部静态变量是“被动初始化”,仅在函数首次调用时触发,实现了“懒加载”特性,这也是其常用于单例模式的原因。

3.同为静态存储区为什么初始化时机不同

静态变量存放在静态存储区,程序执行前会初始化,但局部静态变量为什么没有在执行前初始化,而全局变量会先初始化。首先要明确静态存储区的概念,确实全局变量、静态全局变量、局部静态变量都存放在静态存储区,但它们的初始化时机不同。

首先,全局变量(包括静态全局变量)属于静态存储期,它们的初始化发生在程序启动的静态初始化阶段(在 main 函数执行之前)。对于自定义类的全局对象,其构造函数会在这个阶段执行,也就是 main 之前。

而局部静态变量虽然也存放在静态存储区,但其初始化属于延迟初始化(lazy initialization),这是 C++ 标准明确规定的:局部静态变量在函数第一次被调用时才会初始化,而非程序启动时。这是为了避免不必要的初始化开销 —— 如果函数从未被调用,局部静态变量就不会占用初始化资源。

总结核心区别:

存储区域:全局变量和局部静态变量都在静态存储区(生命周期都是程序整个运行期间)。
初始化时机:
全局变量:在 main 函数执行前的静态初始化阶段完成(构造函数此时调用)。
局部静态变量:在函数第一次被调用时才初始化(构造函数此时调用)。
未使用情况:
全局变量:无论是否被使用,都会在程序启动时初始化(构造函数一定会执行)。
局部静态变量:如果函数从未被调用,不会初始化(构造函数永不执行)。

虽然同属静态存储区,但全局变量和局部静态变量的初始化时机存在差异 ——C++ 标准对它们的初始化策略做了不同设计,以平衡效率和确定性。


后续

全局变量(包括全局作用域的变量、命名空间作用域的变量、类的静态成员变量)的初始化发生在程序执行之前

    1. 看下类的静态变量与全局变量性质是否一直
    1. 全局变量与静态全局变量的区别,即static对全局变量作用域影响详解
http://www.dtcms.com/a/347835.html

相关文章:

  • 基于电力电子变压器的高压脉冲电源方案复现
  • 最小覆盖子串+滑动窗口
  • 【JVM内存结构系列】二、线程私有区域详解:程序计数器、虚拟机栈、本地方法栈——搞懂栈溢出与线程隔离
  • mysql为什么使用b+树不使用红黑树
  • tcpdump命令打印抓包信息
  • 用vscode使用git工具
  • 深度优先搜索(DFS)和广度优先搜索(BFS)
  • 【内网渗透】Relay2LDAP之NTLMKerberos两种利用
  • windows中bat脚本的一些操作(三)
  • 如和在不同目录之间引用模块-python
  • 微调系列:LoRA原理
  • MVC模式在个人博客系统中的应用
  • 【通俗易懂】TypeScript 增加了 JavaScript 的可选链 (?.) 和空值合并运算符 (??)理解
  • 【集合和映射】USACO Bronze 2019 December - 我在哪Where Am I?
  • 机器学习案例——预测矿物类型(模型训练)
  • DS18B20温度传感器详解
  • 电阻的功率
  • 多光谱相机检测石油石化行业的跑冒滴漏的可行性分析
  • 电蚊拍的原理及电压电容参数深度解析:从高频振荡到倍压整流的完整技术剖析
  • 决策树基础学习教育第二课:量化最优分裂——信息熵与基尼系数
  • 01_Python的in运算符判断列表等是否包含特定元素
  • [Vid-LLM] 创建和训练Vid-LLMs的各种方法体系
  • crypto.randomUUID is not a function
  • 一个备份、去除、新增k8s的node标签脚本
  • Redis(八股二弹)
  • 玳瑁的嵌入式日记D24-0823(数据结构)
  • 每日一题8.23
  • Day26 树的层序遍历 哈希表 排序算法 内核链表
  • 线程池理解
  • CMake安装教程