变量的存储、作用域和生存期

一、局部变量和全局变量

       在函数内定义的变量是局部变量,在函数外定义的变量是全局变量。从变量的作用域(即从空间)的角度来观察,局部变量的作用域是只在本函数内有效。全局变量可以为本文件其他函数所共用,它的有效范围从定义变量的位置开始到本源文件结束。

1.1 局部变量的一些概念

       (1)主函数(main)中定义的变量也只在主函数中有效,并不因为在主函数中定义而在整个文件或程序中有效。主函数也不能使用其他函数中定义的变量。

       (2)不同函数中可以使用同名的变量,它们代表不同的对象,互不干扰。

       (3)形式参数也是局部变量。

       (4)在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句内有效,离开复合语句系统会把它占用的内存单元释放。

1.2 全局变量的一些概念

       (1)设置全局变量的作用是增加了函数间数据联系的渠道。

       (2)为了便于区别全局变量和局部变量,在C程序设计人员有一个习惯(非规定),全局变量名的第一个字母用大写表示。

       非必要时不建议使用全局变量,原因如下:

       (1)全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟内存单元。

       (2)程序的可靠性和通用性降低,在程序设计时要求“高内聚、低耦合”,即模块的功能要单一(不要把许多互不相干的功能放在一个模块中,与其他模块的相互影响要尽量少,而使用全局变量是不符合这个原则的)。一般要求把C程序中的函数做成一个相对的封闭体,除了可以通过“实参-形参”的渠道与外界发生联系外,没有其他渠道。这样程序的移植性好、可读性强。

       (3)使用全局变量过多,会降低程序的清晰性,人们往往难以清楚地判断出每个瞬时各个外部变量的值。由于在各个函数执行时都可能改变外部变量的值,程序容易出错。

       如果在同一个源文件中,全局变量和局部变量同名系统如何处理?

#include<stdio.h>

int a = 3, b = 5;
int main()
{
	int max(int a, int b);		//函数声明
	int a = 8;					//定义局部变量a与全局变量同名
	printf("max = %d\n", max(a, b));
	return 0;
}

int max(int a, int b)			//形参也是局部变量,a与全局变量同名
{
	int c = 0;
	c = a > b ? a : b;
	return c;
}

       上面例程第3行定义了全局变量a,在main函数中第7行定义了局部变量a与全局变量同名,局部变量a的作用范围为第7~9行,在此范围内,全局变量a被局部变量a屏蔽,相当于全局变量a在此范围内不存在,不起作用。在第12行起定义max函数,形参a和b是局部变量,全局变量a和b在max函数范围内不起作用。总而言之:全局变量如果与局部变量同名,在局部变量作用域内全局变量无效,应避免出现。

二、变量的存储方式

2.1 动态存储、静态存储

       在C语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类别。由上一节已知从变量的作用域(即从空间)的角度来观察变量可分为局部变量和全局变量。还可以从另一个角度,即从变量值存在的时间(即生存期)来观察:有的变量在程序运行的整个过程都是存在的,而有的变量则是在调用其所在的函数时才临时分配内存单元,而在函数调用结束后该存储单元就马上释放了,变量不存在了。变量的存储方式有两种:静态存储方式和动态存储方式。静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式,而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。内存中供用户使用的存储空间分为程序区、静态存储区、动态存储区三部分。

2.2 堆、栈

       非静态的局部变量、函数的形式参数和返回值是分配在内存中的动态存储区的,这个存储区域称为栈(stack);除此以外,C语言还允许建立动态内存分配区域,以存放一些临时用的数据,这些数据不必在程序的声明部分定义,也不必等到函数结束时才释放,而是需要时随时开辟,不需要时随时释放。这些数据是临时存放在一个特别的自由存储区,称为堆(heap)区。由于未在声明部分定义它们为变量和数组,因此不能通过变量名或数组名去引用这些数据,只能通过指针来引用。对内存的动态分配是通过系统提供的库函数来实现的,主要有malloc,calloc,free,realloc这4个函数。栈区和堆区都属于动态存储区,栈区是程序运行时由系统自动分配的一块连续的内容,而堆区是在内存开辟一块不连续的存储区域,由程序员分配释放。

        一个由C/C++编译的程序占用的内存分为栈区、堆区、静态区(全局区)、常量区、程序区。其中常量区和代码区的内容不可修改,字符串、数字等常量和const修饰的全局变量存放在常量区;代码区存放程序执行代码,字符串常量和define定义的常量也可能存放在代码区。网上查阅资料看到这样一个说法:STM32的ROM区域存放代码区和常量区;RAM区域存放全局(静态)区、堆区和栈区。还可基于STM32编程定义各种类型的常量和变量输出其地址,对比STM32内存配置即ROM和RAM地址范围进行验证。C语言内存分区示意图如下:

