• C++变量作用域、生存期、存储类别


    写C、C++代码的小伙伴一定在头疼变量的作用域、生存期、存储类别问题。什么静态、外部、寄存器、局部、全局搞得一头雾水。今天咱们就来梳理一下他们的变态关系(什么不得了的事情???

    1、变量的作用域

    说白了,作用域就是一个”代码块“,也就是大括号包裹的那一段东西。包括函数体、控制语句块这些。大家应该都有所耳闻。

    #include<stdio.h>
    int x = 5; // 全局变量
    int main() {
        printf("%d ",x);
        int x = 6; // 局部变量
        printf("%d ",x); // 输出结果是5还是6?
        return 0;
    }
    

    这段代码算很经典了。它展示了不同定义位置的变量的作用域。

    首先一个输出肯定是5,毫无悬念。但是下面那个就不同了,因为在main函数中又出来一个同名的局部变量。C++遵循向上覆盖原则,在一个代码块的子块中定义的变量,覆盖掉原块中定义的变量。所以,下面那个输出应该是6。一旦在子块中定义了同名变量,这个块外部的变量就不可见了。所以,实际编程要避免这种情况

    但C++提供了一种叫做“作用域运算符”的东西,也就是::。这个运算符可以使得全局变量重新可见。比如我在x = 6定以后想再用全局变量的值,就可以这样写:printf("%d", ::x);这样输出就是5了。这个运算符大家应该都见过,在类和namespace中经常使用。但不管怎么说,除非是在class和namespace中,其它的情况应该严格避免内外作用域的变量重名

    说了这么多,作用域的概念给大家:作用域,就是从变量定义开始到所在代码块或文件结束为止,对编译器可见的范围

    如果对上面这些概念不理解,就往下看,局部变量和全局变量的概念。

    2、局部变量和全局变量

    局部变量,是指在一个函数内部定义的变量。它的作用域从定义(或声明)开始,到函数结束。它只对函数本身可见,对函数外部不可见。任何手段都无法访问函数内部的变量。因为除了main函数之外,其它函数都不是程序实体,它只提供了一个模块运行的模板,里面的变量只是为了这个函数服务,函数外部引用它没有意义。

    为了方便往下讲,先说一下生存期的概念。作用域是个静态概念,表示变量的可见范围,是对编译器而言的,而生存期是动态概念,表示变量在内存中的创建、使用、销毁过程,是运行时概念。生存期是指变量从创建到被操作系统回收的这一段时间。缩句的话,作用域是代码范围,生存期是时间范围。

    回到正题,局部变量的生存期,就是从函数创建它开始,到函数调用完毕,被操作系统回收这一段时间

    我们知道,一个进程有一段栈空间,函数调用的过程是用栈管理的。栈保存函数的参数、局部变量、返回地址。当一个函数被调用时,CPU首先把当前执行地址保存到一个特殊寄存器中,作为这一函数的返回地址。然后,CPU跳转到函数入口地址,栈顶指针扩展一帧,栈空间随之增长。把刚才的返回地址保存到栈中,并从栈中加载参数。之后就是执行过程。执行完毕后,从栈中取出返回地址,CPU跳回这一地址。最后栈顶指针回退一帧,这一函数的栈空间将被释放,局部变量也就随之销毁。

    上面这一段看不懂也没关系,如果你们学过汇编语言和操作系统,就会明白这是一个怎样的过程。总之,需要记住局部变量是在函数调用并执行到定义语句时创建,函数返回时销毁

    全局变量,是指不在任何函数内定义的变量。它定义在文件的顶层,对任何函数都可见(我们从现在起,假设任何函数中没有和全局变量重名的局部变量)。

    全局变量的作用域是从定义开始到这个文件结束。其实这种说法不精确,因为全局变量可以被其他文件调用,这就是外部变量,我们稍后再讲。

    和局部变量不同,全局变量是在进程创建(注意进程创建和main函数调用是两个概念,进程创建包括代码加载、数据加载、内存空间分配过程,是操作系统完成的,而main函数是进程调用的)时同时创建的。所以,全局变量在main函数调用之前就已经存在了。一个进程的地址空间分为代码段、数据段、用户段,代码段就是机器指令(参看冯诺依曼体系结构),数据段就是我们所说的全局变量,而用户段是供进程运行过程中动态分配内存的。我们刚才说的栈空间就在用户段。所以说,全局变量的存储位置和局部变量完全不同。

    全局变量又分为已初始化的和未初始化的。已初始化的全局变量,操作系统会自动为其初始化值,放在数据段前部,而未初始化的全局变量,则会放在数据段的后部,并自动清零。所以,你看到未初始化的全局变量初始值都是0。

    全局变量的生存期从进程创建开始,一直到进程运行完毕,所有内存被操作系统回收位置为止。

    另外,在不是函数的代码块中创建的局部变量,也是类似的。比如

    int s = 0;
    for (int i = 0; i < 10; i++) {
        s += i;
        int t = s;
    }
    printf("%d %d %d", i, s, t); // 错误,i和t只对for循环体可见
    

    3、变量存储类别

    说完了生存期、作用域的概念,我们再来看变量存储类别。

    变量的存储类别分为自动(auto),静态(static),外部(extern)和寄存器(register)四种。

    3.1、自动变量

    注意,虽然叫auto变量,但是,auto关键字在C++11中已经不再是“自动变量”的意思,而是“自动类型推断”。所以,不要试图用auto关键字来创建自动变量

    比如:

    auto a = 1;
    printf("%d
    ", a); // a的类型自动推断为整型
    auto int b = 2; // 错误,auto是指自动类型推断,不可以与类型标识符连用
    printf("%d
    ", b);
    

    我们刚才所说的变量,都是自动变量。所谓自动变量,就是按照操作系统的内存分配和回收规则来管理的变量,不需要程序员干预,动态管理。如果是局部变量,则由栈空间保存,全局变量则由数据段保存。

    3.2、静态变量和外部变量

    这个是难点中的难点,大家一定要仔细看。

    3.2.1、静态局部变量

    我们刚才说的局部变量,是自动局部变量,也就是说作用域和生存期是捆绑的,一旦出了作用域,则销毁,不再可见。而静态局部变量则不同。它和全局变量一样,是放在数据段中的,只初始化一次,下次调用这个函数时,保留原来的值,继续使用。如下:

    int count() {
        static int cnt = 0;
        return ++cnt;
    }
    

    如果cnt是个自动局部变量,则每次调用函数的时候,都要初始化为0,所以每次的返回值都是1。但定义成静态局部变量就不同了,它在函数调用结束后并不销毁,保留原来的值,下一次调用时,初始化语句是不起作用的,所以它返回的是函数被调用的次数。

    本质上,静态局部变量和全局变量的生存期完全相同,只是作用域不同。刚才说了,作用域是相对于编译器来说的,所以静态局部变量编译器提供的“语法糖”,为避免全局变量重名造成干扰而引入的机制。

    3.2.2、外部变量和静态全局变量

    在讲静态全局变量之前,我们来先讲一下外部变量。

    我们刚才说的全局变量,仅仅是对本文件可见吗?No,它对其他文件也可见。我们编译C++程序的时候,往往不止一个源文件,如果1.cpp要调用2.cpp的某个全局变量,怎么办呢?

    答案就是,使用extern声明。比如main.cpp的内容:

    #include<stdio.h>
    int a[20];
    void operate(); // 函数声明
    int main() {
        operate();
        for (int i = 0; i < 20; i++) {
            printf("%d ",a[i]);
        }
    }
    

    operate.cpp中内容:

    extern int a[20]; // extern声明
    void operate() {
        a[0] = 0;
        a[1] = 1;
        for (int i = 2; i < 20; i++) {
            a[i] = a[i-1] + a[i-2];
        }
    }
    

    编译命令:g++ main.cpp operate.cpp -o main.exe

    运行结果:0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181

    是不是很神奇?就像一个文件调用其他文件定义的函数一样,其他文件的全局变量也是可以使用的,只不过需要声明为extern。所以,外部变量是这样一个概念。把它类比成函数声明就行。

    说完了外部变量,我们来说静态全局变量。刚才说到,全局变量可以被其他文件所引用,如果我们不想让它被其他文件引用,怎么办?答案就是,把这个全局变量加上static修饰。这时,static已经不再是静态的概念,而是阻止其它文件调用的意思。所以,静态全局变量这个称呼太过直译,有点误导。正确的叫法是文件内变量。

    最初C++团队想增加一个intern关键字来表示这种变量,但是遭到广大程序员强烈反对,本着关键字能少即少的原则,就对static进行了“重载”,赋予了这么一个功能。其实我觉得还不如直接叫intern,static总是让人误会。

    static也可以修饰函数,这表示,其他文件不可调用这个函数。如果把operate.cpp中的operate()定义成static void operate(),则编译报错。

    3.3、寄存器变量

    说完了最难的,咱们来放松一下。寄存器是CPU的一些存储部件,它用来存放立即就要参加CPU运算的数据。它的读写速度是纳秒级别,比内存、缓存都快至少2~3个数量级。

    用register关键字声明的变量,叫做寄存器变量。表示,通知编译器把这个变量直接放在寄存器中而不是内存中,对一些频繁访问的变量,这样可以加快速度。但是,仅仅是通知编译器这样做,而编译器可能不会理会你的声明,而仍然把变量放在内存中。因为寄存器个数非常少,我们PC机的64位x86 CPU,只有8个通用寄存器,其中还有两个不是存放普通数据的。即使是寄存器较多的MIPS CPU,也只有32个。

    另外需要注意的一点,就是寄存器变量不可以使用取地址运算符&,也不可以用指针指向它。因为寄存器没有地址,指针只能保存内存地址值,而不能保存寄存器。

    4、类中的static

    4.1、静态字段

    类中的字段分为普通字段和静态字段。普通字段在类对象创建时被创建,它的生存期和类对象是捆绑的,同生共死。而静态字段,不依赖于对象创建,它被保存在数据段中。引用静态字段,可以用成员运算符,也可以用作用域运算符。

    class Point {
    private:
        int x;
        int y;
    public:
        static int cnt; // 其实这是不安全的,容易被篡改,应该设为private。但是作为一个例子来讲解静态字段
        Point(int xx, int yy) {
            cnt++; // 对当前拥有的对象个数计数
            x = xx;
            y = yy;
        }
        ~Point() {
            cnt--;
        }
    };
    static int Point::cnt = 0; // 静态变量初始化必须在类外进行,除非它是const的
    
    int main() {
        printf("%d ", Point::cnt); // 类名访问
        Point p(3, 4);
        printf("%d ", p.cnt); // 对象名访问
        Point *pp = new Point(5, 6);
        printf("%d ", pp->cnt); // 指针访问
        delete pp;
        printf("%d ", Point::cnt);
        return 0;
    }
    

    4.2、静态方法

    静态方法和普通方法不同,它也是所有类对象共享的。可以通过类名调用,也可以通过对象名调用。把刚才的类改一下:

    class Point {
    private:
        int x;
        int y;
        static int cnt;
    public:
        Point(int xx, int yy) {
            cnt++; // 对当前拥有的对象个数计数
            x = xx;
            y = yy;
        }
        static int getCnt() {
            return cnt;
        }
        /*
        static int getX() {
            return x; 
        }
        */ // 这个函数是错误的,静态方法不可引用非静态字段
        ~Point() {
            cnt--;
        }
    };
    static int Point::cnt = 0;
    
    int main() {
        printf("%d ", Point::getCnt()); // 类名访问
        Point p(3, 4);
        printf("%d ", p.getCnt()); // 对象名访问
        Point *pp = new Point(5, 6);
        printf("%d ", pp->getCnt()); // 指针访问
        delete pp;
        printf("%d ", Point::getCnt());
        return 0;
    }
    

    注意,静态方法不可访问非静态字段,也不可调用非静态方法。

    4.3、静态内部类

    内部类分为普通内部类和静态内部类两种。它们唯一的不同就是,普通内部类是外部类的“奴隶”,它不仅受制于外部类,而且一切字段不能对外部类隐藏,即使是private。而静态内部类则不同,相当于放在某个类内部的外部类,private字段是不可访问的。所以静态内部类也叫嵌套类。和Java是一样的。

    比如:

    class aaa {
    private:
        class bbb {
        private:
            int a;
        };
        static class ccc {
        private:
            int a; // a对外不可见
        };
    public:
        bbb bb;
        ccc cc;
        int bbbb() {
            return bb.a; // 正确
        }
        int cccc() {
            return cc.a; // 错误
        }
    };
    
  • 相关阅读:
    TXLSReadWriteII 公式计算
    Delphi TXLSReadWriteII2 带的demo中直接编辑XLS文件的例子
    delphi图片欣赏
    SQL 读取csv 文件批量插入数据
    Delphi TXLSReadWriteII 导出EXCEL
    Rollup 与 webpack的区别
    ref 属性使用eslint报错
    内容超出省略实现
    mac 环境配置
    前端学习资料整理
  • 原文地址:https://www.cnblogs.com/wancong3/p/10714451.html
Copyright © 2020-2023  润新知