15.1 存储类型
15.1.1 外部存储
15.1.2 静态存储类型
15.2 作用域和可见性
15.2.1 局部作用域
15.2.2 全局作用域 和 域操作符
15.2.3 作用域嵌套及可见性
15.3 生存期
15.3.1 程序的内存分区
15.3.2 动态生存期
15.3.3 局部生存期
15.3.4 静态生存期
15.3.5 局部静态变量
15.4 对前15章的一点小结
上一章我们讲了“程序的文件结构”。主要涉及到一个问题,即:A文件中定义的某个变量,如果在B文件也能使用它。其间我们学到一个新关键字:extern,它用来声明一个变量,并且指明这是一个“外来的”的变量。如果你对我说的这些感到陌生,那么你先复习一下上一章。
这一章,我们正是要从extern说起。
存储类型分“外部存储”和“静态存储”两种。
外部存储类型使用 extern 关键字表示。
上一章我们其实一直在用外部存储类型的变量。
一个全局变量或函数,如果你需要在其它源文件中可以共用到,那么你必须将它声明为“外部存储类型”。这其实就是上一章我们所讲的内容。这里再举个例子,简要复述一次。
在A.cpp 文件中,有一个全局变量 a,和一个函数: func();
//A.cpp 文件:
...
int a;
void func()
{
...
}
...
我们希望在B.cpp 或更多其它文件可以使用到变量a和函数func(),必须在“合适的位置”声明二者:
//B.cpp 文件:
...
extern int a; //a 由另一源文件(A.cpp)定义
extern void func(); //func 由另一源文件(A.cpp)定义
a = 100;
func();
...
这里例子中,“合适的位置”是在B.cpp文件里。其它合适的位置,比如在头文件里的例子,请复习上一章。
另外一点需要得强调一次:函数的定义默认就是外部的,所以上面 func()之前的extern也可以省略。
在使用extern 声明全局变量或函数时,一定要注意:所声明的变量或函数必须在,且仅在一个源文件中实现定义。
如果你的程序声明了一个外部变量,但却没有在任何源文件中定义它,程序将可以通编译,但无法链接通过。下面是该错误类型的一个例子,大家请打开CB,将下面代码写入完整的一个控制台工程。
错误一、只有声明,没有定义:
1、用CB新建一个空白控制台工程,CB将自动生成Unit1.cpp。加入以下黑体部分:
extern void func2();
int main(int argc, char* argv[])
{
func2();
return 0;
}
2、新建一个单元文件(菜单:New | Unit):Unit2.cpp。在Unit2.cpp后面加入:
extern int a; //a 由另一源文件(A.cpp)定义
extern void func(); //func 由另一源文件(A.cpp)定义
void func2()
{
a = 100;
func();
}
现在按Ctrl + F9,将出现以下错误:
[Linker Error] 表明这是一个“链接”错误。两个错误分别是说变量a和函数func()没有定义。
(你可能奇怪为什么错误消息里,变量'a'的名字变成了'_a'?这是编译器遵循某些标准,在编译结果上对变量名做了一些改变,我们不必理会)
请大家想一想,并试一试,如何解决这两个链接错误。
错误二、有声明,但重复定义
1、用CB新建一个空白控制台工程,CB将自动生成Unit1.cpp。加入以下黑体部分:
extern void func2();
int a; // <--全局变量a在此定义了一次
void func() // <--函数func()在此定义了一次
{
a = 20;
}
int main(int argc, char* argv[])
{
func2();
return 0;
}
2、和错误一的第2步完全一样:
extern int a; //a 由另一源文件(A.cpp)定义
extern void func(); //func 由另一源文件(A.cpp)定义
void func2()
{
a = 100;
func();
}
3、再新建一个单元文件:Unit3.cpp,在文件后加入:
int a; // <--全局变量a在此又定义了一次
void func() // <--函数func()在此又定义了一次
{
a = 20;
}
现在编译这个含有三个单元文件的工程。这回答的是一个链接“警告/Warning”:
警告很长,无非是说全局变量 'a' 在两个模块内重复定义。 对了,func()函数我们不是也重复定义了吗?为什么没有得到警告?这是因为CB对重复定义的函数,将只取其一,然后自动抛弃所有重复项。下面的操作可以看到这一结果。
既然是错误类型只是“警告”,那就是说我们可以硬下心肠不管,继续运行。我们现在来看看,两个func()函数,CB到底用了哪一个?
这里需要在运行前,在两个func()的函数定义处,都设置断点:
第一个断点:在Unit1.cpp 文件里:
第二个断点:在Unit3.cpp 文件里:
然后按F9,运行,我们看到断点停在 Unit1.cpp中的 func()定义上:
而另一处:Unit3.cpp 里的断点,变“黄”(无效断点)了:
之所以成为无效断点,有两种原因:
其一是某些代码,比如单纯的变量声明:int a; 或如宏定义等,这些代码在编译后成为程序的初始化部分,无需运行。
其二是某些无用,或可优化的代码中编译过程被丢弃。
这里正是第二种情况。
尽管变量或函数重复定义似乎并不造成“致命”错误,但我们同样需要严加注意,消除所有这类错误。请大家对本例进行改错。
静态存储类型使用 static 关键字表示。
static 关键限定其所修饰的全局变量或函数只能在当前源文件中使用。
反过来说,如果我们确定某个全局变量仅仅是在当前源文件中使用,我们可以限定它为静态存储类型。
static 的使用格式 :
static 变量定义或函数定义
如:
static int a;
static void func();
举一个例子,下面的代码可以正确编译、运行:
Unit1.cpp 文件:
...
extern int a;
int main(int argc, char* argv[])
{
a = 100;
return 0;
}
Unit2.cpp 文件:
...
int a;
说明:在Unit1.cpp 文件中用到了外部变量:a, a在Unit2.cpp文件内定义。
现在,我们要限定 Unit2.cpp 里的变量 a 只能在 Unit2.cpp 内可以使用:
Unit2.cpp 文件:
...
static int a;
我们为 a 的定义加了一个修饰:static。现在再编译,编译器提示一个链接错误,我们在本章前面说过的:“变量 a 没有定义”:
静态函数的例子类似:
Unit1.cpp 文件:
...
void func();
int main(int argc, char* argv[])
{
func();
return 0;
}
Unit2.cpp 文件:
int i;
static void func()
{
i = 100;
}
按Ctrl+F9,得到以下链接错误:
又是两个制造错误例子,不要偷懒,务必亲手制造出这两个错误,并且再改正后,才继续看下面的课程。千万不要仅满足于“看得懂”就不动手。那样绝对不可能学会编程。
static 还有一种用法,称为函数局部静态变量,作用和这里的“全局静态”关系不大,我们在后面的“生存期”中会讲到。
由于静态变量或静态函数只在当前文件(定义它的文件)中有效,所以我们完全可以在多个文件中,定义两个或多个同名的静态变量或函数。
比如在A文件和B文件中分别定义两个静态变量a:
A文件中:
static int a;
B文件中:
static int a;
这两个变量完全独立,之间没有任何关系,占用各自的内存地址。你在A文件中改a的值,不会影响B文件中那个a的值。
作用域和可见性可以说是对一个问题的两种角度的思考。
“域”,就是范围;而“作用”,应理解为“起作用”,也可称为“有效”。所以作用域就是讲一个变量或函数在代码中起作用的范围,或者说,一个变量或函数的“有效范围”。打个比方,就像枪发出的子弹,有一定的射程,出了这个射程,就是出了子弹的“有效”范围,这颗子弹就失去了作用。
代码中的变量或函数,有的可以在整个程序中的所有范围内起作用,这称为“全局”的变量或函数。而有的只能在一定的范围内起作用,称为“局部”变量。
我们在 5.1.3 “如何为变量命名”这一小节中讲到: “不能在同一作用范围内有同名变量”。因此,下面的代码是错误的:
...
int a; //第一次定义a
int b;
b = 2*a;
int a; //错误:又定义了一次a
...
那么,在什么情况下,变量属于不同的作用范围呢?我们这里说的是第一种:一对{}括起来的代码范围,属于一个局部作用域。如果这个局部作用域包含更小的子作用域,那么子作用域的具有较高的优先级。在一个局部作用域内,变量或函数从其声明或定义的位置开始,一直作用到该作用域结束为止。
例一:变量只在其作用域内有效
void func()
{
int a;
a = 100;
cout << a << endl; //输出a的值
}
int main(int argc, char* argv[])
{
cout << a << endl; // <-- 错误: 变量a未定义
return 0;
}
说明:在函数 func()中,我们定义了变量a,但这个变量的“作用域”在 } 之前停止。所以,出了花括号以后,变量a就不存在了。请看图示:
结论:在局部作用域内定义的变量,其有效范围从它定义的行开始,一直到该局部作用域结束。
在局部作用域内定义的变量,称为“局部变量”。
上例中的局部作用域是一个函数。其它什么地方我们还能用到{}呢?很多,所有使用到复合语句的地方,比如:
//if 语句
if( i> j)
{
int a;
... ...
}
上面的a是一个局部变量,处在的if语句所带的那对 {} 之内。
//for 语句:
for(int i=0;i<100;i++)
{
int a;
... ...
}
上面的a也是一个局部变量。处在for语句带的{}之内。
for 语句涉及局部作用域时,有一点需要特别注意:上面代码中,变量 i 的作用域是什么?
根据最新的 ANSI C++ 规定,在for的初始语句中声明的变量,其作用范围是从它定义的位置开始,一直到for所带语句的作用域结束。而原来老的标准是出了for语句仍然有效,直到for语句外层的局部作用域结束。请看对比:
假设有一for语句,它的外层是一个函数。新老标准规定的不同作用域对比如下:
如果按照旧标准,下面的代码将有错,但对新标准,则是正确的,请大家考虑为什么:
void func()
{
for(int i=0;i<9;i++)
{
cout << i << endl;
}
for(int i=9;i>0;i--) //<-- 在这一行,旧标准的编译器将报错,为什么?
{
cout << i << endl;
}
}
Borland C++ Builder 对新旧标准都可支持,只需通过工程中的编译设置来设置采用何种标准。默认总是采用新标准。记住:如果你在代码中偶尔有需要旧标准要求的效果,你只需把代码码写成这样:
int i;
for(i=0;i<9;i++)
{
...
}
这时候,i的作用域就将从其定义行开始,一直越过整个for语句。
其它还有不少能用到复合语句(一对{}所括起的语句组)的流程控制语句,如do..while等。请复习以前相关课程。
其实,就算没有流程控制语句,我们也可以根据需要,在代码中直接加上一对{},人为地制造一个“局部作用域”。比如在某个函数中:
void func()
{
int a = 100;
cout << a << endl;
{
int a = 200;
cout << a << endl;
}
cout << a << endl;
}
代码中红色部分即是我们制造的一个局部作用域。执行该函数,将有如下输出:
100
200
100
你能理解吗?
如果一个变量声明或定义不在任何局部作用域之内,该变量称为全局变量。同样,一个函数声明不处于任何局部作用域内,则该函数是全局函数。
一个全局变量从它声明或定义的行起,将一起直接作用到源文件的结束。
请看下例:
//设有文件 Unit1.cpp,内定义一个全局变量:
int a = 100;
void func()
{
cout << a << endl;
}
输出:
100
我们今天还要学习到一个新的操作符,域操作符 “::”。域操作符也称“名字空间操作符”,由于我们还没学到“名字空间”,所以这里重点在于它在全局作用域上的使用方法。
:: 域操作符,它要求编译器将其所修饰的变量或函数看成全局的。反过来说,当编译器遇到一个使用::修饰的变量或函数时,编译器仅从全局的范围内查找该变量的定义。
下面讲到作用域的嵌套时,你可以进一步理解全局作用域如何起作用,同时,下例也是我们实例演示如何使用域作用符::的好地方。
例二:嵌套的两个作用域
在例一的基础上,我增加一个全局变量:
int a = 0; //<-- 全局变量,并且初始化为0
void func()
{
int a;
a = 100;
cout << a << endl; //输出a的值
}
int main(int argc, char* argv[])
{
cout << a << endl; //输出a的值
}
我们在 5.1.3 “如何为变量命名”这一小节中讲到: “不能在同一作用范围内有同名变量”。 上面的代码中,定义了两个a,但并不违反这一规则。因为二者处于不同的作用范围内。下图标明了两个a的不同作用范围:
从图示中看到:两个变量a:1个为全局变量,一个为局部变量。前者的作用域包含了后者的作用域。这称为作用域的嵌套。
如果在多层的作用域里,有变量同名,那么内层的变量起作用,而外层的同名变量暂时失去作用。比如在上例中,当代码执行到①处时,所输出的是函数 func()内的a。而代码②处,输出的是全局变量a。
这就引出一个“可见性”这个词,当内层的变量和外层的变量同名时,在内层里,外层的变量暂时地失去了可见性。
不过,如果外层是全局作用域,那么我们可以使用::操作符来让它在内层有同名变量的情况下,仍然可见。
int a = 0;
void func()
{
int a;
a = 100;
cout << a << endl; //输出内层的a;
cout << ::a << endl; //输出全局的a。
}
最后请大家把本节中讲到例子,都在CB上实例际演练一下。
一个变量为什么有会不同的作用域?其中一种最常见的原因就是它有一定的生存期。什么叫生存期?就像人一样,在活着的时候,可以“起作用”,死了以后,就不存在了,一了百了。
那么,在什么情况下一个变量是“活”着,又在什么情况下它是“死”了,或“不存在”了呢?
大家知道,变量是要占用内存的。比哪一个int类型的变量占用4个字节的内存,或一个char类型的变量占用1个字节的内存。如果这个变量还占用着内存,那么我们就认为它是“活着”,即,它存在着。而一个变量释放了它所占用的内存,我们就认为它“死了”,“不存在”了。
有哪个同学能告诉我,在我们的教程中,我这是第几次讲到“变量和内存”的关系?呵,我也记不得了。不管怎样,这里又是一次——我们必须从整体上讲一讲:一个程序在内存中如何存放?
先从程序上看“生”和“死”。
用CB编译出一个可执行文件(.exe),它被存放在磁盘上。当它没有运行时,我们认为它是“死”的。而当我们双击它,让它“跑”起来时,我们认为它是“活”的,有了“生命”。等我们关闭它,或它自行运行结束,它又回到了“死”的状态下。在这个过程里。
程序运行时,它会从操作系统那里分得一块内存。然后程序就会把这些内存(严格讲是内存的地址)进行划分,哪里到哪里用来作什么。这有点像我们从老板那里领来2000大洋,其中1000无要交月租,500元做生活费……真惨。
那么,程序有哪些需要入占用内存呢?
首先,代码需要一个空间来存放。因此,到手的内存首先要分出一块放代码的地方,称为代码区。剩下的是数据。根据不同需要,存放数据有区域有三种:数据区,栈区,堆区。为什么存放数据的内存需要分成三个区域?这个我先不说,先来说说数据(变量等)被放入不同的区内,将遇上什么样不同的命运。
第一、放入数据区的数据。
生存期:这些数据的命运最好。它们拥有和程序一样长的生存期。程序运行时,它们就被分配了内存,然后就死死占着,直到程序结束。
谁负责生死:这些数据如何产生,如何释放,都是程序自动完成的,我们程序员不用去费心为产生或释放这些变量写代码。
占用内存的大小:这些数据都必须有已知,且固定的大小,比如一个int变量,大小是4个字节,一个char类型,大小是1个字节。为什么必须这样?因为如果这个数据可以占用的大小是未定的,那么,程序就不可能为自动分配内存。
初始化:就是这个变量最开始的值是什么?放在数据区里的数据,可以是程序员用代码初始化,比如:
int a = 100;
这样,a的值按你意思去办,并初始化为100;但如果你没有写初始的代码,如:
int a;
那么,数据区内的数据将被初始化为全是0。
第二、放入堆区的数据。
生存期:堆内的数据什么时候“生(分配内存)”,什么时候“死(释放内存)”,由程序员决定。
谁负责生死:当然就是程序员了。C++里,有专门的函数或操作符来为堆里的变量分配或释放内存。程序员通过写这些代码来在需要时,让某个堆里的变量“生”,不需要时,让它“死”。
占用内存的大小:堆里的数据占用的内存可以是固定的,也可以是可变的。这就是C,C++里最强大也最难学的内容:“指针”所要做事。
初始化:由程序员完成。如果程序员不给它初始值,则它的值是未定的。
由于程序员掌握着堆区内的数据的“生死大权”,并且决定着该数据占用多少内存。所以在写程序时,必须特别注意这些数据。一不小心就会出错。比如一个数据还没有分配内存呢,你就要使用它,就会出错。更常见的是,一个数据,你为它分配了内存,可是却始终没有为替它释放内存,那样就会造成“内存泄漏”。就算你的程序都退出了,这个数据依然可能“阴魂不散”地占用着内存。
第三、放入栈区的数据。
生存期:对比前面的两种,数据区里数据具有永久的生存期,而堆里的数据的生存期算是“临时”的。需要了,程序员写代码产生;不需要了,又由程序员写代码释放。在程序员,临时才需要变量非常多,如果每个变量都由程序员来负责产生、释放,那程序员岂不很累?并且很危险(万一忘了释放哪个大块头的家伙....)。所以,必须有一种机制可以让程序自已来产生和释放某些临时变量。所以,放入堆区的数据是只有程序员才能决定的何时需要,何时不需的临时数据,而栈区数据则是编译器就能决定是否需要的临时数据。 当然,要想让编译器能知道数据什么时候需要,什么时候不需要,就必须做一种约定。这正是我们现在讲的“生存期”的语法内容。
谁负责生死:程序(和数据区的一样)。
占用内存的大小:固定大小(和数据区的一样)。
初始化:由程序员完成。如果程序员不给它初始值,则它的值是未定的(和堆区的一样)。
下面是三个区加上代码区的分布示意图:
现在,我们也比较好回答前面的问题:“为什么存放数据的内存需要分成三个区域”?原因正在于程序所要用到的数据具有不同的生存期要求,所以编译器把它们分别放到不同空间,好方便实现要求。
生存期和作用域的关系是:如果一个变量已经没有了生存期,那么自然它也就没了有作用域。但反过来,如果一个变量出了它的作用域,它并不一定就失去了生存期。典型的如函数内的静态数据,下面会讲到。
就是放在“堆区”的数据。这些数据是在程序运行到某一处时,由程序员写的代码动态产生;后面又由程序员写的代码进行释放。我们现在还没有学习如何为变量(指针变量)分配和释放的内存的知识。
这里的局部和前面讲“局部作用域”一致,都是指“一对{}括起来的代码范围”。
请看下面代码,并思考问题:
//从前,有一个函数……
void func()
{
//函数内,有一个局部变量……
int a;
cout << a << endl;
a = 100;
}
//看清楚了,上面输出 a 的值的语句, 位于给a赋值之前!
//然后,下面的代码是两次调用这个函数:
...
func();
func();
...
第一次调用,我们知道屏幕肯定是要输出一个莫名其妙的数,因未初始化的局部变量,其值是不定的。我们以前讲变量时,就做过实例。现在,这里的变量a被输出后,我们让赋于它100。再接下来,我们又调用了一次函数func();请问这回输出的值,是100呢?或者仍然是莫名其妙的数?
大家打开CB,把这个例子做做。注意,动手生成一个空白的控制台工程后,调用func()的那两行代码,要放到主函数main()内,形如:
……
int main(int argc, char* argv[])
{
func();
func();
……
}
正确答案应该是:“仍然是莫名其妙的数”。尽管在第一次调用时func()时,局部变量最后被赋值为100;但很可惜,出了函数这个作用域,a 立即就死掉了……第二次再调用函数func()时,那个像个a投胎转世的婴儿,一切又重新开始……它又是一个没有被赋值的的变量了。
请大家把本例中的变量a改为全局变量,并且在函数func()的定义之前定义。再试一试。
就是放在“数据区”里的数据。程序一运行时,它们就开始存在;程序结束后,它们自动消亡。
这里讲的“静态”,和前面的“静态存储类型”不是一个意思。(老师,我忘了什么叫“静态存储类型”?呵,这有可能,本章的内容互相都有些关联和相似,大家多看几遍本章,最主要是课程让你动手的地儿,你就动手做,正所谓“该出手时就出手手……”)。
“静态存储类型”是指:一个全局变量,它被加上static之后,就只能在本文件内使用,别的文件不能通过加extern的声明来使用它。
“静态生存期“是指:一个变量,它仅仅产生和消亡一次(即在程序运行时产生,在程序退时消亡),而不像“动态生存期”或“局部生存期”那样可以生生死死,不断“投胎转世”。
下面的代码演示了“静态生存期”和“局部生存期”变量的不同。请你看完以后,回答问题。
#include <iostream.h>
//定义,声明一个全局变量:
int a;
//声明一个函数,定义在后面
void func();
int main(int argc, char* argv[])
{
int b = 100;
a = 10;
cout << a << end;
cout << b << end;
//调用函数func:
func();
}
//func()的定义:
void func()
{
cout << a << endl;
cout << b << endl;
}
哪里有错呢?请大家想想,试试。
//从前,有一个函数……
void func()
{
//函数内,有一个局部变量……
int a;
cout << a << endl;
a = 100;
}
//调用两次:
func();
func();
同样是这个例子,我们只是要把 int a 之前加上一个 static 关键字:
void func()
{
//函数内,有一个局部静态变量……
static int a;
cout << a << endl;
a = 100;
}
...
func();
func();
...
我们要问的也是同样一个问题:第二次调用 func()后,输出的 a 值是多少?
这回答案是:输出的值是100。
这就是局部静态变量的特殊之处:尽管出了函数的作用域之后,变量已经不可见,并且也失去了作用。但是,它仍然存在着!并且保留着它最后的值。因此,它也是静态生存期。它也只在程序结束之后,才失去生存期。
上面讲的是局部静态变量“死”的问题,它也只“死”一次,对应地,显然它也只能“生”一次。
void func()
{
static int a = 30; //在定义时,同时初始化该局部静态变量为30。
cout << a << endl;
a = 100;
}
...
func();
func();
...
这回要问的是第一次调用func()时,输出的是什么?第二次呢?
答案:第一次输出30,第二次输出100,以后若有第三次,第四次,也是输出100。这就是说,初始化:static int = 30;这一句,仅被执行一次!
好,假如代码是这样子呢?
void func()
{
static int a;
a = 30; //改成不是在定义时同时初始化
cout << a << endl;
a = 100;
}
...
func();
func();
...
请回答我,这回,两次调用func()分别输出什么?
有关内存的堆、栈内容,最近我曾在CSDN上做过回答。大家如有兴趣,不妨去看看。(别忘了,我叫“nanyu”)。要学习编程,CSDN是不错的地方。大家有空常去(最好注册个ID)。我因为太忙,去不了几次。
http://expert.csdn.net/Expert/topic/1255/1255577.xml?temp=.1724665
一般教材用6或7章的内容,我们扩展成15章。大家可能嫌我讲得太慢。这我承认。哎,我的事情太多了。特别是最近这一段时间,先是笔者得了肾结石,剧痛了一夜最后住入医院,接着是我的宝宝发高烧,我在医院里守了一宿,未料到我的爱人接着也 得上医院……可恨的是电信局里不知哪个家伙一时兴起,来个什么“改线”,改来改去也不知改错了什么,我的ASDL就一断数天……幸好我天天打电话免费(不,是倒贴电话费 啊)为他们培训什么叫ADSL。(其实我也不懂,不过我总得旁敲侧击地暗示他们,“你们是不是动了什么啊?好好想想?再想想?”,哎,总的来说,他们的客户服务态度还是很好的……)。
不管怎样,在此我向所有付费报名的学员致歉。
这15章的内容,属于C,C++的基础知识,其中有些更是基础中基础。从第16章(数组)开始,就开始中级或高级的内容了。这些新的内容都有一个特点:都和内存地址有着千丝万缕的关系。所以大家有时间抓紧把前面的都复习中,其中犹其是要把我讲到的,有关“变量和内存”关系,全部重新消化一遍。
明天就是冬至了。圣诞节即到,在此我向大家问个节日快乐,并感谢早早给我发来贺卡的几位同学。
更重要的是春节即将到来,但愿我们能在春节前学完C++。然后一起向更为精彩的 Windows编程世界出发。