变量类型以及作用域和生命周期

变量的作用域

变量的作用域就该变量可以被访问的区间,变量的作用域可以分为以下四种:

  • 进程作用域(全局):在当前进程的任何一个位置都可以访问
  • 函数作用域:当流程转移到函数后,在其开始和结束的花括号内可访问
  • 块作用域:最常见的就是if(...){...},while(..){...},类似这种,

    块内部可以访问
  • 文件作用域:在当前源码文件内可以被访问

变量的生命周期

变量的生命周期就是从创建该变量开始到该变量被销毁的这一段时间,

各种变量的生命周期:

  • 全局变量:进程开始时创建,进程结束时销毁,在代码编译链接后,直接将

    其初始值写入到可执行文件中,创建时按照定义时的初始值进

    行赋值
  • 局部变量和参数变量:进入函数时创建,退出函数时销毁
  • 全局静态变量:定义一个全局变量并使用static关键字修饰时,这个变量

    就成了全局静态变量,它的生命周期和全局变量一样,但是

    作用域被限制在定义文件内,无法使用extern来让其他源

    文件中使用它
  • 静态局部变量:在函数内使用static关键字修饰一个变量时,这个变量就

    是静态局部变量,它的生命周期同全局变量一样,作用域被

    限制在函数内
  • 寄存器变量:在VC++的Debug版本中,寄存器变量和普通变量没区别,在

    Release版本中VC++编译器会自动优化,即使一个变量不是

    寄存器变量也有可能放到寄存器中,所以register关键字对

    于VC++编译器来说只是个建议

