当你使用现代结构,例如std::vector,std::string等等,从一开始到现在以及到未来,C++是一个安全的编程语言。该语言提供了许多的道路,路线以及红绿灯,比如C++核心指导,静态代码分析器来分析代码的正确性,等等。
然而,C++依然允许你出轨。一个出轨的例子就是手动管理内存(分配与释放内存)。对于C++编程这种手动管理内存是一个特别容易出错的领域。为了写出高质量的C++程序,专业的C++程序员需要理解内存在底层是怎么工作的。前面的博文也介绍过一点儿,我们会继续讨论动态内存的陷阱以及避免以及消除它们的一些技巧。
因为专业的C++程序员会碰到底层内存处理的代码,所以我们会讨论这一部分的内容。然而,在现代C++代码中,你应该尽量避免底层内存操作。例如,不要使用C风格的数组进行动态内存分配,要使用像vector这样的标准库容器,它们会为你自动处理所有的内存管理。不要用原始的指针,要用智能指针,比如unique_ptr和shared_ptr,后面我们慢慢讨论,它们会自动释放分配的资源,像不再需要的内存等。本质上来说,就是不要再去调用像new/new[]和delete/delete[]这样的内存分配函数了。当然,这不总是可以的,在原有的代码中,可能不是这样,所以做为一个专业的C++程序员,你依然需要知道内存在底层是如何工作的。你可以不使用这些底层的操作,但不代表你可以不明白。
在现代C++代码中,应该尽可能地避免进行内存底层操作,当牵涉到属主的时候避免原始指针,避免使用旧的C风格的结构与函数。反过来,要使用安全的C++替代方法,例如自动管理内存的对象,像C++的string类,vector容器,智能指针等等。
好了,言归正传吧,我们先来讨论一下动态内存的使用。
内存是一个底层的部件 ,对于计算机来说,有时候很不幸地成为像C++这样的高级编程语言来说也是一个不祥之物。当然了,真正理解了动态内存是如何工作的对于成长为一个专业的C++程序员非常重要。
1、如何展示内存
如何 你有一个内存对象的在大脑当中的模型的话,那理解起动态内存来就会容易得多了。我们的表示方法就是在一个方框旁边一个标记。这个标记就是一个对这个内存对应的名字。方框内的数据就是这个内存的当前值。
举个例子,下图显示了以下代码执行为的内存状态。这段代码应该在一个函数中,所以变量是一个局部变量。
int i { 7 };
i就是一个在栈上分配了空间的自动变量。当程序流离开它声明的范围时会自动释放。
当你使用new关键字时,内存就会在自由空间内存上进行分配。如果没有显式初始化,通过对new的调用分配的内存就没有初始化;也就是说,内存空间的值是任何可能的随机值。我们会以一个?来代表这种没有初始化的状态。以下代码生成了一个ptr的变量,在栈上用nullptr进行了初始化,然后在自由空间内存上给ptr指针分配了内存:
int* ptr { nullptr };
ptr = new int;
也可以用一行代码来实现:
int* ptr { new int };
下图展示了代码执行后的内存状态。
注意变量ptr仍然在栈上,即使它指向了自由内存空间。指针只是一个变量,可以在栈上或自由内存空间存在。虽然这个事实很容易被遗忘。然而,动态内存总是在自由内存空间进行分配。
提醒一下,C++核心指导指出,每一次声明指针变量的时候,都要立即用合适的指针或者nullptr进行初始化。不要留后患。
下面的例子展示了指针可以在栈上也可以在自由内存空间存在。
int** handle { nullptr };
handle = new int*;
*handle = new int;
这段代码首先声明了一个指向指向整数的指针变量handle。它就动态地分配的足够的内存来保存一个指向整数的指针,在handle中保存指向新内存的指针。然后,内存(*handle)被分配给了一个指向另一块足够保存整数的动态内存片的指针。下图展示 了两层指针,一个指针位于栈上(handle),另一个位于自由内存空间(*handle)。
2、分配与释放内存
为变量分配空间,需要作用use关键字。释放该空间可以让程序的其他部分使用,需要使用delete关键字。
2.1、使用use和delete
当你想要分配一块儿内存,你需要调用带有该类型变量需要的空间的new关键字。new返回一个指向该内存的指针,保存这个指针变量就靠你了。如果你忽略掉new的返回值或者指针变量越界,内存就会变成孤儿,因为无法再访问到它。这就叫做内存泄露。
例如,下面的代码就使用保存int的内存变成了孤儿。下图展示了代码执行后的内存状态。
void leaky()
{
new int; // BUG! 内存泄露!
println("I just leaked an int!");
}
当有大块的数据在自由内存空间无法访问,从栈上不管是直接访问或者间接访问都无法做到,那么这块内存就是孤儿或者内存泄露了。
在找到让计算机无限地供给内存之前,需要告诉编译器当与之相连的对象可以被释放用于其他目的。为了释放自由内存空间上的内存,需要使用delete关键字后跟指向内存的指针,如下代码所示:
int* ptr { new int };
delete ptr;
ptr = nullptr;
从经验上来讲,每一行用new分配内存的代码,使用原始指针而不是使用智能指针保存指针的代码,都应该有对应的使用delete的一行代码来释放同样的内存。
推荐在释放了内存之后,将指针赋一个nullptr的值。这样的话,就可以避免使用已经被释放了内存的指针。也需要指出的是,对于nullptr指针调用delete也是被允许的,只不过它啥也不做而已。