4.2 作用域和生命期
作用域和生命期描述了常量、变量、函数等对象的适用范围。当程序代码中的这些对象超出了其适用范围时将出现编译错误,这经常出现在面向过程的程序开发中。本节将介绍有关对象的作用域和生命期的相关知识。
4.2.1 局部作用域
局部作用域描述的是函数体中变量、常量等对象的作用范围。每个函数都有一个独立的局部作用域,在函数体中定义的变量、常量,对外部函数是不可见的,因为它处于函数的局部作用域中。对于函数体中出现的复合语句,也有其独立的局部作用域,在其中定义的对象,复合语句之外是不能访问的。例如:
在上述代码中,变量ivar的作用范围处于复合语句的作用域中,因此语句“printf(“%d”,ivar);”访问变量ivar是非法的。
对于处于同一局部作用域的对象来说,对象是不允许重名的。例如:
在上述代码中,语句“char ivar;”定义的变量与整型变量ivar同名,并且处于同一局部作用域中,因此出现编译错误。同样,语句“int argc;”与参数arge同名,并且处于同一局部作用域中,因此也出现了编译错误。对于函数参数来说,其作用域为函数的局部作用域。复合语句部分定义的两个变量是合法的,它们虽然与变量ivar和函数参数同名,但是处于不同的局部作用域。
注意
在定义变量时,即使不在同一局部作用域,也不要定义同名的变量。在开发程序的过程中,同名的变量很容易被混淆,一旦程序出现错误,阅读起来会很复杂。
下面介绍一下编译器对局部作用域内变量命名解析的过程。当编译器在当前代码处发现变量名时,它将在当前的局部作用域内搜索变量的定义,如果没有发现变量的定义,则向外一层的局部作用域搜索变量的定义,直到搜索完局部作用域。如果还没有发现变量的定义,则会出现标识符没有定义的错误。如果在嵌套局部作用域的外层和内部作用域定义了同名的变量,对于内部的局部作用域变量来说,它将隐藏外层局部作用域中的变量。例如:
【例4.20】 隐藏外部局部作用域中的变量。
在上述代码中,复合语句中的printf函数输出的ivar值为10,而外层的printf函数输出的ivar值为5。在复合语句中,定义的变量ivar(值为10)隐藏了外部局部作用域定义的变量ivar(值为5)。
下面对上述代码稍加修改,分析输出结果。
在上面的复合语句中,两次调用了printf函数(黑体部分代码),第1次调用printf函数输出的ivar值为5,因为在当前复合语句的局部作用域内没有发现变量ivar的定义,编译器在外层的局部作用域中发现了ivar的定义。而第2个printf函数输出的ivar值为10,因为编译器在复合语句的局部作用域中直接发现了变量ivar的定义。
4.2.2 全局作用域
全局作用域是指函数、变量、常量等对象的作用范围是整个应用程序,这些对象在整个应用程序中都是可用的。在全局作用域内定义的对象被称为全局对象。例如,在全局作用域内定义的函数被称为全局函数,在全局作用域内定义的变量称为全局变量。
说明
全局对象的生命期开始于应用程序的运行,结束于应用程序的退出。
下面以定义全局变量为例,介绍全局对象的定义。全局变量的定义与局部变量的定义相同,只是在函数外部进行定义。例如:
对于全局变量来说,如果没有进行初始化,其存储区为0。因此,对于整型的全局变量,如果没有进行初始化,其值为0。但是,对于局部变量来说,如果没有进行初始化或赋值,其值是不可预见的。此外,在整个应用程序中,一个全局变量只能定义一次,不能重名。
如果在函数内部定义了一个与全局变量同名的局部变量,则全局变量被隐藏,如果需要访问全局变量,需要使用域运算符“::”。
【例4.21】 使用域运算符访问被隐藏的全局变量。(实例位置:资源包\TM\sl\4\11)
执行上述代码,结果如图4.9所示。
图4.9 访问隐藏的全局变量
说明
至于在当前文件中访问其他文件中的全局变量,可以使用extern关键字进行声明,可参考第2.2.5节。
4.2.3 定义和使用命名空间
在一个应用程序的多个文件中可能会存在同名的全局对象,这样会导致应用程序的链接错误。使用命名空间是消除命名冲突的最佳方式。命名空间的定义格式如下。
例如,下面的代码定义了两个命名空间。
【例4.22】 定义命名空间。(实例位置:资源包\TM\sl\4\12)
如果使用命名空间中的对象,需要在对象前使用命名空间名作为前缀。例如,下面的函数调用是非法的。
应使用命名空间作为前缀。上述代码应修改为:
如果需要访问同一个命名空间中的多个对象,可以使用using命令引用整个命名空间对象,这样就不必在每个对象前添加命名空间前缀了。例如:
using命令的作用域是从当前引用处到当前作用域的结束。如果将using命令放置在复合语句中,在复合语句结束时,using命令的作用域也随之结束了。例如,下面的函数调用是非法的,因为using命令的作用域已经结束了。
如果在函数中定义的局部变量与命名空间中的变量同名时,命名空间中的变量将被隐藏。例如:
上述代码中局部变量ivar隐藏了命名空间Output中的变量,因此第1个printf函数输出的值为5。为了访问被隐藏的Output命名空间中的变量ivar,需要使用命名空间作为前缀,如“Output::ivar”,尽管前文已经使用using命令引用命名空间Output。
如果程序中使用using命令,同时还引用多个命名空间,并且命名空间中存在相同的函数,将导致歧义,出现编译错误。例如:
解决的方法是必须使用具体命名空间进行区分。例如:
对于同一个命名空间,可以在多个文件中定义。此时,各个文件中的对象将处于同一个命名空间中。例如,在main.cpp头文件中定义了一个命名空间Output。
【例4.23】 在多个文件中定义命名空间。
在login.cpp头文件中还可以定义Output命名空间。例如:
此时,命名空间Output中的内容为两个文件Output命名空间内容的“总和”。
注意
如果在login.cpp文件的Output命名空间中定义一个整型变量ivar将是非法的,因为它与main.cpp文件中Output命名空间中的变量同名。
在定义命名空间时,通常在头文件中声明命名空间中的函数,在源文件中定义命名空间中的函数,将程序的声明与实现分开。例如,在头文件中声明命名空间中的函数。
在源文件中定义函数。
在源文件中定义函数时,注意要使用命名空间名作为前缀,表明实现的是命名空间中定义的函数,否则将是定义一个全局函数。
类似于复合语句,命名空间也可以嵌套。例如,下面的代码定义了一个嵌套的命名空间。
【例4.24】 定义嵌套的命名空间。
在上述代码中,Windows命名空间中又定义了一个命名空间GDI,如果在程序中要访问GDI命名空间中的对象,可以使用外层的命名空间和内层的命名空间作为前缀。例如:
用户也可以直接使用using命令引用嵌套的GDI命名空间。例如:
在上述代码中,“using namespace Windows::GDI;”语句只是引用了嵌套在Windows命名空间中的GDI命名空间,并没有引用Windows命名空间,因此试图访问Windows命名空间中定义的对象是非法的。例如:
注意
using命令用于引用一个命名空间中所有的命名空间对象,容易和正在编写的程序中的局部变量发生冲突,导致命名空间中的变量被隐藏,所以不建议过多地使用using命令。
在使用namespace关键字定义命名空间时,也可以不指定命名空间的名称,此时的命名空间称为未命名的命名空间。
【例4.25】 定义未命名的命名空间。
对于未命名的命名空间来说,其最大特点就是命名空间中的对象只适用于当前文件,各个文件之间不能相互访问定义在各自未命名空间中的对象。这一特点使得未命名空间中的对象与全局静态(static)对象(对于全局静态对象来说,只属于半个全局对象,它只能在当前文件中使用)所起的作用相同。多数C++编译器都支持命名空间,使得越来越多的开发程序更愿意使用未命名的命名空间来代替全局静态对象。对于未命名的命名空间来说,访问其中定义的对象与访问普通的全局对象是相同的。例如:
注意
在定义未命名的命名对象时,注意其中的对象不能与全局对象同名或相同(函数可以重名,表示函数重载,但是参数列表不能完全相同)。
例如,下面的代码是非法的。
在上述代码中,全局函数PutoutText与未命名空间中的PutoutText函数声明格式(函数原型)相同,导致编译器产生了歧义。如果将未命名空间定义为有名称的命名空间(如namespace windows),则不会出现错误。