cpp函数(二):生命周期及作用域

2019-05-23  本文已影响0人  浩波千里

作者邮箱:z_zhanghaobo@163.com
github相关: https://github.com/HaoboLongway/simple_examples

以下讨论不只限于函数中变量的声明周期于作用域
我们可以用存储期来描述对象,所谓存储期表示对象在内存中保留了多长时间。
标识符用来访问对象,可以用作用域链接描述标识符,标识符的作用域和链接表明了程序中的哪些部分可以使用它。不同的存储类型有着不同的存储期,作用域以及链接。

  1. 作用域
    一个C++变量得到作用域可以是块作用域,函数作用域,函数原型作用域或文件作用域。
    1. 是用一对花括号{}括起来的代码区域,例如,整个函数体是一个块,而一个复合语句也是一个块。块作用域的可见范围是从定义处到包含该定义的块的末尾。下面我们看一个例子
...
void main()
{
    blocky();
}
void blocky()
{
    {
        int i=1;
        {
            cout<<"i = "<<i<<endl;
            {
                int q=1;
                cout<<"q = "<<q<<endl;
            }
            cout<<"q = "<<q<<endl;    // There's a mistake, can you see it?
        }
        cout<<"i = "<<i<<endl;
    }
    cout<<"i = "<<i<<endl;     // There's a mistake too
}

上面的代码实际是不能运行的,注意到其中加有There's a mistake的注释了吗,实际上编译器会抛出这样的错误信息:

error: 'q' was not declared in this scope|

由于编译器顺序编译,所以第二处同样的错误并没有被发现,我们关注到变量q是在最里层被定义的,而报错的一行则在花括号外使用了该变量,显然这超出了q的作用域,难怪这里会出错
下面我们可以改正这个程序,把错误的两处删去,输出结果为

i = 1
q = 1
i = 1

与我们的预期一致

那么如果内层块与外层块变量同名会怎么样?这里内层块会隐藏外层块的定义,但是离开了内层块后,外层块变量的作用域又回到了原来,看下面的例子

...
int main(){
    int x=30;    //primitive
    cout<<"x in outer block:"<<setw(30)<<x<<" at "<<&x<<endl;
    {
        int x=77;
        cout<<"x in inner block:"<<setw(30)<<x<<" at "<<&x<<endl;
        x += 33;
        cout<<"x in inner block after certain operation:"<<setw(6)<<x<<" at "<<&x<<endl;
    }
    cout<<"x in outer block again:"<<setw(24)<<x<<" at "<<&x<<endl;
}

输出为

x in outer block:                            30 at 0x6dfeec
x in inner block:                            77 at 0x6dfee8
x in inner block after certain operation:   110 at 0x6dfee8
x in outer block again:                      30 at 0x6dfeec

符合前面提及的规则

现在根据C99标准,for循环,while循环,do while循环和if语句所控制的代码即使没有用花括号括起来,也算是块的一部分,比如下面的例子

... //Let's suppose there's no variable i before.
for(int i=1;i<11;i++){
    i ++;
}
cout<<i;    //A mistake, out of scope!!!

是非法的。

  1. 函数作用域 函数中的变量是局部变量,它们无法被外部访问,函数作用域仅用于goto语句的标签,如果在两个块中使用相同的标签会很混乱,标签的作用域防止了这样事情发生。不过事实上,我们很少使用goto语句,使用goto语句也不是什么好主意,所以这一点仅做了解。
  2. 函数原型作用域 用于函数原型中的形参名,其作用范围是从形参定义处到原型声明结束,这意味着编译器在处理函数原型中的形参时只关心它的类型,如下面的例子只有在变长数组中形参名才会有用:
void use_a_VLA(int n, int m, ar[n][m]);

不过我们并不多接触变长数组,因此基本上也仅作了解

  1. 文件作用域中的变量,从定义处到该定义所在文件的末尾均可见,如
#include <iostream>
int global_var = 1;
int main(){
    cout<<global_var;
    ...
}

这里的global_var具有文件作用域,文件作用域变量也被称为全局变量

  1. 存储期
    一般变量的存储是自动的,对于函数来说,如果函数执行完毕,如果没有特殊情况,其中的变量就会“死亡”,也就是它们的地址不再有效。
    不过所谓的静态变量在存储上有所不同,静态的意思是该变量在内存中原地不动,静态变量又可分为块作用域 和 外部链接 两种。
    1. 块作用域的静态变量
      看下面的例子:
int main()
{
    for(int i=0;i<10;i++){
        cout<<"Times called self_add: "<<i<<setw(20)<<"Return: "<<self_add()<<endl;
        cout<<"Times called self_add_static: "<<i<<"     Return: "<<self_add_static()<<endl;
        cout<<"------------------\n";
    }
    return 1;
}
int self_add(){
    int counter=0;
    return ++counter;
}
int self_add_static(){
    static int counter=0;
    return ++counter;
}

注意这里self_add()函数中的变量counter是通常的,它的生命周期即是函数作用的周期,当主调函数结束对self_add()的调用后,counter即被销毁;与此不同的是,我们使用static关键字指出一个变量是静态的,静态变量在该函数首次调用时被初始化且只初始化一次,当self_add_static()函数执行完毕后,它的counter变量仍会保留,所以我们看到输出结果是下面这样:

Times called self_add_static: 1     Return: 2
------------------
Times called self_add: 2            Return: 1
Times called self_add_static: 2     Return: 3
------------------
Times called self_add: 3            Return: 1
Times called self_add_static: 3     Return: 4
------------------
...

另一方面,我们还可以看到这两个函数中同名变量互不影响。

  1. 外部链接的静态变量
    这里主要想说明外部变量具有静态存储期,例如下面的例子
int Whole_bunch;    //外部定义的变量
double Up[100];     //外部定义的数组
extern char stars;     //如果要使用外部文件中的stars变量,则需要这样声明

其中Whole_bunch, double Up, stars都是全局性的(从作用域考虑),且有静态存储期。
当我们在该文件某处,如use_it()函数中使用时,我们可以直接使用,也可以使用extern关键字显式声明,表明这里使用的Up[]与全局变量中的Up[]一致,像是下面的示例

int use_up_array(void){
    extern double Up[];
    ...
}

当然,如果我们不作extern开头的声明,也可直接使用Up[]数组,因其是全局变量.


其他文章

cpp函数:认识函数
cpp函数:生命周期与作用域

上一篇下一篇

猜你喜欢

热点阅读