C++ 的存储方案决定了变量保留在内存中的时间,也决定了程序的哪一部分能够访问它。本文就来聊聊 C++ 变量存储方案的那些事。
1. 综述
1.1 存储持续性 (Storage Duration)
自动存储持续性 (Automatic)
在函数定义中声明的变量属于此类,如下变量 a, b:
int main() { int a; int b = 1; return 0; }
静态存储持续性 (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; }
动态存储持续性 (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++编译器中是以栈的方式实现的。
栈是一种后进先出(最后进入的数据,最先被取出)的数据结构,它和筒装羽毛球的取放方式类似。
请参考以下对栈的操作与具象的图表来理解它:
- 创建空栈
栈顶 |
---|
栈底 |
- 放入数据“100” (Push)
栈顶 |
---|
100 |
栈底 |
- 放入数据“50” (Push)
栈顶 |
---|
50 |
100 |
栈底 |
- 取出数据“50” (Pop)
栈顶 |
---|
100 |
栈底 |
- 取出数据“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. 总结
回顾全文,我们了解到:
C++的存储方案决定了变量保留在内存中的时间(存储持续性)
也决定了程序的哪一部分能够访问它(作用域与链接性)
单变量原则
栈的原理
C++11 列表初始化
五种变量存储方式,如下表:
存储描述 | 持续性 | 作用域 | 链接性 | 声明方式 |
---|---|---|---|---|
自动存储 | 自动 | 代码块 | 无 | 代码块中 |
静态外部 | 静态 | 代码块 | 外部 | 代码块外 不用static |
静态内部 | 静态 | 当前文件 | 内部 | 代码块外 用static |
静态无链接 | 静态 | 当前文件 | 无 | 代码块中 用static |
动态存储 | 动态 | 动态 | 动态 | 用new, delete |
本文作者:以成
本文链接:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 。转载请注明本文作者与链接!