文章目录
-
- [1. 先分清两件事:内存放在哪里,对象是否已经存在](#1. 先分清两件事:内存放在哪里,对象是否已经存在)
- [2. C 的动态分配:拿到的是内存,不是 C++ 对象的构造过程](#2. C 的动态分配:拿到的是内存,不是 C++ 对象的构造过程)
- [3. `new` 与 `delete`:先从内置类型看语法和初始化](#3.
new与delete:先从内置类型看语法和初始化) - [4. 对自定义类型,分配和构造必须连起来看](#4. 对自定义类型,分配和构造必须连起来看)
- [5. `new` 表达式和 `operator new` 函数,不是同一个东西](#5.
new表达式和operator new函数,不是同一个东西) - [6. 拓展:placement new(定位 new)](#6. 拓展:placement new(定位 new))
- [7. 为何要严格匹配内存开辟与释放规则](#7. 为何要严格匹配内存开辟与释放规则)
- [8. 小结](#8. 小结)
1. 先分清两件事:内存放在哪里,对象是否已经存在
在 C 中,我们常用"栈、堆、数据段、代码段"来描述一个典型程序的内存布局。下面用一段 C++ 代码来观察变量与它具体指向的内容:
cpp
#include <iostream>
//定义全局(静态)变量
int global_value = 1;
static int file_value = 2;
int main()
{
//静态变量
static int local_static = 3;
//普通变量
int local_value = 4;
int local_array[2] = { 5, 6 };
//指针变量
const char* text = "C++";
//动态开辟内存
int* dynamic_value = new int(7);
std::cout << "global_value: " << global_value << std::endl;
std::cout << "file_value: " << file_value << std::endl;
std::cout << "local_static: " << local_static << std::endl;
std::cout << "local_value: " << local_value << std::endl;
std::cout << "local_array[0]: " << local_array[0] << std::endl;
std::cout << "text: " << text << std::endl;
std::cout << "*dynamic_value: " << *dynamic_value << std::endl;
delete dynamic_value;
}
运行示例:

global_value、file_value和local_static具有静态存储期,通常放在静态区(数据段);local_value、local_array、text和dynamic_value这些局部变量通常位于栈帧中。- 指针变量和它指向的内容不是同一块东西 :
dynamic_value自身是局部变量,*dynamic_value是动态创建的整数对象,空间占用在堆上; text自身是局部指针,而字符串字面量具有静态存储期,通常位于只读区域。

2. C 的动态分配:拿到的是内存,不是 C++ 对象的构造过程
先把熟悉的四个函数放在一张表里进行对比:
| C 函数 | 主要作用 | 须记住的边界 |
|---|---|---|
malloc(bytes) |
分配指定字节数 | 内容未初始化;失败返回空指针 |
calloc(count, bytes) |
分配并将字节置零 | 字节置零但不等于调用 C++ 构造函数 |
realloc(ptr, new_bytes) |
调整既有分配块 | 成功后旧指针失效,地址可能改变;失败时原块仍有效 |
free(ptr) |
释放上述函数得到的分配块 | 不调用 C++ 析构函数;free(nullptr) 安全 |
下面是一个 C 语言的内存管理示例,只对整数数组使用 calloc/realloc。:
c
#include <stdlib.h>
#include <stdio.h>
int main()
{
int* values = (int*)calloc(3, sizeof(int));
if (values == NULL)
{
perror("calloc fail!");
return 1;
}
int* grown = (int*)realloc(values, 5 * sizeof(int));
if (grown == NULL)
{
perror("realloc fail!");
free(values);
return 1;
}
values = grown; // 成功后只能继续使用新指针
printf("%d %d\n", values[3], values[4]);
values[3] = 30; // 扩出来的字节不能假定已清零
values[4] = 40;
printf("%d %d\n", values[3], values[4]);
free(values);
return 0;
}
运行示例:

前两次打印时,由于未给新分配好的内存空间赋值,所以打印随机值,正常分配后则输出 30 40。如果直接写 values = realloc(values, ...),失败返回的空指针会覆盖原地址,从而导致原块无法释放(内存泄漏)。
在 C++ 中调用这些函数时,若是用来开辟自定义类型的对象时,它们不会调用类的构造函数。
对已经由 new 创建的对象,也不能 用 realloc 扩容。
3. new 与 delete:先从内置类型看语法和初始化
new T 创建一个 T 对象;new T[n] 创建一个含 n 个对象的数组。
相较于手写 malloc(n * sizeof(T)),new 根据类型和元素个数决定所需存储,并直接返回相应类型的指针;对应地,单个对象用 delete,数组用 delete[]。
cpp
// 完整示例:内置类型的不同初始化方式
#include <iostream>
int main()
{
int* a = new int; // 值未初始化,先赋值再读取
int* b = new int(10); // 初始化为 10
int* c = new int(); // 值初始化,得到 0
int* d = new int[3]; // 三个元素的值未初始化(随机值)
int* e = new int[3](); // 三个元素均值初始化为 0
int* f = new int[3] {1, 2, 3}; //三个元素按照给定元素初始化
int* g = new int[3] {1}; //第一个元素按给定元素初始化,其余为0
*a = 5;
d[0] = 1;
d[1] = 2;
d[2] = 3;
std::cout << "*a: " << *a << std::endl;
std::cout << "*b: " << *b << std::endl;
std::cout << "*c: " << *c << std::endl;
for (size_t i = 0; i < 3; i++)
{
std::cout << "d[" << i << "]:" << d[i] << ' ';
}
std::cout << std::endl;
std::cout << std::endl;
for (size_t i = 0; i < 3; i++)
{
std::cout << "f[" << i << "]:" << f[i] << ' ';
}
std::cout << std::endl;
std::cout << std::endl;
for (size_t i = 0; i < 3; i++)
{
std::cout << "g[" << i << "]:" << g[i] << ' ';
}
delete a;
delete b;
delete c;
delete[] d;
delete[] e;
delete[] f;
delete[] g;
}
运行示例:

普通 new 分配失败时通常抛出 std::bad_alloc而中断程序,而不是像 malloc 那样返回空指针;所以写 if (p == nullptr) 并不能处理普通 new 的分配失败。
后续可以使用异常处理中的
try...catch来进行异常的捕捉。
4. 对自定义类型,分配和构造必须连起来看
在C++中,动态开辟类对象时比整数时多了一段关键过程:new自动调用构造函数让对象可用,delete自动调用析构函数在对象结束前清理资源。这也是与 C 的内存管理有差别的地方,malloc是函数,而new是操作符。
下面的 Cell 类在构造时申请一个整数内存,在析构时释放它。
cpp
// 完整示例:单个对象与对象数组的生命周期
#include <iostream>
class Cell
{
public:
explicit Cell(int value = 0)
: _value(new int(value))
{
std::cout << "构造 " << *_value << std::endl;
}
~Cell()
{
std::cout << "析构 " << *_value << std::endl;
delete _value;
}
private:
int* _value;
};
int main()
{
Cell* one = new Cell(7);
delete one;
Cell* three = new Cell[3];
delete[] three;
}
运行示例:

正常执行时,先输出一次"构造 7、析构 7",再输出三次"构造 0"和三次"析构 0"。
new Cell[3]要能逐个构造,因此这里的Cell需要有可不传参数的构造函数(默认构造函数);delete[]要结束全部数组元素的生命,再释放数组占用的存储。

如果把 delete one 改成 free(one),析构函数不会执行,_value 指向的资源无法经由析构函数释放;所以就可能导致崩溃、破坏分配器状态。
单个元素也可能暂时看不出异常。但还是建议尽量配对使用,并且尤其注意释放开辟的数组时使用方括号
delete[]。
5. new 表达式和 operator new 函数,不是同一个东西
名字接近,职责却不同:
| 写法 | 做什么 | 是否构造或析构对象 |
|---|---|---|
new Cell(7) |
new 表达式:取得存储并初始化一个对象 |
调用构造函数 |
delete p |
delete 表达式:结束对象生命并释放存储 |
调用析构函数 |
::operator new(sizeof(Cell)) |
分配函数:只取得原始存储 | 不调用构造函数 |
::operator delete(raw) |
释放函数:只释放原始存储 | 不调用析构函数 |
在通常的路径中,可以把过程想成:
text
new Cell(7) : 取得足够且对齐的存储 -> 在其中构造 Cell -> 得到 Cell*
delete p : 对 Cell 执行析构 -> 释放其占用的存储
operator new ≠ new表达式
全局 operator new 常由运行库在底层借助 malloc 或其他分配器实现,但标准并不要求它必须调用 malloc 。
- 不要混淆:调用
operator new不会产生对象 ,只是一块未初始化内存,直接当成Cell *去使用,属于未定义行为。 operator new/operator delete可以被类重写(类内重载);new表达式语法本身不能重载。

6. 拓展:placement new(定位 new)
如果你已经用operator new拿到裸内存,想要手动执行构造函数,就用定位 new:
cpp
void* raw = ::operator new(sizeof(Cell)); // 只分配内存,没有对象
Cell* obj = new(raw) Cell(7); // placement new:在raw这块内存上调用构造函数
释放的时候也要手动两步:
cpp
obj->~Cell(); // 手动调用析构
::operator delete(raw); // 再释放原始内存
7. 为何要严格匹配内存开辟与释放规则
先看以下规则,不只看一次运行的表象:
| 取得方式 | 正确释放 | 错误配对 |
|---|---|---|
malloc/calloc/realloc |
free |
delete、delete[] |
new T |
delete |
free、delete[] |
new T[n] |
delete[] |
free、delete |
这些错误配对都是未定义行为 ,包括"内置类型数组用 delete 并没发生崩溃"。对于有析构函数的对象数组,后果往往更明显(对象析构不完全导致内存泄漏):delete 并不能按数组规则逐个结束所有元素的生命,释放时还可能把不合适的地址交给分配器。
那么,delete[] 又怎样知道该析构多少个对象?有的实现会在分配块附近保存元素数目或相关元数据,常称为 array cookie;某些类型、实现或优化下也可能不需要这样的记录。
cookie 是编译器实现细节,C++ 标准没有强制要求必须存在、放哪里、多大 。
但记录的位置、大小、是否存在,都不能拿来随意编写可移植代码,要根据平台与编译器的选择具体决定。

但只要严格遵守配对使用,就能很大程度避免一些问题的发生,尤其对于一些底层实现很复杂的情况。
8. 小结
new/delete是表达式,分配释放内存同时自动调用构造、析构 ;new[]必须配delete[],错配属于未定义行为。operator new/operator delete只操作裸内存,不执行构造析构 ,是new表达式底层调用的函数。malloc/free只管理原始内存,不处理对象;和new/delete指针严禁交叉释放。- 对象完整生命周期:分配内存 -> 构造 -> 使用 -> 析构 -> 释放内存。