图2.1 C语言内存分区示意图

       栈区按内存地址由高到低方向生长,程序运行时由编译器自动分配的一块连续的内容;堆区按内存地址由低到高方向生长,需程序员自己申请(调用malloc,realloc,calloc),在内存开辟另一块不连续的存储区域并指明大小,并由程序员进行释放,容易产生memory leak。下面例程的bug能够很好的说明栈区地址是向下增长的:

#include<stdio.h>

int main()
{
	int i =0;
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	for (i = 0; i <= 12; i++)
	{
		arr[i] = 0;
		printf("jdp love 江清易见鱼 forever\n");
	}
	return 0;
}

        上述例程的运行结果是无限执行循环体内容,如下图所示:

图2.2 例程运行结果

       首先可以判断肯定是跟数组越界访问有关,为什么陷入死循环?F10逐步调试,当循环变量i执行到 i = 12还未执行arr[ i ] = 0时调试结果如下图所示:

图2.3 调试结果1

        arr[ i ] = 0执行结束后调试结果如下图,可以看见i变为0了,以后每次i执行到i = 12时执行arr[ 12 ] = 0语句后i就变为0,这就是造成程序陷入死循环的原因。

图2.4 调试结果2

       arr[12] = 0越界访问赋值的同时i也变为0,说明arr[12]指向的内存地址存的是i变量,可通过监控窗口观察两个变量的地址进一步验证,结果如下图:

图2.5 调试结果3

       原因分析:i和arr都是局部变量,存在栈区中,栈区的内存分配是由高地址到低地址,所以运行此例程时先在高地址出开辟i的内存空间,再开辟arr[10]数组的低地址空间,我们知道数组是随着下标的增长地址是由低到高变化的,若i的地址空间与arr[9]的地址空间之间隔着2个int型的内存空间,那么arr[12]地址指向的内存空间就是i的地址空间,即如果i和arr之间有适当的空间,利用数组的越界操作就有可能改变i的值。内存示意图如下图所示:

图2.6 栈区变量存放示意图

2.3 局部变量的存储类别

       局部变量分为三种,其存储位置是不同的:自动变量(auto变量)存储在动态存储区(栈);静态局部变量存储在静态存储区;寄存器变量存储在CPU的寄存器中。

       自动变量(auto变量):函数中的局部变量,如果不专门声明为static(静态)存储类别,都是动态地分配存储空间的,函数形参和在函数中定义的局部变量(包括在复合语句中定义的局部变量)都属于此类。在调用函数时,系统会给这类局部变量分配存储空间,调用结束时就自动释放这些存储空间。自动变量用关键字auto作存储类别声明,实际上关键字auto可以省略,不写则隐含指定为自动变量。

       静态局部变量(static局部变量):有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用该函数时该变量已有值(就是上一次函数调用结束时的值)。这时就应该指定该局部变量为“静态局部变量”,用关键字static进行声明。

       非静态局部变量(自动变量)和静态局部变量的说明:

       (1)静态局部变量在静态存储区内分配存储单元,在程序整个运行期间都不释放;自动变量分配在动态存储空间,函数调用结束后就释放。

       (2)对静态局部变量是在编译时赋初值的,即只赋值一次,在程序运行时它已有初值,以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值;对自动变量赋初值不是在编译时进行的,而是在函数调用时进行的,每调用一次重新给一次初值,相当于执行一次赋值语句。

       (3)如果在定义局部变量时不赋初值对静态局部变量来说编译时自动赋初值为0(对数值型变量)或空字符'0'(对字符变量);而对自动变量来说它的值是一个不确定的值,这是由于每次调用函数结束后存储单元已释放,下次调用结束时又重新分配存储单元,而所分配的存储单元中的内容是不可知的。

        (4)虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。

       (5)用静态存储要多占内存(长期占用不释放,而不能像动态存储那样一个存储单元可以先后为多个变量使用,节约内存),而且降低了程序的可读性,调用次数过多时不知道变量的当前值是多少。因此若非必要不要多用静态局部变脸。

       寄存器变量:无论是静态存储方式还是动态存储方式变量的值是存放在内存中的,当程序中要用到一个变量的值时需要从内存中取数,如果要存数还要写到内存中,如果一个变量使用频繁则存取变量要花费不少时间。由于对寄存器的存取速度远高于对内存的存取速度,为提高执行效率,允许将变量的值放在CPU的寄存器中,这种变量叫做寄存器变量,用关键字register作声明。由于现在的计算机的速度愈来愈快,性能愈来愈高,优化的编译系统能够识别使用频繁的变量,从而自动地将这些变量放在寄存器中,而不需要程序设计者指定。

2.4 全局变量的存储类别

       全局变量(函数之外定义的变量:外部变量)都是存放在静态存储区中的,无论是静态外部变量(只限本文件引用)还是非静态的外部变量(允许其他文件引用)。

       在一个文件内扩展外部变量的作用域:如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。如果在定义点之前的函数需要引用该外部变量则应在引用之前用关键字extern对该变量作“外部变量声明”,表示把该外部变量的作用域扩展到此位置,如下图正确例程中x变量定义的位置在main函数之后,在main函数开头用extern对其进行外部变量声明,程序编译正常有了此声明,就可以从“声明”处起,合法地使用该外部变量。

