C++的内存知识是关键中的关键,有一些C++的内存坑点,因为C++没有垃圾回收机制(Garbage Collection),因此稍不注意容易发生内存泄露等问题。接下来就说说内存的各种要点。

五种内存分配类型

栈内存

栈(stack)内存,编译根据代码来分配并释放,通常为代码的局部变量,函数形参等,其结构类型与数据结构的栈相似,先进后出。这种内存 由系统分配并且进行回收,效率较高但最大栈内存受到编译器限制,程序猿不用担心其内存泄露,即使出错,编译器也能发现错误。

如:

1
2
3
int a = 3;
char b[] = "abc";
char c = 'k';

这些内存在退出调用时系统就会自动回收。只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示 栈溢出。因此如果递归函数,但是边界设置不正确时,会因超出栈所能申请的内存空间发生栈溢出。

堆内存

堆(heap)内存,通常由程序猿来申请分配,C/C++中的malloc以及new,就是分配这种堆内存。堆内存分配的效率没有栈内存的高,不过可分配的空间一般只受限于系统的有效的内存。通常这种堆内存用来存储对象或者结构体等。这种内存只有在程序猿 主动释放 或者 程序结束时 系统自动释放。

如:

1
2
int a = new int(10);
int b = (int) malloc(sizeof(int)*10);

这种内存需要程序猿手动释放,不然会造成许多内存碎片(无法再度使用的),即内存泄露,而且编译器很难检查得到

全局&静态区

这部分的内存通常用来存放全局变量以及静态变量(static),这一大部分叫做全局区,全局区也分两个小部分,一部分存放已初始化的全局变量和静态变量,另一部分存放未初始化的全局变量和静态变量,并且向这部分申请内存时 只申请一次 ,内存在结束时由系统自动释放。

如:

1
2
3
4
5
6
7
int a; //全局变量
static int b; //全局静态变量
int main()
{
static c; //局部静态变量
}

这种变量只会申请一次,比如在函数A里申请了局部静态变量b,当下次再次调用A时,b仍是那个内存块。

1
2
3
4
5
6
7
8
9
10
11
12
int A()
{
static int a=0; // 程序运行前以及申请内存并初始化
a++;
return a;
}
int main()
{
cout << A(); // 输出 1
cout << A(); // 输出 2
}

文字常量区

常量字符串存放的地方,由系统自行释放。有点像java中的字符串常量,因为同一段文字分配的是同一个内存区。

1
char *p = "hello,world!"; //字符指针p在栈区 “hello,world!\0”在文字常量区

程序代码区

用于存放函数体的而二进制代码。

内存管理

C++的内存管理完全交给程序猿来处理,这是一把双刃剑。好处是GC部分由程序猿自己实现或者有些地方不需垃圾回收,以提高性能效率。坏处是若程序猿不自行管理的话,容易造成内存泄露,造成内存碎片化,严重的会不停的申请内存,直至系统内存无法申请为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void call()
{
int \*a = new int(); //因为没有进行堆内存回收造成泄露(反斜杠由于md的问题自行忽略掉吧)
}
int main()
{
while(1)
{
call(); //会循环申请堆内存
}
return 0;
}

以上代码慎重运行,严重点会卡死系统(别问我怎么知道的/(ㄒoㄒ)/)

为了不造成内存泄露,每次new/malloc了内存之后使用完毕务必delete/free掉刚申请的内存 ,另外还可以使用c++11的智能指针,省去手动delete。智能指针自动释放内存的原理是:智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。 常用的智能指针有 shared_ptr,unique_ptr,weak_ptr具体更细致的解释(挖坑)将放到面向对象篇进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A
{
public:
A(){};
~A(){};
};
int main()
{
shared_ptr<A> ptr(new A()); //使用了智能指针后 指针超出作用域就会自动销毁所申请的内存
unique_ptr<A> ptr1(new A());
}

安利:另外Linux下的Valgrind是检测C++内存泄露好工具哦!

delete 与 delete []

delete 与 delete [] 经常用作回收堆内存,两者有差别,delete用来回收单个对象,delete []用来回收对象数组,下面分两种情形讨论:

(1) 当对象为基础数据类型,如int,char,double时,回收数组可以用delete []和delete

1
2
int *p =new int[5];
delete p; // or delete [] p;

(2) 当对象为自定义的类对象时,回收数组必须使用delete [] ,因为使用delete [] 时 会主动调用每个对象数组元素(即单个对象)的析构函数

1
2
3
4
5
6
7
8
9
10
11
class A()
{
public:
~A(){}
}
int main()
{
A* p = new A[5];
delete [] p; //使用delete会发生内存泄露
}

所以delete的实质其实是调用对象的析构函数,只是当对象为基础数据类型就简便了而已。不过为了方便记忆,建议凡是回收数组都用delete[],回收单体就用delete。即new 对应 delete , new [] 对应 delete []

总结

With great power comes great responsibility is big

性能与易用两者不可兼得也,只能苦学而取性能也!