C++ 中的内存分配和动态内存

描述 C++ 的内存分配方式以及它们之间的区别?

在 C++ 中, 内存区分为5个区, 分别是: 堆, 栈, 自由存储区, 全局/静态存储区, 常量存储区.

  1. 栈: 函数内的局部变量的存储单元都会在栈上创建, 函数执行结束时这些存储单元会被自动释放
  2. 堆: 也称为动态内存. 程序在运行的时候用malloc申请任意大小的内存, 程序员自己负责在何时使用和释放这些内存. 动态内存的生存期由程序员自己决定, 使用非常灵活, 但相关的内存泄漏问题也尝尝发生.
  3. 自由存储区域: 程序在运行时候利用new申请的内存空间.
  4. 全局/静态存储区域: 存储全局变量和静态变量, 处于该内存的变量在程序的整个运行期间都一直存在.
  5. 常量存储区: 存储常量字符串, 程序结束后由系统释放.

自由存储区和堆的区别

从技术上来说, 堆(heap)是 C 语言和操作系统的术语, 它是操作系统所维护的一块特殊内存, 提供了动态分配的功能, 当运行程序调用malloc()时就会从堆中分配内存, 之后会调用free()将内存归还. 而自由存储区是 C++ 中通过newdelete动态分配和释放对象的 抽象概念, 通过new来申请的内存区域成为自由存储区. 实际上, 基本所有的 C++ 编译器都默认使用堆来实现自由存储区, 也就是说, 缺省的 全局运算符 newdelete大多会通过mallocfree来实现, 这时候我们说new申请的内存在自由存储区上或者在堆中都是正确的. 但是程序员可以通过重载newdelete运算符, 来改用其他内存实现自由存储区, 这时候自由存储区就区别于堆了. 我们所需要记住的是: 堆是操作系统维护的一块特殊内存, 而自由存储区是 C++ 中通过newdelete动态分配和释放对象的抽象概念. 二者并不完全等价.

new 和 malloc 的区别

(1). 申请的内存位置
new操作符从 自由存储区 上为对象动态分配内存空间, 而malloc函数从 上动态分配内存. 自由存储区是 C++ 基于new操作符的一个抽象概念, 凡是通过new操作符进行内存申请, 那么该内存就是自由存储区. 而堆是操作系统中的术语, 是操作系统所维护的一块特殊内存, 用于程序的内存动态分配, C 语言使用malloc从堆上分配内存, 使用free释放已经分配的对应内存.
自由存储区是否在堆上(问题等价于new申请的内存是否在堆上), 这取决于new运算符的实现细节. 自由存储区不仅可以是堆, 还可以是静态存储区, 这主要看new实现时在哪里为对象分配内存.

(2). 返回类型安全性
new 运算符内存分配成功时, 返回的是对象类型的指针, 类型严格与对象匹配, 无序进行类型转换, 故new是类型安全的运算符. 而malloc内存分配成功时返回的是void *, 需要通过强制类型转换将void *指针转换成需要的类型.

(3). 内存分配失败时的返回值
new内存分配失败时, 会抛出bad_alloc异常;
malloc分配内存失败时会返回NULL.
因此, 二者在判断是否分配成功时的代码逻辑不太, 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// malloc
int* a = (int*)malloc(sizeof(int));
if (a == NULL) {
...
} else {
...
}

// new
try {
int* a = new int();
} catch (bad_alloc) {
...
}

(4). 是否调用构造函数/析构函数
newdelete会调用对象的构造函数/析构函数以完成对象的构造/析构. 而malloc则不会.

(5). 对数组的处理
C++ 提供了new[]delete[]来专门处理数组类型. 但是对于mallocfree来说, 它并不关心分配的内存是否为数组, 需要程序员自行决定参数的合理设置.

delete 会调用对象的析构函数, 和new对应

free 只会释放内存, 和malloc.

malloc/free是C++/C 语言的标准库函数, new/deletec是C++语言的运算符, 因此, 我们可以通过重载newdelete运算符, 来完成自定义的功能.

它们都可用于申请动态内存和释放内存, 对于非内部数据类型的对象而言, 光用 malloc/free 无法满足动态对象的要求, 对象在创建的同时要自动执行构造函数, 在消亡之时要自动执行析构函数, 由于malloc/free是库函数而不是运算符, 因此不在编译器控制权限之内, 不能够把执行构造函数和析构函数的任务强加于malloc/free. 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new, 以及一个能完成清理与释放内存工作的运算符delete.

new 运算符, operator new, new 表达式

运算符newnew[]实际上是分别调用了如下函数:

1
2
void * operator new(std::size_t);
void * operator new[](std::size_t);

当我们在使用一个new的时候, 它就变成了一个表达式的形式, 如下所示:

1
Test* test = new Test;

new表达式的行为主要有两步:

  1. 执行operator new()函数, 在堆空间中搜索合适的内存并进行分配
  2. 调用相应的构造函数构造对象, 初始化这块内存空间.

delete 和 delete []的区别

delete只会调用一次析构函数, 而delete[]会调用数组内每一个成员的析构函数. 在More Effective C++中有更为详细的解释: 当delete操作符用于数组时, 它为每个数组元素调用析构函数, 然后调用operator delete来释放内存

在对 内建数据类型 使用时, delete和delete[]是等价的, 因此delete[]会调用数组元素的析构函数, 但是内部数据类型没有析构函数, 所以可以直接使用.

如何限制一个类对象只在栈(堆)上分配空间?

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。

  • 静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
  • 动态建立类对象:是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

(1). 只在对上分配类对象
就是不能静态建立类对象, 即不能直接调用类的构造函数.
首先要知道, 当对象建立在栈上面时, 是由编译器分配内存空间的, 当对象使用完以后, 编译器会调用析构函数来释放对象所占的空间. 实际上, 编译器在为类对象分配栈空间时, 会检查类的析构函数的访问性(其他非静态函数也会检查), 如果类的析构函数是私有的, 则编程器不会在栈空间上为类对象分配内存. 因此, 我们只需要将析构函数设为私有, 类对象就无法建立在栈上了, 如下所示:

1
2
3
4
5
6
7
class A{
public:
A(){}
void destroy(){delete this;}
private:
~A(){}
}

注意, 由于new表达式会在分配内存以后调用构造函数, 因此构造函数必须是公有的, 同时, 由于delete此时无法访问私有的析构函数, 因此必须提供一个destroy函数, 来进行内存空间的释放.

存在问题:

  1. 无法解决继承问题: 为了实现多态, 析构函数通常要设为virtual, 因此析构函数不能设为private, 此时我们可以使用protected, 这样, 子类可以访问析构函数, 而外部无法访问.
  2. newdestroy的对应关系容易引起误解, 解决办法是将构造函数也设置为protected, 然后提供一个create函数和destroy对应.

(2). 只在栈上分配类对象
只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。
虽然你不能影响new operator的能力(因为那是C++语言内建的),但是你可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。

因此,将operator new()设为私有即可禁止对象被new在堆上。

代码如下:

1
2
3
4
5
6
7
8
9
class A  
{
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};