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

变量的作用域

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

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

变量的生命周期

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

  • 全局变量:进程开始时创建,进程结束时销毁,在代码编译链接后,直接将
    其初始值写入到可执行文件中,创建时按照定义时的初始值进
    行赋值
  • 局部变量和参数变量:进入函数时创建,退出函数时销毁
  • 全局静态变量:定义一个全局变量并使用static关键字修饰时,这个变量
    就成了全局静态变量,它的生命周期和全局变量一样,但是
    作用域被限制在定义文件内,无法使用extern来让其他源
    文件中使用它
  • 静态局部变量:在函数内使用static关键字修饰一个变量时,这个变量就
    是静态局部变量,它的生命周期同全局变量一样,作用域被
    限制在函数内
  • 寄存器变量:在VC++的Debug版本中,寄存器变量和普通变量没区别,在
    Release版本中VC++编译器会自动优化,即使一个变量不是
    寄存器变量也有可能放到寄存器中,所以register关键字对
    于VC++编译器来说只是个建议

各种变量和常量的小实验

  • 全局常量

    编写对全局常量赋值的代码会导致编译时报错,现在我用指针指向它的地址,
    然后在向它赋值,看看这种猥琐的方式是否能成功:

    可以看出编译时能混过去,但是运行时报错,这是因为全局常量保存在数据区
    的常量区中,常量区的内存属性为只读,如果向只读内存写入数据则会引发错误

  • 局部常量和参数常量

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

  • 全局常量,局部常量,参数常量,全局变量,全局静态变量,静态局部变量的生命周期:
    int g_Test1 = 3;
    const int g_Test2 = 4;
    static int g_Test3 = 5;
    void TestConstVar(const int nTest1)
    {static int nTest4 = 8;const int nTest = 1;int* pTest = (int*)&nTest;*pTest = 2;pTest = (int*)&nTest1;*pTest = 9;
    }int main()
    {TestConstVar(3);return 0;
    }
    

    在程序入口点mainCRTStartup下函数点,程序停在这里,此时程序刚刚
    建立,main函数还没有被执行:

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

    main函数退出后g_Test1,g_Test2,g_Test3依旧存在
    局部常量和参数常量在保存在栈上,但静态局部变量因为只做一次初始化的
    原因所以它也被保存在数据区,在实验的过程中发现了之前的VS2013以及之前
    的版本的编译器在初始化静态局部变量是线程不安全的,对比如下:

    • VS2013:

      从源码对应的汇编语言可以看出,VC++编译器为了做到静态局部变量
      只被初始化一次,所以使用了标记变量,只要发现标记变量没有被置位,
      那么会先进行置位,然后在进行初始化,但是这在多线程环境中是不安全的,
      当两个线程同时调用静态局部变量所在的函数时,会出现两个线程在没有同
      步机制的情况下操作同一个变量,在我这个简单代码中,静态局部变量的类型
      是整型,所以看起来没啥太大危害,但是如果静态局部变量的类型是一个类,
      那么构造函数极有可能发生一个线程,刚刚置标记位还没构造完成,接着另一个
      线程也调用了该函数,这个线程发现标记位被置位了,然而此时对象的构造还未
      完成,如果该线程就执行剩下的代码,那么极有可能发生错误,而且极难排查

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

      int g_Test1 = 3;
      const int g_Test2 = 4;
      static int g_Test3 = 5;
      void TestConstVar(const int nTest1)
      {static int nTest4 = nTest1;nTest4 += 1;const int nTest = 1;int* pTest = (int*)&nTest;*pTest = 2;pTest = (int*)&nTest1;*pTest = 9;
      }
      unsigned __stdcall startaddress(void *)
      {TestConstVar(3);printf("333");return 0;
      }int main()
      {TestConstVar(3);uintptr_t ret = _beginthreadex(NULL, 0, startaddress, NULL, 0, NULL);system("pause");return 0;
      }

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

      void TestConstVar(const int nTest1)
      {
      011F1760  push        ebp
      011F1761  mov         ebp,esp
      011F1763  sub         esp,0DCh
      011F1769  push        ebx
      011F176A  push        esi
      011F176B  push        edi
      011F176C  lea         edi,[ebp-0DCh]
      011F1772  mov         ecx,37h
      011F1777  mov         eax,0CCCCCCCCh
      011F177C  rep stos    dword ptr es:[edi]
      011F177E  mov         eax,dword ptr [__security_cookie (011FA014h)]
      011F1783  xor         eax,ebp
      011F1785  mov         dword ptr [ebp-4],eax  static int nTest4 = nTest1;
      011F1788  mov         eax,dword ptr [_tls_index (011FA194h)]
      011F178D  mov         ecx,dword ptr fs:[2Ch]
      011F1794  mov         edx,dword ptr [ecx+eax*4]
      011F1797  mov         eax,dword ptr ds:[011FA154h]
      011F179C  cmp         eax,dword ptr [edx+104h]
      011F17A2  jle         TestConstVar+6Fh (011F17CFh)
      011F17A4  push        11FA154h
      011F17A9  call        __Init_thread_header (011F104Bh)
      011F17AE  add         esp,4
      011F17B1  cmp         dword ptr ds:[11FA154h],0FFFFFFFFh
      011F17B8  jne         TestConstVar+6Fh (011F17CFh)
      011F17BA  mov         eax,dword ptr [nTest1]
      011F17BD  mov         dword ptr [nTest4 (011FA150h)],eax
      011F17C2  push        11FA154h
      011F17C7  call        __Init_thread_footer (011F10E1h)
      011F17CC  add         esp,4  nTest4 += 1;
      011F17CF  mov         eax,dword ptr [nTest4 (011FA150h)]
      011F17D4  add         eax,1
      011F17D7  mov         dword ptr [nTest4 (011FA150h)],eax  const int nTest = 1;
      011F17DC  mov         dword ptr [nTest],1  int* pTest = (int*)&nTest;
      011F17E3  lea         eax,[nTest]
      011F17E6  mov         dword ptr [pTest],eax  *pTest = 2;
      011F17E9  mov         eax,dword ptr [pTest]
      011F17EC  mov         dword ptr [eax],2  pTest = (int*)&nTest1;
      011F17F2  lea         eax,[nTest1]
      011F17F5  mov         dword ptr [pTest],eax  *pTest = 9;
      011F17F8  mov         eax,dword ptr [pTest]
      011F17FB  mov         dword ptr [eax],9
      }
      011F1801  push        edx
      011F1802  mov         ecx,ebp
      011F1804  push        eax
      011F1805  lea         edx,ds:[11F1830h]
      011F180B  call        @_RTC_CheckStackVars@8 (011F128Fh)
      011F1810  pop         eax
      011F1811  pop         edx
      011F1812  pop         edi
      }
      

      从上述反汇编代码中可以看出VS2015对静态变量的初始化与VS2013完全不一样,
      编译器插入了这两个函数:__Init_thread_header,__Init_thread_footer,
      从VS2015的安装目录下:VS2015\VC\crt\src\vcruntime的thread_safe_statics.cpp,
      源文件中找到了这两个函数的源码和这两个函数中引用到的变量:

      
      int const Uninitialized    = 0;
      int const BeingInitialized = -1;
      int const EpochStart = INT_MIN;extern "C"
      {int _Init_global_epoch = EpochStart;__declspec(thread) int _Init_thread_epoch = EpochStart;
      }extern "C" void __cdecl _Init_thread_header(int* const pOnce)
      {_Init_thread_lock();if (*pOnce == Uninitialized){*pOnce = BeingInitialized;}else{while (*pOnce == BeingInitialized){// Timeout can be replaced with an infinite wait when XP support is// removed or the XP-based condition variable is sophisticated enough// to guarantee all waiting threads will be woken when the variable is// signalled._Init_thread_wait(XpTimeout);if (*pOnce == Uninitialized){*pOnce = BeingInitialized;_Init_thread_unlock();return;} }_Init_thread_epoch = _Init_global_epoch;}_Init_thread_unlock();
      }// Called by the thread that completes initialization of a variable.
      // Increment the global and per thread counters, mark the variable as
      // initialized, and release waiting threads.
      extern "C" void __cdecl _Init_thread_footer(int* const pOnce)
      {_Init_thread_lock();++_Init_global_epoch;*pOnce = _Init_global_epoch;_Init_thread_epoch = _Init_global_epoch;_Init_thread_unlock();_Init_thread_notify();
      }extern "C" void __cdecl _Init_thread_lock()
      {EnterCriticalSection(&_Tss_mutex);
      }

      从反汇编代码中可以看出调用_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循环中
      等待当前正在初始化静态局部变量的线程完成初始化,那么现在来看看这些线程是如何等待的:

        static decltype(SleepConditionVariableCS)* encoded_sleep_condition_variable_cs;extern "C" bool __cdecl _Init_thread_wait(DWORD const timeout){if (_Tss_event == nullptr){return __crt_fast_decode_pointer(encoded_sleep_condition_variable_cs)(&_Tss_cv, &_Tss_mutex, timeout) != FALSE;}else{_ASSERT(timeout != INFINITE);_Init_thread_unlock();HRESULT res = WaitForSingleObjectEx(_Tss_event, timeout, FALSE);_Init_thread_lock();return (res == WAIT_OBJECT_0);}}
      

      _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:

          _Init_thread_lock();++_Init_global_epoch;*pOnce = _Init_global_epoch;_Init_thread_epoch = _Init_global_epoch;_Init_thread_unlock();_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:

      static decltype(WakeAllConditionVariable)* encoded_wake_all_condition_variable;
      extern "C" void __cdecl _Init_thread_notify()
      {if (_Tss_event == nullptr){__crt_fast_decode_pointer(encoded_wake_all_condition_variable)(&_Tss_cv);}else{SetEvent(_Tss_event);ResetEvent(_Tss_event);}
      }
      

      从上面代码可以看出在非XP系统下,调用WakeAllConditionVariable唤醒所有陷入睡眠的线程,
      在XP系统下使用SetEvent和ResetEvent唤醒等待线程,醒来的线程发现while循环中的条件
      *pOnce == BeingInitialized不成立,则退出_Init_thread_header函数,返回到TestConstVar
      函数,然后进行如下判断:

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

      发现与静态局部变量关联的标记变量已经不是BeingInitialized,则说明该静态局部变量已经被
      其他线程初始化了,则跳过静态局部变量的初始化代码。

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

      011F1788  mov         eax,dword ptr [_tls_index (011FA194h)]
      011F178D  mov         ecx,dword ptr fs:[2Ch]
      011F1794  mov         edx,dword ptr [ecx+eax*4]
      011F1797  mov         eax,dword ptr ds:[011FA154h]
      011F179C  cmp         eax,dword ptr [edx+104h]
      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语言变量作用域生命周期,C/C++——C++变量的作用域与生命周期,C语言中变量的作用域和生命周期...

    谭浩强书: 从存储模型可以看到,谭浩强和钱能的模型有一定的对应关系: 静态存储区  ->  全局数据区 动态存储区  ->  栈(stack) 变量的类型: 1. 局部变量和全局变量 局部 ...

  2. c语言变量生存期,C语言变量的生命周期

    变量的存储期是指程序运行过程中,变量在内存中的生存期,可以理解为变量的寿命.C语言中变量的存储期有自动存储期和静态存储期两种. 一般情况下,变量的存储期和作用域是紧密相关的.在函数外面定义的全局变量都 ...

  3. C++异常(异常的基本语法、栈解旋unwinding、异常接口声明、异常变量的生命周期、异常的多态使用、C++系统标准异常库)

    文章目录 1 异常的基本概念 1.1 C语言中的异常处理 1.2 C++中的异常处理 1.3 异常严格类型匹配 2 栈解旋(unwinding) 3 异常的接口声明[C++11已废弃] 4 异常变量的 ...

  4. Android静态变量的生命周期

    Android是用Java开发,其静态变量的生命周期遵守Java的设计.我们知道静态变量是在类被load的时候分配内存的,并且存在于方法区.当类 被卸载的时候,静态变量被销毁.在PC机的客户端程序中, ...

  5. Delphi匿名方法(三):扩展本地变量的生命周期

    本地变量,一般是随着函数执行结束,就不能再访问: 而如果在匿名函数,访问了外部函数的本地变量,本地变量的生命周期会被扩展 unit Unit1;interfaceusesWinapi.Windows, ...

  6. JVM详解之:汇编角度理解本地变量的生命周期

    文章目录 简介 本地变量的生命周期 举例说明 优化的原因 总结 简介 java方法中定义的变量,它的生命周期是什么样的呢?是不是一定要等到方法结束,这个创建的对象才会被回收呢? 带着这个问题我们来看一 ...

  7. C/C++构造及析构顺序及变量的生命周期

    (1)变量的构造及析构顺序 1)在全局范围内定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用.如果一个程序中有多个文件,而不同文件之间都定义了全 ...

  8. c++中的异常---2(异常接口声明,异常变量的生命周期,异常的多态使用)

    异常接口声明 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类的异常 如果 ...

  9. 【C语言简单说】十三:变量的生命周期

    这次我们就来说说生命周期的问题.其实声明周期的意思就是他这个变量的作用范围,啥是作用范围?唔...看我举例子吧,意会,意会... 首先,我想问一下你们,如果你们校长叫做小明,你们班也有一个小明.那么你 ...

  10. 语言const的生命周期_C语言的角落——这些C语言不常用的特性你知道吗?

    变长参数列表 头文件定义了一些宏,当函数参数未知时去获取函数的参数 变量:typedef va_list 宏: va_start() va_arg() va_end() va_list类型通过stda ...

最新文章

  1. 如何用JavaScript判断dom是否有存在某class的值?
  2. pythonfor输入多个数字_我一天学会了python最基础的编程
  3. 解决方案:Gateway实现全局跨域
  4. [转载] python json unicode utf-8处理总结
  5. c++sizeof求类大小 sizeof与strlen对比
  6. 对HTML5标签的认识(三)
  7. Perl语言入门,第17章自写习题答案。
  8. 自然语言处理(三) 语料库和语言知识库
  9. 希沃集控系统流媒体服务器未开启,希沃集控,让教育信息化管理尽在“掌控”之中...
  10. java地铁线路规划_地铁路线规划系统
  11. FFMpeg 常用命令格式转换,视频合成
  12. pcre c语言,pcre函数详细解析
  13. MFC中Combo的使用
  14. 网络安全入门(黑客)学习路线-2023最新版
  15. lin通讯从节点同步间隔场_LIN模块介绍
  16. 人工神经网络 :模糊神经网络
  17. c语言中关键字的含义,c语言中的关键字有哪些?有什么含义?
  18. WifiDisplay开启流程
  19. java对对碰游戏设计报告_手把手带你用Java打造一款对对碰游戏(下篇)
  20. Cookie 攻防世界

热门文章

  1. 用 Python 爬取网易严选妹子内衣信息,探究妹纸们的偏好
  2. 常用保险术语256条
  3. 单火开关和零火开关的区别
  4. 智慧环保监测系统提升环境监管水平
  5. 在线框架引用|bootstrap|jq|jqmobile|CSS框架_无需整理
  6. 先发新机再搞碰瓷,酷派复活就有胜算?
  7. 【零基础玩转BLDC系列】基于霍尔传感器的无刷直流电机控制原理
  8. windows下手动修改/修复启动项
  9. 如何申请免费163邮箱?如何申请163邮箱账号?
  10. 免费素材库:mixkit