图2.7 全局变量extern使用例程(媳妇儿制作的图^_^)

       如果不作extern声明,如上图错误例程,编译main函数时就会出错,如下图所示。提倡将外部变量的定义放在应用它的所有函数之前,事实上我见过的程序全局变量的定义都在源文件开头处,这样可以避免在函数中多加一个extern声明。 

图2.8 编译报错

       用extern声明外部变量时,类型名可以写也可以省写。例如,“extern intA,B,C”也可以写成“extern A,B,C”。因为它不是定义变量,可以不指定类型,只须写出外部变量名即可。

       将外部变量的作用域扩展到其他文件:一个C程序可以由多个源程序文件组成,如果两个文件都要用到同一个外部变量,不能分别在两个文件中各自定义一个外部变量,否则在程序编译时会出现“重复定义”的错误。正确的做法是在其中一个文件中定义该外部变量,而在另一文件中用extern对其作外部变量声明。在编译和链接时,系统会由此知道变量有“外部链接”,可以从别处找到已定义的外部变量并将其作用域扩展到本文件中。

       这样扩展全局变量的作用域应慎重,因为在执行一个文件中的操作时可能会改变该全局变量的值,从而影响到另一文件中全局变量的值,进而影响另一文件中的执行结果。

       在编译时遇到extern时,先在本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域;如果找不到,就在连接时从其他文件中找外部变量的定义。如果从其他文件中找到了,就将作用域扩展到本文件;如果再找不到,就按出错处理。 

       将外部变量的作用域限制在本文件中:有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个static声明,这种变量称为静态外部变量。在程序设计中,常由若干人分别完成各个模块,各人可以独立地在其设计的文件中使用相同的外部变量名而不相干,只需在每个文件中定义外部变量加上static即可。这就为程序的模块化、通用性提供方便,如果已确认其他文件不需要引用本文件的外部变量,就可以对本文件中的外部变量都加上static以免被其他文件误用,这就相当于把本文件的外部变量对外部屏蔽起来。

图2.9 全局变量和局部变量存储总结脑图

三、extern和static

3.1 关于函数、变量的声明和定义

       对“int a;”而言,它既是声明,又是定义;而对“extern A;”而言,它是声明而不是定义,声明将已定义的外部变量A的作用域扩展到此。前者称为定义性声明(defining declaration),或简称定义(definition);后者称为引用性声明(referencing declaration)。把建立存储空间的声明称定义,而把不需要建立存储空间的声明称为声明。

在函数中出现的对变量的声明(除了用extern声明的以外)都是定义。

       外部变量的声明和外部变量的定义的含义是不同的。外部变量的定义只能有一次,而声明可以有多次,哪里需要用就可以在哪里声明。系统根据外部变量的定义(而不是外部变量的声明)分配存储单元。

       函数定义是指函数的具体实现,交代函数的功能实现。函数声明是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。函数的声明一般出现在函数的使用之前。要满足先声明后使用,函数的声明一般是放在头文件中的,例如一个Add函数可以定义在add.c文件中,声明放在add.h中,只需要在主函数文件中包含add.h即可在主函数文件中使用Add函数。

3.2 extern和static

       extern关键字用来对变量和函数进行声明。当extern与一个变量使用,它只是声明,没有定义,声明将已定义的外部变量的作用域扩展到此。extern声明函数时,表示该函数是在其他文件中定义的外部函数,就能够在本文件中调用这个函数,或者说把这个函数的作用域扩展到本文件。

       在程序中经常需要调用其他文件中的外部函数,为方便编程C语言允许在声明函数时省写extern,这就是函数原型。用函数原型能够把函数的作用域扩展到定义该函数的文件之外。只要在使用该函数的每一个文件中包含该函数的函数原型即可。函数原型通知编译系统:该函数在本文件中稍后定义,或在另一文件中定义。利用函数原型扩展作用域最常见的例子是#include指令的应用,如上3.1举例说明。

       包含头文件相当于把头文件中的内容全部拷贝过来,在头文件中声明函数,包含头文件就是实现在本文件中声明该函数。函数的定义和声明分开在不同的文件大有裨益,其中一个好处就是如果你开发了一个库或者游戏,只需要把源码编译成静态库再提供一个.h头文件即可卖给用户,让用户使用你开发出来的软件功能而不会泄露你的源码。

       static有三种用法,总结如下脑图:

图3.1 static用法总结脑图

       用static声明局部变量的存储类型和声明全局变量的存储类型的含义是不同的:对于局部变量来说声明存储类型的作用是指定变量存储的区域(静态存储区还是动态存储区)以及由此产生的生存期的问题;而对于全局变量来说,由于都是在编译时分配内存的,都存放在静态存储区,声明存储类型的作用是变量作用域的扩展问题。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值