天天看點

C語言變量及其生命周期

變量類型以及作用域和生命周期

變量的作用域

變量的作用域就該變量可以被通路的區間,變量的作用域可以分為以下四種:

  • 程序作用域(全局):在目前程序的任何一個位置都可以通路
  • 函數作用域:當流程轉移到函數後,在其開始和結束的花括号内可通路
  • 塊作用域:最常見的就是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都可以在"監視"視窗中檢視

    C語言變量及其生命周期

    main函數退出後g_Test1,g_Test2,g_Test3依舊存在

    局部常量和參數常量在儲存在棧上,但靜态局部變量因為隻做一次初始化的

    原因是以它也被儲存在資料區,在實驗的過程中發現了之前的VS2013以及之前

    的版本的編譯器在初始化靜态局部變量是線程不安全的,對比如下:

    • VS2013:
      C語言變量及其生命周期

      從源碼對應的彙編語言可以看出,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生成的二進制代碼的反彙編代碼才發現這事,同時也順帶學會了條件變量的使用。