📄 教学--第十五章 存储类型、作用域、可见性和生存期.htm
字号:
<P> </P>
<P>说明:在Unit1.cpp 文件中用到了外部变量:a, a在Unit2.cpp文件内定义。</P>
<P> </P>
<P>现在,我们要限定 Unit2.cpp 里的变量 a 只能在 Unit2.cpp 内可以使用:</P>
<P> </P>
<P>Unit2.cpp 文件:</P>
<P>...</P>
<P><B>static </B>int a;</P>
<P> </P>
<P>我们为 a 的定义加了一个修饰:static。现在再编译,编译器提示一个链接错误,我们在本章前面说过的:“变量 a 没有定义”:</P>
<P><IMG height=20 src="教学--第十五章 存储类型、作用域、可见性和生存期.files/ls15.h8.gif"
width=642 border=0></P>
<P> </P>
<P> </P>
<P>静态函数的例子类似:</P>
<P> </P>
<P>Unit1.cpp 文件:</P>
<P> </P>
<P>... </P>
<P>void func();</P>
<P>int main(int argc, char* argv[])</P>
<P>{</P>
<P> func();</P>
<P> return 0;</P>
<P>}</P>
<P> </P>
<P>Unit2.cpp 文件:</P>
<P>int i;</P>
<P><B>static</B> void func()</P>
<P>{</P>
<P> i = 100;</P>
<P>}</P>
<P>按Ctrl+F9,得到以下链接错误:</P>
<P> </P>
<P><IMG height=19 src="教学--第十五章 存储类型、作用域、可见性和生存期.files/ls15.h7.gif"
width=653 border=0></P>
<P> </P>
<P>又是两个制造错误例子,不要偷懒,务必亲手制造出这两个错误,并且再改正后,才继续看下面的课程。千万不要仅满足于“看得懂”就不动手。那样绝对不可能学会编程。</P>
<P> </P>
<P>static 还有一种用法,称为函数局部静态变量,作用和这里的“全局静态”关系不大,我们在后面的“生存期”中会讲到。</P>
<P> </P>
<P>由于静态变量或静态函数只在当前文件(定义它的文件)中有效,所以我们完全可以在多个文件中,定义两个或多个同名的静态变量或函数。</P>
<P> </P>
<P>比如在A文件和B文件中分别定义两个静态变量a:</P>
<P> </P>
<P>A文件中:</P>
<P>static int a;</P>
<P> </P>
<P>B文件中:</P>
<P>static int a;</P>
<P> </P>
<P>这两个变量完全独立,之间没有任何关系,占用各自的内存地址。你在A文件中改a的值,不会影响B文件中那个a的值。</P>
<P> </P>
<H3><A name=15.2>15.2</A> 作用域和可见性</H3>
<P> </P>
<P>作用域和可见性可以说是对一个问题的两种角度的思考。</P>
<P> </P>
<P>“域”,就是范围;而“作用”,应理解为“起作用”,也可称为“有效”。所以作用域就是讲一个变量或函数在代码中起作用的范围,或者说,一个变量或函数的“有效范围”。打个比方,就像枪发出的子弹,有一定的射程,出了这个射程,就是出了子弹的“有效”范围,这颗子弹就失去了作用。</P>
<P> </P>
<P>代码中的变量或函数,有的可以在整个程序中的所有范围内起作用,这称为“全局”的变量或函数。而有的只能在一定的范围内起作用,称为“局部”变量。</P>
<P> </P>
<H4><A name=15.2.1>15.2.1</A> 局部作用域</H4>
<P> </P>
<P>我们在 5.1.3 “如何为变量命名”这一小节中讲到: <B>“不能</B>在同一作用范围内有同名变量”。因此,下面的代码是错误的:</P>
<P>...</P>
<P>int a; //第一次定义a</P>
<P>int b;</P>
<P>b = 2*a;</P>
<P>int a; //错误:又定义了一次a</P>
<P>...</P>
<P> </P>
<P>那么,在什么情况下,变量属于不同的作用范围呢?我们这里说的是第一种:<B>一对{}括起来的代码范围,属于一个局部作用域</B>。如果这个局部作用域包含更小的子作用域,那么子作用域的具有较高的优先级。在一个局部作用域内,变量或函数从其声明或定义的位置开始,一直作用到该作用域结束为止。</P>
<P> </P>
<P>例一:变量只在其作用域内有效</P>
<P> </P>
<P>void func()</P>
<P>{</P>
<P> int a; </P>
<P> </P>
<P> a = 100;</P>
<P> </P>
<P> cout << a << endl; //输出a的值</P>
<P>}</P>
<P> </P>
<P>int main(int argc, char* argv[])</P>
<P>{</P>
<P> cout << a << endl; // <-- 错误:
变量a未定义</P>
<P> </P>
<P> return 0;</P>
<P>}</P>
<P> </P>
<P> </P>
<P>说明:在函数 func()中,我们定义了变量a,但这个变量的“作用域”在 }
之前停止。所以,出了花括号以后,变量a就不存在了。请看图示:</P>
<P><IMG height=241 src="教学--第十五章 存储类型、作用域、可见性和生存期.files/ls15.h9.gif"
width=353 border=0></P>
<P> </P>
<P>结论:在局部作用域内定义的变量,其有效范围从它定义的行开始,一直到该局部作用域结束。</P>
<P>在局部作用域内定义的变量,称为“局部变量”。</P>
<P> </P>
<P>上例中的局部作用域是一个函数。其它什么地方我们还能用到{}呢?很多,所有使用到复合语句的地方,比如:</P>
<P> </P>
<P>//if 语句</P>
<P>if( i> j)</P>
<P>{</P>
<P> int a; </P>
<P> ... ... </P>
<P>}</P>
<P>上面的a是一个局部变量,处在的if语句所带的那对 {} 之内。</P>
<P> </P>
<P>//for 语句:</P>
<P>for(int i=0;i<100;i++)</P>
<P>{</P>
<P> int a; </P>
<P> ... ...</P>
<P>}</P>
<P>上面的a也是一个局部变量。处在for语句带的{}之内。</P>
<P>for 语句涉及局部作用域时,有一点需要特别注意:上面代码中,变量 i 的作用域是什么?</P>
<P>根据最新的 ANSI C++
规定,在for的初始语句中声明的变量,其作用范围是从它定义的位置开始,一直到for所带语句的作用域结束。而原来老的标准是出了for语句仍然有效,直到for语句外层的局部作用域结束。请看对比:</P>
<P>假设有一for语句,它的外层是一个函数。新老标准规定的不同作用域对比如下:</P>
<P><IMG height=189 src="教学--第十五章 存储类型、作用域、可见性和生存期.files/ls15.h11.gif"
width=410 border=0></P>
<P>如果按照旧标准,下面的代码将有错,但对新标准,则是正确的,请大家考虑为什么:</P>
<P> </P>
<P>void func()</P>
<P>{</P>
<P> for(int i=0;i<9;i++)</P>
<P> {</P>
<P> cout << i << endl;</P>
<P> }</P>
<P> </P>
<P> for(int i=9;i>0;i--) //<-- 在这一行,旧标准的编译器将报错,为什么?</P>
<P> {</P>
<P> cout << i << endl; </P>
<P> }</P>
<P>}</P>
<P> </P>
<P>Borland C++ Builder
对新旧标准都可支持,只需通过工程中的编译设置来设置采用何种标准。默认总是采用新标准。记住:如果你在代码中偶尔有需要旧标准要求的效果,你只需把代码码写成这样:</P>
<P>int i;</P>
<P>for(i=0;i<9;i++)</P>
<P>{</P>
<P> ...</P>
<P>}</P>
<P>这时候,i的作用域就将从其定义行开始,一直越过整个for语句。</P>
<P> </P>
<P>其它还有不少能用到复合语句(一对{}所括起的语句组)的流程控制语句,如do..while等。请复习以前相关课程。</P>
<P>其实,就算没有流程控制语句,我们也可以根据需要,在代码中直接加上一对{},人为地制造一个“局部作用域”。比如在某个函数中:</P>
<P>void func()</P>
<P>{</P>
<P> int a = 100;</P>
<P> cout << a << endl;</P>
<P> </P>
<P><FONT color=#ff0000> {</FONT></P>
<P><FONT color=#ff0000> int a =
200;</FONT></P>
<P><FONT color=#ff0000> cout << a
<< endl;</FONT></P>
<P><FONT color=#ff0000> }</FONT></P>
<P> </P>
<P> cout << a << endl; </P>
<P>}</P>
<P> </P>
<P>代码中红色部分即是我们制造的一个局部作用域。执行该函数,将有如下输出:</P>
<P><FONT color=#ffffff><SPAN
style="BACKGROUND-COLOR: #000000">100</SPAN></FONT></P>
<P><FONT color=#ffffff><SPAN
style="BACKGROUND-COLOR: #000000">200</SPAN></FONT></P>
<P><FONT color=#ffffff><SPAN
style="BACKGROUND-COLOR: #000000">100</SPAN></FONT></P>
<P>你能理解吗?</P>
<P> </P>
<H4><A name=15.2.2>15.2.2</A> 全局作用域 和 域操作符</H4>
<P> </P>
<P>如果一个变量声明或定义不在任何局部作用域之内,该变量称为<B>全局变量</B>。同样,一个函数声明不处于任何局部作用域内,则该函数是全局函数。</P>
<P> </P>
<P>一个全局变量从它声明或定义的行起,将一起直接作用到源文件的结束。</P>
<P> </P>
<P>请看下例:</P>
<P>//设有文件 Unit1.cpp,内定义一个全局变量:</P>
<P> </P>
<P>int a = 100;</P>
<P> </P>
<P>void func()</P>
<P>{</P>
<P> cout << a << endl; </P>
<P>}</P>
<P> </P>
<P>输出:</P>
<P><FONT color=#ffffff><SPAN
style="BACKGROUND-COLOR: #000000">100</SPAN></FONT></P>
<P> </P>
<P>我们今天还要学习到一个新的操作符,域操作符
“::”。域操作符也称“名字空间操作符”,由于我们还没学到“名字空间”,所以这里重点在于它在全局作用域上的使用方法。</P>
<P> </P>
<P>::
域操作符,它要求编译器将其所修饰的变量或函数看成全局的。反过来说,当编译器遇到一个使用::修饰的变量或函数时,编译器仅从全局的范围内查找该变量的定义。</P>
<P> </P>
<P>下面讲到作用域的嵌套时,你可以进一步理解全局作用域如何起作用,同时,下例也是我们实例演示如何使用域作用符::的好地方。</P>
<P> </P>
<H4><A name=15.2.3>15.2.3</A> 作用域嵌套及可见性</H4>
<P> </P>
<P>例二:嵌套的两个作用域</P>
<P> </P>
<P>在例一的基础上,我增加一个全局变量:</P>
<P> </P>
<P><B>int a = 0; //<-- 全局变量,并且初始化为0</B></P>
<P> </P>
<P>void func()</P>
<P>{</P>
<P> int a;</P>
<P> </P>
<P> a = 100;</P>
<P> </P>
<P> cout << a << endl; //输出a的值</P>
<P>}</P>
<P> </P>
<P>int main(int argc, char* argv[])</P>
<P>{</P>
<P> cout << a << endl; //输出a的值</P>
<P>} </P>
<P> </P>
<P>我们在 5.1.3 “如何为变量命名”这一小节中讲到: <B>“</B>不能在同一作用范围内有同名变量”。
上面的代码中,定义了两个a,但并不违反这一规则。因为二者处于不同的作用范围内。下图标明了两个a的不同作用范围:</P>
<P><IMG height=215 src="教学--第十五章 存储类型、作用域、可见性和生存期.files/ls15.h10.gif"
width=357 border=0></P>
<P>从图示中看到:两个变量a:1个为全局变量,一个为局部变量。前者的作用域包含了后者的作用域。这称为作用域的嵌套。</P>
<P>如果在多层的作用域里,有变量同名,那么内层的变量起作用,而外层的同名变量暂时失去作用。比如在上例中,当代码执行到<FONT
color=#ff0000>①</FONT>处时,所输出的是函数 func()内的a。而代码<FONT
color=#0000ff>②</FONT>处,输出的是全局变量a。</P>
<P>这就引出一个“可见性”这个词,当内层的变量和外层的变量同名时,在内层里,外层的变量暂时地失去了可见性。</P>
<P> </P>
<P>不过,如果外层是全局作用域,那么我们可以使用::操作符来让它在内层有同名变量的情况下,仍然可见。</P>
<P> </P>
<P>int a = 0;</P>
<P>void func()</P>
<P>{</P>
<P> int a;</P>
<P> a = 100;</P>
<P> </P>
<P> cout << a << endl; //输出内层的a;</P>
<P><B> cout << ::a << endl; </B>//输出全局的a。</P>
<P>}<BR> </P>
<P>最后请大家把本节中讲到例子,都在CB上实例际演练一下。</P>
<P> </P>
<H3><A name=15.3>15.3</A> 生存期</H3>
<P> </P>
<P>一个变量为什么有会不同的作用域?其中一种最常见的原因就是它有一定的生存期。什么叫生存期?就像人一样,在活着的时候,可以“起作用”,死了以后,就不存在了,一了百了。</P>
<P>那么,在什么情况下一个变量是“活”着,又在什么情况下它是“死”了,或“不存在”了呢?</P>
<P>大家知道,变量是要占用内存的。比哪一个int类型的变量占用4个字节的内存,或一个char类型的变量占用1个字节的内存。如果这个变量还占用着内存,那么我们就认为它是“活着”,即,它存在着。而一个变量释放了它所占用的内存,我们就认为它“死了”,“不存在”了。</P>
<P>有哪个同学能告诉我,在我们的教程中,我这是第几次讲到“变量和内存”的关系?呵,我也记不得了。不管怎样,这里又是一次——我们必须从整体上讲一讲:一个程序在内存中如何存放?</P>
<P> </P>
<H4><A name=15.3.1>15.3.1</A> 程序的内存分区</H4>
<P> </P>
<P>先从程序上看“生”和“死”。</P>
<P>用CB编译出一个可执行文件(.exe),它被存放在磁盘上。当它没有运行时,我们认为它是“死”的。而当我们双击它,让它“跑”起来时,我们认为它是“活”的,有了“生命”。等我们关闭它,或它自行运行结束,它又回到了“死”的状态下。在这个过程里。</P>
<P>程序运行时,它会从操作系统那里分得一块内存。然后程序就会把这些内存(严格讲是内存的地址)进行划分,哪里到哪里用来作什么。这有点像我们从老板那里领来2000大洋,其中1000无要交月租,500元做生活费……真惨。</P>
<P>那么,程序有哪些需要入占用内存呢?</P>
<P>首先,代码需要一个空间来存放。因此,到手的内存首先要分出一块放代码的地方,称为代码区。剩下的是数据。根据不同需要,存放数据有区域有三种:数据区,栈区,堆区。为什么存放数据的内存需要分成三个区域?这个我先不说,先来说说数据(变量等)被放入不同的区内,将遇上什么样不同的命运。</P>
<P> </P>
<P>第一、放入数据区的数据。</P>
<P>生存期:这些数据的命运最好。它们拥有和程序一样长的生存期。程序运行时,它们就被分配了内存,然后就死死占着,直到程序结束。</P>
<P>谁负责生死:这些数据如何产生,如何释放,都是程序自动完成的,我们程序员不用去费心为产生或释放这些变量写代码。</P>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -