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都可以在"监视"窗口中查看
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语言变量及其生命周期相关推荐
- c语言变量作用域生命周期,C/C++——C++变量的作用域与生命周期,C语言中变量的作用域和生命周期...
谭浩强书: 从存储模型可以看到,谭浩强和钱能的模型有一定的对应关系: 静态存储区 -> 全局数据区 动态存储区 -> 栈(stack) 变量的类型: 1. 局部变量和全局变量 局部 ...
- c语言变量生存期,C语言变量的生命周期
变量的存储期是指程序运行过程中,变量在内存中的生存期,可以理解为变量的寿命.C语言中变量的存储期有自动存储期和静态存储期两种. 一般情况下,变量的存储期和作用域是紧密相关的.在函数外面定义的全局变量都 ...
- C++异常(异常的基本语法、栈解旋unwinding、异常接口声明、异常变量的生命周期、异常的多态使用、C++系统标准异常库)
文章目录 1 异常的基本概念 1.1 C语言中的异常处理 1.2 C++中的异常处理 1.3 异常严格类型匹配 2 栈解旋(unwinding) 3 异常的接口声明[C++11已废弃] 4 异常变量的 ...
- Android静态变量的生命周期
Android是用Java开发,其静态变量的生命周期遵守Java的设计.我们知道静态变量是在类被load的时候分配内存的,并且存在于方法区.当类 被卸载的时候,静态变量被销毁.在PC机的客户端程序中, ...
- Delphi匿名方法(三):扩展本地变量的生命周期
本地变量,一般是随着函数执行结束,就不能再访问: 而如果在匿名函数,访问了外部函数的本地变量,本地变量的生命周期会被扩展 unit Unit1;interfaceusesWinapi.Windows, ...
- JVM详解之:汇编角度理解本地变量的生命周期
文章目录 简介 本地变量的生命周期 举例说明 优化的原因 总结 简介 java方法中定义的变量,它的生命周期是什么样的呢?是不是一定要等到方法结束,这个创建的对象才会被回收呢? 带着这个问题我们来看一 ...
- C/C++构造及析构顺序及变量的生命周期
(1)变量的构造及析构顺序 1)在全局范围内定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用.如果一个程序中有多个文件,而不同文件之间都定义了全 ...
- c++中的异常---2(异常接口声明,异常变量的生命周期,异常的多态使用)
异常接口声明 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类的异常 如果 ...
- 【C语言简单说】十三:变量的生命周期
这次我们就来说说生命周期的问题.其实声明周期的意思就是他这个变量的作用范围,啥是作用范围?唔...看我举例子吧,意会,意会... 首先,我想问一下你们,如果你们校长叫做小明,你们班也有一个小明.那么你 ...
- 语言const的生命周期_C语言的角落——这些C语言不常用的特性你知道吗?
变长参数列表 头文件定义了一些宏,当函数参数未知时去获取函数的参数 变量:typedef va_list 宏: va_start() va_arg() va_end() va_list类型通过stda ...
最新文章
- 如何用JavaScript判断dom是否有存在某class的值?
- pythonfor输入多个数字_我一天学会了python最基础的编程
- 解决方案:Gateway实现全局跨域
- [转载] python json unicode utf-8处理总结
- c++sizeof求类大小 sizeof与strlen对比
- 对HTML5标签的认识(三)
- Perl语言入门,第17章自写习题答案。
- 自然语言处理(三) 语料库和语言知识库
- 希沃集控系统流媒体服务器未开启,希沃集控,让教育信息化管理尽在“掌控”之中...
- java地铁线路规划_地铁路线规划系统
- FFMpeg 常用命令格式转换,视频合成
- pcre c语言,pcre函数详细解析
- MFC中Combo的使用
- 网络安全入门(黑客)学习路线-2023最新版
- lin通讯从节点同步间隔场_LIN模块介绍
- 人工神经网络 :模糊神经网络
- c语言中关键字的含义,c语言中的关键字有哪些?有什么含义?
- WifiDisplay开启流程
- java对对碰游戏设计报告_手把手带你用Java打造一款对对碰游戏(下篇)
- Cookie 攻防世界