提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
我们都知道C++和C语言一样都是基于堆栈结构设计的语言,Java虽然有虚拟机而且自动回收内存,他也是基于堆栈式结构设计的语言。当然,我们今天主要讨论C++的内存空间类型,只不过首先要了解它的语言结构。
C++中有3种类型的内存空间,接下来我们逐个解答。
除非显式声明为static
,在函数中定义的变量(包括函数的参数),存储在自动内存中(即,栈
中)。当函数被调用时,会为其分配
自动内存空间,当它返回时,这部分空间被释放
。
示例:
#include <iostream>
void test(int a){
int b = 0;
}
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
a和b都是自动内存,当test函数执行完毕,a和b的空间被系统自动回收
,不用手动回收
。
全局变量和名字空间中声明的变量存储在静态内存空间中,在函数和类
中声明的变量,如果显式声明为static
,也存储在静态内存
中。连接器在程序开始执行之前
分配静态内存空间。
示例:
#include <iostream>
int a = 0;
namespace WebCore {
int a = 0;
class Demo {
public:
static int a;
};
int Demo::a = 0;
void test() {
static int a = 0;
}
}
int main() {
std::cout << "Hello, World!" << std::endl;
}
上面所有的变量a都在静态内存里。
示例:
#include <iostream>
int *a = new int{1};
namespace WebCore {
int *a = new int{1};
}
void test() {
static int *a = new int{1};
delete a;//这个写法比较尴尬,出了这个作用域就访问不到a了,要不然就把指针引出去
}
class Demo {
public:
static int *a;
};
int *Demo::a = new int{1};//类内声明,类外定义
int main() {
std::cout << "Hello, World!" << std::endl;
std::cout << *::a << std::endl;
test();
std::cout << *WebCore::a << std::endl;
std::cout << *Demo::a << std::endl;
delete WebCore::a;
delete Demo::a;
delete ::a;
return 0;
}
上面的所有变量a都在自由存储里,使用完了必须delete,要不然会内存溢出
。上面的写法有几个地方不合理,但是这只是演示,不要太较真。
上面提到了程序开始执行之前
,很多人可能不能很好理解这个概念。如果换个说法可能就容易接受了。我们都知道任何C++程序
都需要定义main函数
,这是程序的起点函数
,也就是我们程序执行的第一个函数
。就是说静态内存的分配在main函数执行之前就完成了,当你在main函数里面使用静态变量的时候是已经初始化好的,这下容易理解了吧。
我们都知道C++是C语言的扩展
,当然是很成功的扩展。C++保留了很多(不是全部
)C语言的特性,让这两种语言混编几乎无任何阻碍(不支持的特性我用不到)。实际上,C语言malloc
和calloc
的内存也是堆内存
,当然也需要free释放掉
。
在C++中我们所说的手动回收
内存是值堆内存
,当然栈
是自动回收
的。但请记住,C++依然是一门手动回收内存的语言,不能因为栈自动回收你就认为它是自动回收内存!我们说Java是一门自动回收内存的语言是因为Java的堆内存也是自动回收的,而负责这件事情的就是垃圾回收器
,这个超出了这篇文章,感兴趣的请自行查阅。
C++设计之初是考虑过自动垃圾回收的,很多机构也基于这个接口实现了自己的垃圾回收器
。但是,作为一款定位手动回收内存的语言来说,这还是存在很多的挑战,最典型的例子就是数据转换的问题。我们都知道reinterpret_cast是完成两个不相干的数据格式之间的转换,这算是C++的一个魔幻特性了,但是不要小瞧它,有时候很有用喔!
考虑下面的代码:
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
int *p = new int{1};
std::cout << *p << std::endl;
long x = reinterpret_cast<long>(p);
p = nullptr;
//垃圾回收器时间
p = reinterpret_cast<int *>(x);
*p = 10;//这个时候p还在吗?
std::cout << *p << std::endl;
delete p;
}
正常情况下应该打印1和10。但是,如果p = nullptr之后垃圾回收器回收了p原指向的内存,就会出现后面转换回来内存无效的情况。
有些人可能要问了,我不这么做不就行了吗?其实像这种情况还有很多,而且reinterpret_cast有时候是一个非常有用的工具。比如:OpenCV的Java代码调用C++底层就用到了这个特性,Mat的内存指针被转换成long型传回Java层,在需要使用的时候再传到C++层转换回来。这种转换几乎没有效率上的损耗,要不然Java和C++之间的内存数据转换将是一个及其头疼的问题!
说到底,终究是谁也不能给出一个完美的解决方案,有利也有弊。
1、用好三种内存就足够了