C++ 的存储方案决定了变量保留在内存中的时间,也决定了程序的哪一部分能够访问它。本文就来聊聊 C++ 变量存储方案的那些事。

1. 综述

1.1 存储持续性 (Storage Duration)

  1. 自动存储持续性 (Automatic)

    在函数定义中声明的变量属于此类,如下变量 a, b:

    int main() {
        int a;
        int b = 1;
        return 0;
    }
  2. 静态存储持续性 (Static)

    在函数定义外声明的变量属于此类,如下变量 a, b:

    int a;
    int b = 1;
    int main() {
        return 0;
    }

    使用 static 声明的变量属于此类,如下变量 a, b:

    static int a;
    int main() {
        static int b;
        return 0;
    }
  3. 动态存储持续性 (Dynamic)

    使用 new 分配的内存属于此类,如下变量 a:

    int main() {
        int *p = new int a;
        return 0;
    }

1.2 作用域 (Scope)

  • 作用域,顾名思义是一种作用范围
  • 其代表了一个变量的可用区域。

如下变量 a 的作用域为当前函数内(仅在函数 Now 内):

void Now(int a) {

}
int main() {
    return 0;
}

如下变量 a 的作用域为当前整个文件内

static int a;
int main() {
    return 0;
}

如下变量 a 的作用域为所有包含该定义的声明的文件内(稍后将详细说明):

int a;
int main() {
    return 0;
}

1.3 链接性 (Linkage)

  • 链接性指一个变量如何在不同区间相互共享。
  • 链接性包含外部链接性内部连接性无链接性

2. 自动存储

2.1 默认情况

在默认情况下,在函数中声明的函数参数变量存储持续性自动,作用域为局部无链接性,如:

#include <iostream>
using namespace std;
void OutPut() {
    int a = 2;
    cout << a << endl;
}
int main() {
    int a = 1;
    OutPut();
    cout << a << endl;
    return 0;
}
运行结果:
1
2

作用域为局部,体现在函数 OutPut 与 函数 main 中的变量 a 相互独立,互不干扰。

无连接性,体现在变量 a 只能在它的函数内部使用,而不能在其它函数中使用。

另外,当程序开始执行变量所属的代码时,将为其分配内存,当函数结束时,这些变量都将消失。

2.2 代码块中的变量

若在代码块中定义了变量,则该变量的作用域就是其所在的代码块,如:

#include <iostream>
using namespace std;
int main() {
    int a = 1;
    {
        int a = 2;
        cout << a << endl;
    }
    cout << a << endl;
    return 0;
}
运行结果:
2
1

如上所示,在代码块外的变量 a 与在代码块内的变量 a 也是相互独立,互不干扰的。也可以这么说,代码块内的变量 a 在代码块中暂时覆盖了代码块外的变量 a 的定义。

2.3 自动变量与栈

事实上,自动存储持续的变量在C++编译器中是以的方式实现的。

是一种后进先出(最后进入的数据,最先被取出)的数据结构,它和筒装羽毛球的取放方式类似。

653d608e8d31c

请参考以下对的操作与具象的图表来理解它:

  1. 创建空
栈顶
栈底
  1. 放入数据“100” (Push)
栈顶
100
栈底
  1. 放入数据“50” (Push)
栈顶
50
100
栈底
  1. 取出数据“50” (Pop)
栈顶
100
栈底
  1. 取出数据“100” (Pop)
栈顶
栈底

不难理解,我们只能从栈顶往下放入一条数据,也只能从栈顶取出最顶端的一条数据。就像我们只能打开筒装羽毛球的盖子,从上面拿出(或倒出)一只球,或从上面放入一只球一样。

回到正题,当文件编译时,编译器会对每一个代码块都取一段连续的内存,并视其为该代码块的变量,以管理自动变量

当程序尝试在一个代码块中创建一个新自动变量时,新的变量就会被放入这个变量中。就像刚刚演示的一样,申请一个,便会放一个。

当程序不再使用该变量时(通常是程序执行到了该变量的作用域外),该变量就会被从中取出并删除(通常是将整个栈清空)。

这样,我们便不难理解在自动存储中,代码块(或函数)之间的变量为什么能互不干扰了。

3. 静态存储

3.1 链接性综述

在 1.3 ,我们曾提出过静态存储的三种链接性:

  • 外部链接性:允许在当前文件访问该变量,可通过声明允许在其他文件(非当前文件)访问该变量
  • 内部链接性:只允许在当前文件访问该变量
  • 无连接性:只允许在当前代码块中访问该变量

然而无论是以上的哪种链接性,只要是静态存储的变量,其寿命必定比自动存储的变量长。因为静态变量从程序的运行至结束都一直存在于内存中,而不会受到诸如之类的限制。

另外,如果没有显式地初始化静态变量,编译器会默认将其设置为0。

下面介绍如何创建这三种不同链接性静态存储变量

  • 外部链接性:在代码块外声明变量,且不使用 static。
  • 内部链接性:在代码块外声明变量,且使用 static。
  • 无连接性:在代码块内声明变量,且使用 static。

如下所示。具有外部链接性的变量 a,具有内部链接性的变量 b,具有无链接性的变量 c:

int a = 1;
static int b = 2;
int main() {
    static int c = 3;
    return 0;
}

3.2 外部链接性

事实上,C++提供了两种变量声明方式:

  • 定义声明(简称定义):不加任何关键字可选择性地初始化变量,编译器变量分配存储空间
  • 引用声明(简称声明):添加关键字 extern不可初始化变量,编译器不给变量分配存储空间