各种变量和常量的小实验

  • 全局常量



    编写对全局常量赋值的代码会导致编译时报错,现在我用指针指向它的地址,

    然后在向它赋值,看看这种猥琐的方式是否能成功:



    可以看出编译时能混过去,但是运行时报错,这是因为全局常量保存在数据区

    的常量区中,常量区的内存属性为只读,如果向只读内存写入数据则会引发错误
  • 局部常量和参数常量



    可以看出局部常量和参数常量都在栈上,只是在编译时检查是否被赋值,运行时

    还是可以猥琐修改
  • 全局常量,局部常量,参数常量,全局变量,全局静态变量,静态局部变量的生命周期:
    1. int g_Test1 = 3;
    2. const int g_Test2 = 4;
    3. static int g_Test3 = 5;
    4. void TestConstVar(const int nTest1)
    5. {
    6. static int nTest4 = 8;
    7. const int nTest = 1;
    8. int* pTest = (int*)&nTest;
    9. *pTest = 2;
    10. pTest = (int*)&nTest1;
    11. *pTest = 9;
    12. }
    13. int main()
    14. {
    15. TestConstVar(3);
    16. return 0;
    17. }

    在程序入口点mainCRTStartup下函数点,程序停在这里,此时程序刚刚

    建立,main函数还没有被执行:



    可以看出g_Test1,g_Test2,g_Test3都可以在"监视"窗口中查看



    main函数退出后g_Test1,g_Test2,g_Test3依旧存在

    局部常量和参数常量在保存在栈上,但静态局部变量因为只做一次初始化的

    原因所以它也被保存在数据区,在实验的过程中发现了之前的VS2013以及之前

    的版本的编译器在初始化静态局部变量是线程不安全的,对比如下:

    • VS2013:



      从源码对应的汇编语言可以看出,VC++编译器为了做到静态局部变量

      只被初始化一次,所以使用了标记变量,只要发现标记变量没有被置位,

      那么会先进行置位,然后在进行初始化,但是这在多线程环境中是不安全的,

      当两个线程同时调用静态局部变量所在的函数时,会出现两个线程在没有同

      步机制的情况下操作同一个变量,在我这个简单代码中,静态局部变量的类型

      是整型,所以看起来没啥太大危害,但是如果静态局部变量的类型是一个类,

      那么构造函数极有可能发生一个线程,刚刚置标记位还没构造完成,接着另一个

      线程也调用了该函数,这个线程发现标记位被置位了,然而此时对象的构造还未

      完成,如果该线程就执行剩下的代码,那么极有可能发生错误,而且极难排查

    • VS2015:

      我在测试程序中创建另一个线程,以便观察:

      1. int g_Test1 = 3;
      2. const int g_Test2 = 4;
      3. static int g_Test3 = 5;
      4. void TestConstVar(const int nTest1)
      5. {
      6. static int nTest4 = nTest1;
      7. nTest4 += 1;
      8. const int nTest = 1;
      9. int* pTest = (int*)&nTest;
      10. *pTest = 2;
      11. pTest = (int*)&nTest1;
      12. *pTest = 9;
      13. }
      14. unsigned __stdcall startaddress(void *)
      15. {
      16. TestConstVar(3);
      17. printf("333");
      18. return 0;
      19. }
      20. int main()
      21. {
      22. TestConstVar(3);
      23. uintptr_t ret = _beginthreadex(NULL, 0, startaddress, NULL, 0, NULL);
      24. system("pause");
      25. return 0;
      26. }

      TestConstVar函数完整的反汇编代码:

      1. void TestConstVar(const int nTest1)
      2. {
      3. 011F1760 push ebp
      4. 011F1761 mov ebp,esp
      5. 011F1763 sub esp,0DCh
      6. 011F1769 push ebx
      7. 011F176A push esi
      8. 011F176B push edi
      9. 011F176C lea edi,[ebp-0DCh]
      10. 011F1772 mov ecx,37h
      11. 011F1777 mov eax,0CCCCCCCCh
      12. 011F177C rep stos dword ptr es:[edi]
      13. 011F177E mov eax,dword ptr [__security_cookie (011FA014h)]
      14. 011F1783 xor eax,ebp
      15. 011F1785 mov dword ptr [ebp-4],eax
      16. static int nTest4 = nTest1;
      17. 011F1788 mov eax,dword ptr [_tls_index (011FA194h)]
      18. 011F178D mov ecx,dword ptr fs:[2Ch]
      19. 011F1794 mov edx,dword ptr [ecx+eax*4]
      20. 011F1797 mov eax,dword ptr ds:[011FA154h]
      21. 011F179C cmp eax,dword ptr [edx+104h]
      22. 011F17A2 jle TestConstVar+6Fh (011F17CFh)
      23. 011F17A4 push 11FA154h
      24. 011F17A9 call __Init_thread_header (011F104Bh)
      25. 011F17AE add esp,4
      26. 011F17B1 cmp dword ptr ds:[11FA154h],0FFFFFFFFh
      27. 011F17B8 jne TestConstVar+6Fh (011F17CFh)
      28. 011F17BA mov eax,dword ptr [nTest1]
      29. 011F17BD mov dword ptr [nTest4 (011FA150h)],eax
      30. 011F17C2 push 11FA154h
      31. 011F17C7 call __Init_thread_footer (011F10E1h)
      32. 011F17CC add esp,4
      33. nTest4 += 1;
      34. 011F17CF mov eax,dword ptr [nTest4 (011FA150h)]
      35. 011F17D4 add eax,1
      36. 011F17D7 mov dword ptr [nTest4 (011FA150h)],eax
      37. const int nTest = 1;
      38. 011F17DC mov dword ptr [nTest],1
      39. int* pTest = (int*)&nTest;
      40. 011F17E3 lea eax,[nTest]
      41. 011F17E6 mov dword ptr [pTest],eax
      42. *pTest = 2;
      43. 011F17E9 mov eax,dword ptr [pTest]
      44. 011F17EC mov dword ptr [eax],2
      45. pTest = (int*)&nTest1;
      46. 011F17F2 lea eax,[nTest1]
      47. 011F17F5 mov dword ptr [pTest],eax
      48. *pTest = 9;
      49. 011F17F8 mov eax,dword ptr [pTest]
      50. 011F17FB mov dword ptr [eax],9
      51. }
      52. 011F1801 push edx
      53. 011F1802 mov ecx,ebp
      54. 011F1804 push eax
      55. 011F1805 lea edx,ds:[11F1830h]
      56. 011F180B call @_RTC_CheckStackVars@8 (011F128Fh)
      57. 011F1810 pop eax
      58. 011F1811 pop edx
      59. 011F1812 pop edi
      60. }

      从上述反汇编代码中可以看出VS2015对静态变量的初始化与VS2013完全不一样,

      编译器插入了这两个函数:__Init_thread_header,__Init_thread_footer,

      从VS2015的安装目录下:VS2015\VC\crt\src\vcruntime的thread_safe_statics.cpp,

      源文件中找到了这两个函数的源码和这两个函数中引用到的变量:


      1. int const Uninitialized = 0;
      2. int const BeingInitialized = -1;
      3. int const EpochStart = INT_MIN;
      4. extern "C"
      5. {
      6. int _Init_global_epoch = EpochStart;
      7. __declspec(thread) int _Init_thread_epoch = EpochStart;
      8. }
      9. extern "C" void __cdecl _Init_thread_header(int* const pOnce)
      10. {
      11. _Init_thread_lock();
      12. if (*pOnce == Uninitialized)
      13. {
      14. *pOnce = BeingInitialized;
      15. }
      16. else
      17. {
      18. while (*pOnce == BeingInitialized)
      19. {
      20. // Timeout can be replaced with an infinite wait when XP support is
      21. // removed or the XP-based condition variable is sophisticated enough
      22. // to guarantee all waiting threads will be woken when the variable is
      23. // signalled.
      24. _Init_thread_wait(XpTimeout);
      25. if (*pOnce == Uninitialized)
      26. {
      27. *pOnce = BeingInitialized;
      28. _Init_thread_unlock();
      29. return;
      30. }
      31. }
      32. _Init_thread_epoch = _Init_global_epoch;
      33. }
      34. _Init_thread_unlock();
      35. }
      36. // Called by the thread that completes initialization of a variable.
      37. // Increment the global and per thread counters, mark the variable as
      38. // initialized, and release waiting threads.
      39. extern "C" void __cdecl _Init_thread_footer(int* const pOnce)
      40. {
      41. _Init_thread_lock();
      42. ++_Init_global_epoch;
      43. *pOnce = _Init_global_epoch;
      44. _Init_thread_epoch = _Init_global_epoch;
      45. _Init_thread_unlock();
      46. _Init_thread_notify();
      47. }
      48. extern "C" void __cdecl _Init_thread_lock()
      49. {
      50. EnterCriticalSection(&_Tss_mutex);
      51. }

      从反汇编代码中可以看出调用_Init_thread_footer,和_Init_thread_header时,前面都会有

      011F17C2 push 11FA154h,这行代码是将与静态变量关联的标记变量的地址作为参数

      传递,在_Init_thread_footer中先调用_Init_thread_lock函数进入临界区,确保在当前线程

      独占此标记变量,进入临界区后判断此标记变量的值是否为Uninitialized(值为0,表示静态局部

      变量未被初始化),如果标记变量为0,那么则将标记变量置为BeingInitialized(值为-1,表示该

      变量正在被初始化),然后当前线程调用_Init_thread_unlock函数释放临界区,退出_Init_thread_footer

      函数,流程转移到TestConstVar函数中进行静态局部变量的初始化,如果在此时紧接着又有好几个线程同

      时调用TestConstVar函数,假设此时静态局部变量还咩有初始化完成,那么后来的线程就会进入

      _Init_thread_header中,然后发现与该静态变量关联的标记变量已经被置为BeingInitialized

      那么这些线程则会进入到_Init_thread_header的else分支中,然后在else分支的while循环中

      等待当前正在初始化静态局部变量的线程完成初始化,那么现在来看看这些线程是如何等待的:

      1. static decltype(SleepConditionVariableCS)* encoded_sleep_condition_variable_cs;
      2. extern "C" bool __cdecl _Init_thread_wait(DWORD const timeout)
      3. {
      4. if (_Tss_event == nullptr)
      5. {
      6. return __crt_fast_decode_pointer(encoded_sleep_condition_variable_cs)(&_Tss_cv, &_Tss_mutex, timeout) != FALSE;
      7. }
      8. else
      9. {
      10. _ASSERT(timeout != INFINITE);
      11. _Init_thread_unlock();
      12. HRESULT res = WaitForSingleObjectEx(_Tss_event, timeout, FALSE);
      13. _Init_thread_lock();
      14. return (res == WAIT_OBJECT_0);
      15. }
      16. }

      _Tss_event只有在XP系统下才不为空,因为XP系统不支持条件变量,所以只能用WaitForSingleObjectEx

      来模拟条件变量,这里的encoded_sleep_condition_variable_cs是函数指针,这行代码:

      __crt_fast_decode_pointer(encoded_sleep_condition_variable_cs)(&_Tss_cv, &_Tss_mutex, timeout),就是在调用SleepConditionVariableCS,然后睡眠timeout(100ms),在睡眠的期间会释放

      _Tss_mutex,超时或者醒来时在重新进入临界区_Tss_mutex。

      当前线程初始化完成后会调用_Init_thread_footer:

      1. _Init_thread_lock();
      2. ++_Init_global_epoch;
      3. *pOnce = _Init_global_epoch;
      4. _Init_thread_epoch = _Init_global_epoch;
      5. _Init_thread_unlock();
      6. _Init_thread_notify();

      正是因为那些后来等待的线程调用SleepConditionVariableCS时会释放临界区,所以_Init_thread_footer

      中调用_Init_thread_lock()不会卡在这里,当前线程进入临界区后,那些在_Init_thread_wait

      中调用SleepConditionVariableCS函的线程将会卡在这个函数中,因为_Tss_mutex临界区被当前线程

      所占有;++_Init_global_epoch则是累加全局计数器,然后将全局计数器的值赋值给标记变量,而每个线程

      都有一个计数器(_Init_thread_epoch),全局计数器的值也被赋值给当前线程的计数器,至此标记变量和

      计数器都已赋值完成,此时在调用_Init_thread_unlock释放临界区,然后在调用_Init_thread_notify:

      1. static decltype(WakeAllConditionVariable)* encoded_wake_all_condition_variable;
      2. extern "C" void __cdecl _Init_thread_notify()
      3. {
      4. if (_Tss_event == nullptr)
      5. {
      6. __crt_fast_decode_pointer(encoded_wake_all_condition_variable)(&_Tss_cv);
      7. }
      8. else
      9. {
      10. SetEvent(_Tss_event);
      11. ResetEvent(_Tss_event);
      12. }
      13. }

      从上面代码可以看出在非XP系统下,调用WakeAllConditionVariable唤醒所有陷入睡眠的线程,

      在XP系统下使用SetEvent和ResetEvent唤醒等待线程,醒来的线程发现while循环中的条件

      *pOnce == BeingInitialized不成立,则退出_Init_thread_header函数,返回到TestConstVar

      函数,然后进行如下判断:

      1. 011F17B1 cmp dword ptr ds:[11FA154h],0FFFFFFFFh
      2. 011F17B8 jne TestConstVar+6Fh (011F17CFh)

      发现与静态局部变量关联的标记变量已经不是BeingInitialized,则说明该静态局部变量已经被

      其他线程初始化了,则跳过静态局部变量的初始化代码。

      现在回过头解释下反汇编中的第一个判断语句:

      1. 011F1788 mov eax,dword ptr [_tls_index (011FA194h)]
      2. 011F178D mov ecx,dword ptr fs:[2Ch]
      3. 011F1794 mov edx,dword ptr [ecx+eax*4]
      4. 011F1797 mov eax,dword ptr ds:[011FA154h]
      5. 011F179C cmp eax,dword ptr [edx+104h]
      6. 011F17A2 jle TestConstVar+6Fh (011F17CFh)

      这里前三行代码从局部线程存储中取出的一个值与静态局部变量对应的标记变量进行比较,

      根据_Init_thread_epoch变量的声明可以判断出取出的值就是_Init_thread_epoch,

      _Init_thread_epoch初值被置为EpochStart(一个负数),而标记变量未完成初始化时的

      值是0,比_Init_thread_epoch大,所以jle指令不满足跳转条件,后续的静态变量初始化

      代码得以执行;静态变量初始化完成后标记变量被置为_Init_thread_epoch(++_Init_global_epoch),

      所以标记变量时小于或者等于_Init_thread_epoch,jle指令跳转条件成立,静态局部的

      初始化代码全部跳过.

      At Last: 这个静态局部变量初始化bug经历了将近20年才被修复,我也是偶然间观察VS2013和

      VS2015生成的二进制代码的反汇编代码才发现这事,同时也顺带学会了条件变量的使用。

