当前课程知识点:基于Linux的C++ > 第五讲 程序组织与开发方法 > 5.5 作用域与生存期 > LinuxCPP0505
第三个部分就是作用域和生存期
同学们了解了这两个概念呢
就对理解程序代码会非常非常有用
先来看量的作用域和可见性
作用域就是标识符的有效范围
定义了一个标识符 那么这个标识符
从它的定义处开始到语句块的结束
就叫标识符的有效范围
这个就叫作用域
跟它相关的概念称为可见性
可见性就是我可以看见它
程序中的某一个位置
能不能够使用标识符
就叫标识符的可见性
需要特别注意的地方是什么呢
对于任何标识符来说
只能在作用域内可见
但是在它的作用域内
标识符不一定可见 特别注意这一点
所以说作用域和可见性紧密相关
但是并不一样
局部数据对象
定义在一个函数或一个复合语句块内
具有块的作用域
它的作用域就从它的定义处开始
到这个语句块的结尾
就是因为这一点
不同的函数可以定义同名的变量
比如说Add这个函数
里面可以定义变量t
然后Compare函数里面还可以定义变量t
Swap函数里边还可以定义变量t
都没有问题
就是因为作用域它是不一样的
你在你的空间里 它在它的空间里
两者是不交叉的
如果是在一个函数内
想定义同名的变量 那肯定就不行了
我们来看这样的例子
一个函数func带两个参数x和y
返回值是int
在这个函数内部定义局部变量t
然后把t赋值为x + y
注意后面又来了一个
孤零零的花括号对
它前面也没有if 也没有switch
也没有for 也没有while 没关系
就孤零零一个复合语句块 可以
c允许你这么写 C++还允许你这么写
那么它就给你构造了一个内部的
复合语句块
int n初始化成2
n作为一个局部变量
在一个内部的嵌套的复合语句块里
所以作用域就从定义处开始
到语句块结尾 也就是cout << n
这一条语句做完
那个n值就无效了
t呢从定义处开始一直到return t
整个这个函数内部你都可以用
这就是局部数据对象的作用域
另外一个方面就是全局数据对象
一旦把这样的量定义在任何一个函数
或复合语句块之外 就是全局量
所有的全局量都具有文件作用域
它的有效性就从定义处开始
一直到文件结尾
中间任何函数都可以用它
所以称它为具有文件作用域
有时候也称全局作用域
有一点需要同学们特别注意
如果有这样的全局量
定义在特定的文件里
而这个文件被别人包含了
那么作用域就会扩展出去
谁包含了它
它的作用域就会扩展到
包含它的宿主文件里面去
所以不要在头文件中
定义全局数据对象
因为放在头文件里
非常容易被别人包含
一被别人包含作用域就会扩展过去
哪怕你真需要 现在也不能这么写
后面会告诉你怎么写才对
因为被别人一包含 作用域就过去了
编译器十之八九会报错
因为不允许定义同名的全局变量
在你的整个工程项目里面
不允许定义同名的全局变量
哪怕这个全局变量在这个文件里
另外一个全局变量
是在另外一个文件里
它们不允许同名
接下来是函数原型作用域
写了一个函数原型
函数原型里有一些参数
这些参数具有函数原型作用域
也就是 有效性就从定义处开始
一直到函数原型结束
这就叫函数原型作用域
它不会延展到函数所对应的
实现里面去
所以这就意味着函数原型里面
形式参数的那个名字
和函数真正实现的时候
那个参数的名字可以不一样
我们来看这个例子
同学们一旦理解了这个例子
那么作用域和可见性这个概念
就算基本掌握了
先看第1行 int i
i这个变量定义在任何一个
函数或语句块之外 所以它是全局量
有效性就从定义处开始
第1行代码到文件结尾
也就是第23行 可见性不一定
第2行定义了一个函数原型func
从第3行一直到第10行是main函数
第5行在main函数里边
定义了一个局部量n
那么作用域就从定义处开始
一直到main函数结尾
然后i初始化成10
这是第6行
访问的就是第1行定义的i
全局变量i
然后cout输出 输出i和n的值
n当然就是第5行n
i还是第1行的i 然后n赋值为func(i)
i作为实际参数调用一次func
返回值给n 这也没有问题
再输出i、n
还是访问的局部量的n和全局量的i
然后在第11行定义了全局量n
那么作用域就从它的定义处开始
一直到文件结尾
第12行开始定义func函数
第14行i赋值为0
访问的当然还是全局量
然后cout输出i、n
i还是全局量
n当然是在第11行定义的全局量的n
如果你没有对它进行初始化
所有的全局量都自动地初始化为0
第16行n赋值为20 没有问题
第11行的n被赋值了20
在第17行和第20行之间
是新的复合语句块
第18行int i初始化为n + x 这个可以
注意n是第11行的全局量n
i就是它自个儿
第18行里定义的这个i
x是func函数它的形式参数
第12行出现的x
cout输出 i就是第18行的i
n就是第11行的n
在这个位置
事实上有两个i可供你使用的
一个是局部量的i 一个是全局量的i
第一行i的作用域
包括了第19行代码
第18行定义的i
它的作用域是一直到第20行的
那么这个时候
就会产生一个重要的问题
就是两个不同的量
在这一行上都是可以访问的
那你说这个i访问到底是哪一个i啊
作用域它都在嘛 那就体现一个点
可见性
第一行定义的全局变量i
作用域很长
第19行在这个作用域里面 不可见
原因就在于它被一个新定义的
作用域更小的局部量i覆盖了可见性
这是非常非常重要的一个地方
在这里 第19行访问的i
就是第18行的i
绝不是第1行的i
你可能就会说
那么有没有什么招
第19行里就想访问第1行的i呢
在C++里面提供了一个“::”操作符
叫做全局解析操作符
写一个 ::i
那么访问的就是全局量的i
单写i访问的就是第18行的i
这个叫全局解析
接下来就是存储类和生存期
生存期是量在程序中存在的时间范围
通俗地讲就是这个量能活多长时间
C程序和C++程序都使用存储类
来表达生存期
刚才讲作用域、可见性
表达的是量的空间概念
现在讲存储类和生存期
表达量的时间概念
C和C++里面使用存储类来表达生存期
也就是量在程序中存在的时间范围
一般来讲
这个C/C++代码里面的量具有两类生存期
一个称为静态生存期
一个称为自动生存期
静态生存期有时候也称为全局生存期
全局数据对象一般都具有静态生存期
也就是说它的生生死死
就只和程序是不是运行有关
程序一运行它就出生了
程序一结束它就死掉
整个生存期是跨越程序始终的
在整个程序运行期间它都是有效的
这是非常非常重要的一个概念
第二个就是自动生存期
称它为局部生存期
它的生生死死 只和这个程序
有没有进入这个语句块有关
程序代码进入语句块
在语句块内部定义的局部量
就算出生了
离开了这个局部块 局部量就死掉了
执行一次
这样的局部量就会出生一次 死一次
出生一次 死一次
就是按照这样方式来进行运作的
所以程序每次进入这样的语句块里边
它创建的静态局部量都是不一样的
虽然它们叫同样的名字
但事实上是两个不同的东西
每次进入语句块就会为这样的局部量
分配一个存储空间
每次离开就会销毁它的存储空间
释放内存
所以从某种程度上来讲
套用过去富有哲理的一句话
叫做“人不能两次踏进同一条河流”
两次进入语句块的时候
使用的量不是同一个
接下来呢
有一个非常非常特殊的关键字
称它为static
它用于修饰局部变量的时候
会让局部变量具有静态的生存期
本来嘛 标准的局部变量
具有自动的生存期
可是前面加了static关键字之后
就会让它变成静态生存期
程序在退出块的时候
这样的量并不会消失
当程序下一次进入块的时候
将会使用原先的量继续参与运算
并且使用上一次退出块的时候
设定的值
因为static修饰的局部变量的生命
就像全局量一样得长
它的初始化动作
必须做一些特别的规定
必须进行初始化
静态局部量只改变生存期
不改变作用域
有效性仍然局限在那个地方
所以说生命被拉长了
但是存在的空间仍然没有发生变化
如果用static去修饰全局量
那么意义 就和修饰局部量完全不一样
已经是全局量了
再用static关键字
来拉长它的时间特性有意义吗
用static修饰全局量的时候
不表示将生存期拉长
而表示限定作用域
只在本文件内部使用
其它文件不可见
先来看静态局部量的例子
有一个主函数 还有一个func
在这个func函数里边定义静态局部量count
把它初始化成0 然后输出count值
递增count值之后return
在主函数里边写for循环
然后输出 调用func函数
count就是计数器
记录func被调用了多少次
局部量是没用的
每次运行count 值都会初始化成0
但是静态局部量写在这个地方
初始化为0
那也不意味着每次调用func
都会初始化成0
这个静态局部量看上去就像全局量一样
初始化在程序启动前就做好
调用main函数之前
count就会被初始化成0
所以进入func函数的时候
这个count值变成了1
第2次进入的时候
它将在1的基础上继续递增成2
第3次就会继续递增成3
所以可以来记录func被调用了多少次
如果没有静态局部量技术
定义一个全局量在外面不就可以了吗
但是定义成全局量
就没有和func关联起来
现在有了静态局部量
事实上可以把count这个量写在函数里
这一点和刚才那肯定不一样
这样程序设计逻辑要合理得多
对于一个函数
它也有自己的作用域和生存期
所有的函数都具有文件作用域
和静态生存期
也就是说 这些函数有效范围尽可能大
时间尽可能得长
可有的时候还想写一些函数
就想自己用 不想给别人用
哪怕写一个库
库里面提供了一系列的接口
写在库对应的头文件里 你就可以用
但是我还为了这些库
内部还实现了自己的一些函数
外面接口已经够你用的了
剩下这些函数不想给你用
就可以把这些函数定义成内部函数
函数分成内部的和外部的两个部分
内部函数就是不可以被其它文件用的
外部函数就可以被其它文件中的函数
所调用的
缺省情况下 所有的函数都是外部函数
具有文件作用域和静态生存期
要想让它变成一个内部函数
要在这个函数前边加上static关键字
它就是内部函数
不能再把它写在这个库
所对应的头文件里
特别需要注意
当我们在创造、引入一个类型、
一个量的时候
有两个概念 一个叫声明 一个叫定义
这两者不是一回事
定义是在程序中产生新的实体
而声明呢我们不创造它 只是引入它
这个实体可能是别人替我们创造的
可能是我们在另外一个文件中创造的
声明和定义是不一样的
对于函数来讲
声明就是给出函数的原型
定义就是给出函数的编码实现
如果产生一个新的类型
那就叫定义
如果新的类型没有被产生
那就是声明
定义一定会构造出新的型来
而声明是没有构造出来
看我类型定义的例子
typedef enum __BOOL { FALSE, TRUE }
后面跟着BOOL
一个新的类型 名字叫enum __BOOL
这就是新的枚举型
有两个可能的取值 FALSE和TRUE
这个类型的构造是清晰的、完整的
后面又用typedef把它定义成新的型BOOL
等价于enum __BOOL
它的型也是完整的
所以就创造了一个新的类型
那你看下面这一行
enum __BOOL创造了一个新的类型吗 没有
这个类型写在这个地方
定义是不完整的 后面缺了花括号对
没跟着枚举文字的序列就不全
意味着这一条语句仅仅给出了类型
enum __BOOL一个声明
而没有给出它的定义
接下来回到前面遗留的问题
如果是个全局量 加上static
那就表示别人不能用了
不加static 全局量别人能用吗
在一个文件里面定义了它
工程项目的另外一个文件能够用它吗
是不能的
本身就是一个程序
只是为了便于组织分成很多个文件
这个量整个程序里边都要用到它
那这个量定义在一个文件里
另外一个文件不能用
程序咋写啊
就把这个量写在头文件里
不行 千万记得不要在头文件里
定义全局数据对象
工程项目可能有很多个源文件
每一个源文件都可能#include头文件
只要头文件里面定义全局变量
那么工程项目中
每一个包含头文件的文件
都会把变量的定义写进去
这就意味着工程项目中的好几个文件
都会定义同名的全局变量
编译器是通不过的
它会认为你这是错的
想要这个量跨文件能够使用
又不能够重复定义
那怎么办呢 有一个技巧就在这里
这个关键字叫extern
要用它才能解决这个问题
你想定义这个量
那么你就把这个量定义在库的源文件里
然后在头文件里写上“extern int a;”
就表示这条语句将变成变量的声明
而不是变量的定义
它将把整型变量a导出
所有包含头文件的文件
就自动包含了“extern int a;”这一行
就表示它会将一个整型变量a
导入到它的空间里边去
凡是包含了这个头文件的文件
就可以使用整型变量a了
使用的就是在这个库的源文件里定义的a
解决了变量的重复定义的问题
所以同学们一定要会用这个关键字extern
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口