在默认情况下,具有外部链接性静态存储变量,作用域只限于当前文件。但也可以在其它文件通过声明以在其它文件使用该变量。如:

//file1.cpp
int a = 1; //注:此处是定义声明,可选择性地初始化该变量
//file2.cpp
#include <iostream>
using namespace std;
extern int a; //注:此处是引用声明,不可初始化该变量
int main() {
    cout << a << endl;
    return 0;
}

同时编译 file1.cpp 与 file2.cpp,并运行,得出:

运行结果:
1

需要注意的是,C++有“单定义原则”,即每个变量只能定义一次。

因此,在多个源文件的程序中,在除原始定义的变量 a 以外的其它文件声明变量 a 时,不添加 extern 就会引起报错。因为其违反了“单定义原则”。

3.3 内部链接性

尝试阅读如下代码:

//file1.cpp
int a = 1; //定义声明
//file2.cpp
#include <iostream>
using namespace std;
static int a; //未被显式赋初始值的具有内部链接性的静态变量
int main() {
    cout << a << endl;
    return 0;
}

思考一下,同时编译 file1.cpp 与 file2.cpp 时,会报错吗?

显然,并不会。因为 file2.cpp 中的变量 a 是一个具有内部链接性静态存储变量。它并不能在外部文件中被引用

我们也不难猜到:

运行结果:
0

这是因为,file2.cpp 的变量 a 被静态定义了,其作用域仅限于 file2.cpp 这整个文件。同时,未被显式赋初始值时,变量 a 将被默认赋值为 0。因此,在输出时,变量 a 的值便是 0。

3.4 无链接性

在 3.1 提到过,无链接性静态变量只能在函数或代码块内使用。接下来,我们再更仔细地讨论一下静态变量的特点。

静态变量在程序运行时就已经被定义,直到程序允许结束。

这也就说明,一个静态变量在其所处的代码块不处于活动状态时,它依旧是存在的。

而且,一个静态变量只会在程序启动时被初始化。即使以后再次调用到变量所处的定义行,该变量也不会被重新初始化。如:

#include <iostream>
using namespace std;
void Now() {
    static int a = 1;
    cout << a << endl;
    a++;
}
int main() {
    Now();
    Now();
    return 0;
}
运行结果:
1
2

如上,在第二次调用函数 Now 时,变量 a 不会被重新赋值为 1,因此其输出了变量 a 自增的结果—— 2。

4. 动态存储

4.1 动态存储综述

上文讨论了许多作用域链接性的相关知识,而接下来将要讨论的动态存储与这些完全无关。因为动态存储完全由运算符 new 和 delete 控制。

要注意的是,一旦使用运算符 new 从内存中分配了空间,它将会一直被保留在内存中,直到它被使用 delete 归还给系统内存。当然,倘若该空间一直到程序运行结束都还未被显式地归还给内存,则其将被自动归还

4.2 动态存储与初始化 (C++98)

对于标量类型数据(如 int, float),可以在类型名后加上初始值,并用圆括号括起。如:

#include <iostream>
using namespace std;
int main() {
    int *pa = new int (20); //给动态变量 pa 初始化
    double *pb = new double (22.5); //给动态变量 pb 初始化
    cout << *pa << endl << *pb;
    return 0;
}
运行结果:
20
22.5

对于有构造函数的类,也是如此。如:

#include <iostream>
using namespace std;
class MyClass {
    int a_;
    double b_;
public:
    MyClass(int a, double b) {
        a_ = a;
        b_ = b;
    }
    void OutPut() {
        cout << a_ << endl << b_;
    }
};
int main() {
    MyClass *pA = new MyClass (20, 22.5); //给动态变量 pA 初始化
    pA->OutPut();
    return 0;
}
运行结果:
20
22.5

4.3 列表初始化 (C++11)

C++11 提供了新的做法:列表初始化。若编译器支持C++11,则可以通过列表初始化来给变量初始化,其做法是用花括号括起初始值,如:

#include <iostream>
using namespace std;
int main() {
    int *pa = new int {20}; //用花括号给动态变量 pa 初始化
    double *pb = new double {22.5}; //用花括号给动态变量 pb 初始化
    cout << *pa << endl << *pb;
    return 0;
}
运行结果:
20
22.5
#include <iostream>
using namespace std;
class MyClass {
    int a_;
    double b_;
public:
    MyClass(int a, double b) {
        a_ = a;
        b_ = b;
    }
    void OutPut() {
        cout << a_ << endl << b_;
    }
};
int main() {
    MyClass *pA = new MyClass {20, 22.5}; //用花括号给动态变量 pA 初始化
    pA->OutPut();
    return 0;
}
运行结果:
20
22.5

同样,C++11 的列表初始化,还支持给具有自动存储持续性变量赋初始值。如:

#include <iostream>
using namespace std;
int main() {
    int a {5}; //用花括号给自动变量 a 初始化
    cout << a;
    return 0;
}
运行结果:
5

5. 总结

回顾全文,我们了解到:

  1. C++的存储方案决定了变量保留在内存中的时间(存储持续性

    也决定了程序的哪一部分能够访问它(作用域链接性

  2. 单变量原则

  3. 栈的原理

  4. C++11 列表初始化

  5. 五种变量存储方式,如下表:

存储描述 持续性 作用域 链接性 声明方式
自动存储 自动 代码块 代码块中
静态外部 静态 代码块 外部 代码块外 不用static
静态内部 静态 当前文件 内部 代码块外 用static
静态无链接 静态 当前文件 代码块中 用static
动态存储 动态 动态 动态 用new, delete