C语言变量及其生命周期的更多相关文章

  1. C++ 炼气期之变量的生命周期和作用域

    1. 前言 什么是变量的生命周期? 从变量被分配空间到空间被收回的这一个时间段,称为变量的生命周期. 什么是变量的作用域? 在变量的生命周期内,其存储的数据并不是在任何地方都能使用,变量能使用的范围, ...

  2. [转]Android静态变量的生命周期

    原文地址:https://my.oschina.net/jerikc/blog/137207 Android是用Java开发,其静态变量的生命周期遵守Java的设计.我们知道静态变量是在类被load的 ...

  3. 内存分配、C++变量的生命周期和作用域

    1.内存分配 程序的内存分配有以下几个区域:堆区.栈区.全局区.程序代码区,另外还有文字常量区. 栈区 ——存放局部变量,即由auto修饰的变量,一般auto省略.由编译器自动分配释放.局部变量定义在 ...

  4. C++临时变量的生命周期

    C++ 中的临时变量指的是那些由编译器根据需要在栈上产生的,没有名字的变量.主要的用途主要有两类: 1) 函数的返回值, 如: string proc() { return string(" ...

  5. 警惕rapidxml的陷阱:添加节点时,请保证变量的生命周期

    http://www.cnblogs.com/chutianyao/p/3246592.html 项目中要使用xml打包.解析协议,HQ指定了使用rapidxml--号称是最快的xml解析器. 功能很 ...

  6. android中少用静态变量(android静态变量static生命周期)

    在android中,要少用静态变量. 我现在做的一个应用中,之前的开发人员使用静态变量来存储cookie,这个全局的静态变量用来验证身份. 这时客户反应,应用长时间不使用,再次使用,会提示身份过期. ...

  7. [转] C++临时变量的生命周期

    http://www.cnblogs.com/catch/p/3251937.html C++中的临时变量指的是那些由编译器根据需要在栈上产生的,没有名字的变量. 主要的用途主要有两类: 1) 函数的 ...

  8. 【转载】ASP.NET线程安全与静态变量的生命周期浅谈

    ASP.NET线程安全所涉及的是什么呢?让我们先来看看静态变量的生命周期问题,下面是我理解的静态变量的生命周期: void Application_Start开始 void Application_E ...

  9. 配置Session变量的生命周期

    在Web.config文件中配置Session变量的生命周期是在<sessionState></sessionState>节中完成的,在配置Session的生命周期时,可以设置 ...

随机推荐

  1. [USACO15JAN]Moovie Mooving G

    [USACO15JAN]Moovie Mooving G 状压难题.不过也好理解. 首先我们根据题意: she does not want to ever visit the same movie t ...

  2. zhihu level

    zhihu level https://www.zhihu.com/creator/account/growth-level refs xgqfrms 2012-2020 www.cnblogs.co ...

  3. Apple CSS Animation All In One

    Apple CSS Animation All In One Apple Watch CSS Animation https://codepen.io/xgqfrms/pen/LYZaNMb See ...

  4. Regular Expression & rgb2hex

    Regular Expression & rgb2hex regex // 颜色字符串转换 function rgb2hex(sRGB = 'rgb(255, 255, 255)') { co ...

  5. Beacon API All In One

    Beacon API All In One Beacon API https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API https:/ ...

  6. SSL (Secure Sockets Layer)

    本文转载自SSL (Secure Sockets Layer) TLS简介 The Transport Layer Security (TLS) protocol aims primarily to ...

  7. Vue学习笔记-Vue.js-2.X 学习(一)===>基本知识学习

    一  使用环境: windows 7 64位操作系统 二  IDE:VSCode/PyCharm 三  Vue.js官网: https://cn.vuejs.org/ 四  下载安装引用 方式1:直接 ...

  8. 开工大吉:TcaplusDB将持续为您提供可靠的数据服务

    开工大吉 新的一年 新的开始 我们也带着新的心意 向您奔赴而来    在此,TcaplusDB祝广大客户朋友,开工大吉,2021,我们将一如既往地守护您的数据,继续做您最坚实的后盾.    作为专为游 ...

  9. C#语言特性及发展史

    本文按照C#语言的发展历史,介绍C#每个版本的新增特性,主要参考微软官方文档.了解这些语言特性可以帮助我们更高效的编写C#代码. C# 1.0 与Visual Studio .NET 2002一起发布 ...

  10. Centos7.7下安装Python3.7 并兼容python2.7

    前言 1.首先来看一下系统版本 [root@python3 ~]# cat /etc/redhat-release CentOS Linux release 7.7.1810 (Core) 2.更新一 ...