文章目录
- 前言
- [一. C/C++内存分布](#一. C/C++内存分布)
- [二. C语言中动态内存管理方式](#二. C语言中动态内存管理方式)
-
- [2.1 malloc函数](#2.1 malloc函数)
- [2.2 realloc函数](#2.2 realloc函数)
- [2.3 calloc函数](#2.3 calloc函数)
- [2.4 free函数](#2.4 free函数)
- [2.5 总结区别](#2.5 总结区别)
- [三. C++内存管理方式](#三. C++内存管理方式)
-
- [3.1 new/delete操作内置类型](#3.1 new/delete操作内置类型)
- [3.2 new/delete操作自定义类型](#3.2 new/delete操作自定义类型)
- [四. operator new与operator delete函数](#四. operator new与operator delete函数)
- [五. new和delete的实现原理](#五. new和delete的实现原理)
-
- [5.1 内置类型](#5.1 内置类型)
- [5.2 自定义类型](#5.2 自定义类型)
-
- [5.2.1 new的原理](#5.2.1 new的原理)
- [5.2.2 delete的原理](#5.2.2 delete的原理)
- [5.2.3 new T[N] 的原理](#5.2.3 new T[N] 的原理)
- [5.2.4 delete[] 的原理](#5.2.4 delete[] 的原理)
- [六. 定位new表达式](#六. 定位new表达式)
- [七. malloc/free和new/delete的区别](#七. malloc/free和new/delete的区别)
- END
前言
在这篇博文中,我会详细介绍C/C++的内存管理并对比它们的内存管理方式。
一. C/C++内存分布
回顾之前的知识,我们先来看下面的一段代码和相关问题:
cpp
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
对于以上问题,这些全局变量,局部变量,静态变量或常量都存储在哪些区域?
说明:
栈 (Stack)
定义:栈是一种遵循后进先出(LIFO)原则的数据结构,用于存储局部变量 、函数参数 和返回地址 。
特点:自动分配和释放,由编译器管理;存储生命周期与函数调用相关,函数调用时分配,函数返回时释放;存储空间较小,访问速度快。
堆 (Heap)定义:堆是用于动态内存分配 的内存区域,通常需要手动管理。
特点:
通过malloc、new等函数分配内存,通过free、delete等函数释放内存
;存储生命周期由程序员控制,容易出现内存泄漏和野指针;存储空间较大,但访问速度较栈慢。
静态区 (Static Area)定义:静态区用于存储全局变量 和静态变量 ,包括在程序整个运行期间都存在的数据,它们的内存空间在程序编译时就已经分配好。
特点:
存储生命周期与程序运行周期相同,程序结束时释放
;存储空间相对较小,访问速度较快;未初始化的全局变量和静态变量会被自动初始化为0(对于基本数据类型)和空指针(对于指针类型)。
常量区 (Constant Area)定义:常量区主要用于存储常量数据 ,这些数据在程序运行期间是不可修改的。
特点:常量区的数据只是可读的 ,任何试图修改常量区数据的操作都会导致程序运行时错误;
编译器会为常量区的数据分配内存空间,并且这些空间在程序的整个运行期间保持不变
;访问速度较快,因为常量数据通常被缓存。
对以上选择题中的 *char2 在哪里可能会有疑问,以下做详细解释:
1.char2是一个字符串数组,并用字符串字面量"abcd"来初始化。
2.字符串字面量"abcd"是存储在只读数据段(常量区)的,因为它是一个不可修改的常量值。
3.
char2数组本身存储在栈上,因为它是一个局部数组,数组char2包含了字符串"abcd"的副本,这个副本是可修改的
,并且它的生命周期仅限于函数的执行。4.char2数组的每个元素(即'a', 'b', 'c', 'd')实际上是存储在栈上的,而不是常量区。这些元素是对存储在常量区的字符串字面量的拷贝。
5.在C/C++中,数组名代表的是数组第一个元素的地址 。因此,char2是指向数组char2第一个元素的指针,这个指针指向的是存储在栈上的字符 'a' 的地址。
*char2是对指向数组char2第一个元素的指针的解引用,得到的是字符 'a'
。
所以,*char2 得到的值是存储在栈上的,而不是常量区。常量区只存储了原始的字符串字面量 "abcd",而 char2 数组是这个字符串的一个可修改的副本,存储在栈上。
特别说明:内存映射段 是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
二. C语言中动态内存管理方式
2.1 malloc函数
cpp
void* malloc(size_t size);
size_t
是一种无符号整数类型,用于表示内存块的大小(字节数),size
参数指定了要分配的字节数。函数返回一个指向所分配内存空间起始地址的void*
指针,如果分配失败(例如内存不足),则返回NULL
。
功能:malloc
函数用于在内存的动态存储区(堆)中分配一块指定大小的连续空间。这块内存空间在分配后是未初始化的,其内存是不确定的。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr;
ptr = (int *)malloc(10 * sizeof(int));
if (ptr == NULL)
{
printf("内存分配失败!\n");
return 1;
}
// 在这里可以使用分配的内存,例如给数组元素赋值
for (int i = 0; i < 10; i++)
{
ptr[i] = i;
}
// 使用完后,应该释放内存
free(ptr);
return 0;
}
在这个示例中,首先使用了malloc
函数分配了10个int类型的内存空间。sizeof(int)
用于获取整数类型的字节数,乘以 10 就是所需的总字节数。然后将返回的void*
类型指针强制转换为int*
类型,以便可以像操作普通数组一样操作这块内存。如果malloc返回NULL,则表示内存分配失败,程序会输出错误信息并退出。最后,使用完内存后通过free
函数释放内存。
2.2 realloc函数
cpp
void* realloc(void* ptr, size_t size);
ptr
是指向原来分配的内存块的指针,size
是重新分配后的内存块大小(字节数)。函数返回一个指向重新分配后的内存块起始地址的void*
指针。如果分配失败,则返回NULL,原有的内存块仍然有效(相当于对原有的内存块进行扩容),需要使用free函数手动释放。
功能:realloc
函数用于修改(重新分配)已经通过malloc、calloc或realloc函数分配的内存块的大小。它可以使内存块扩大或者缩小。如果是扩大内存块,并且原有内存块之后有足够的连续空闲空间,realloc会尝试在原有内存块的基础上进行扩展,不会移动原有数据;如果原有内存块之后没有足够的空间,realloc会另外寻找一块足够大的连续内存空间,将原有数据复制到新的内存块中,然后释放原来的内存块。如果是缩小内存块,数据不会丢失,只是内存块的末尾部分被释放。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr;
ptr = (int *)malloc(5 * sizeof(int));
if (ptr == NULL)
{
printf("内存分配失败!\n");
return 1;
}
for (int i = 0; i < 5; i++)
{
ptr[i] = i;
}
// 重新分配内存,使其可以存储10个整数
ptr = (int *)realloc(ptr, 10 * sizeof(int));
if (ptr == NULL)
{
printf("内存重新分配失败!\n");
// 需要释放原来分配的内存
free(ptr);
return 1;
}
// 继续使用扩展后的内存,例如给新的数组元素赋值
for (int i = 5; i < 10; i++)
{
ptr[i] = i;
}
free(ptr);
return 0;
}
在这个示例中,首先使用malloc
分配了一个可以存储5个整数的内存块,然后使用realloc
函数将其扩展为可以存储10个整数的内存块。如果realloc成功,就可以继续使用新的内存块。最后使用完内存后通过free
函数释放内存。
2.3 calloc函数
cpp
void* calloc(size_t num, size_t size);
num
是要分配的元素个数,size
是每个元素的大小(字节数)。函数返回一个指向所分配内存空间起始地址的void*
指针,如果分配失败(例如内存不足),则返回NULL。
功能:calloc
函数也用于在内存的动态存储区(堆)中分配内存空间,但与malloc不同的是,calloc会将分配的内存空间全部初始化为0。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr;
ptr = (int *)calloc(10, sizeof(int));
if (ptr == NULL)
{
printf("内存分配失败!\n");
return 1;
}
// 此时数组元素已经被初始化为0,可以直接使用
for (int i = 0; i < 10; i++)
{
printf("%d ", ptr[i]);
}
free(ptr);
return 0;
}
在这个示例中,使用calloc
函数分配了一个可以存储10个整数的内存空间,并且由于calloc会自动将内存初始化为0,所以可以直接打印数组元素,它们的值都是0。最后,使用完内存后通过free函数释放内存。
2.4 free函数
cpp
void free(void* ptr);
ptr
是指向要释放的内存块的指针。调用free
函数后,ptr所指向的内存空间被释放,之后程序不能再访问这块内存。
功能:free
函数用于释放之前通过malloc、calloc或realloc函数分配的内存空间。如果不释放动态分配的内存,会导致内存泄漏,即程序占用的内存越来越多,最终可能耗尽系统内存资源。
在前面的malloc、realloc和calloc示例中,都有使用free
函数来释放动态分配的内存。需要注意的是,不能多次释放同一块内存,也不能释放不是通过动态分配函数(如malloc、calloc、realloc)获得的内存,否则会导致程序出现错误。例如:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr;
ptr = (int *)malloc(5 * sizeof(int));
if (ptr == NULL)
{
printf("内存分配失败!\n");
return 1;
}
// 正确释放内存
free(ptr);
// 错误示范:再次释放已经释放的内存,就会报错
free(ptr);
return 0;
}
在这个错误示例中,第二次调用free函数会导致程序出现错误,因为已经释放过的内存不能再次释放。
2.5 总结区别
malloc :只负责分配内存,不负责初始化。
calloc :负责分配内存并初始化为 0。
realloc:可以调整已分配内存块的大小,并且可以处理内存块的移动(如果新大小与原大小不同,可能会移动内存块到新的地址)。
三. C++内存管理方式
由于C++兼容C语言,所以C语言的内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
new
用途 :用于在堆
上动态分配内存,并调用对象的构造函数(如果分配的是对象)。
语法:
new 类型:分配一个指定类型的单个对象,并调用其构造函数来初始化。
new 类型[size]:分配一个指定类型的数组,并调用每个元素的构造函数。
返回值 :返回一个指向分配内存的指针,其类型为 类型* 或 类型*[ ](对于数组)。
特点:
如果分配失败,会抛出一个 std::bad_alloc 异常
可以分配基本数据类型,对象以及对象数组
分配的对象需要调用其构造函数进行初始化
delete
用途 :用于释放之前通过 new 分配的内存,并调用对象的析构函数(如果删除的是对象)
语法:
delete 指针:释放单个对象,并调用其析构函数。
delete[] 指针:释放一个对象数组,并调用每个元素的析构函数。
特点:
必须与new 或者 new[] 分配的内存配对使用。
释放后,指针应立即设置为
nullptr
,以避免悬空指针问题。如果尝试释放未分配的内存或已经释放的内存,结果是未定义的,可能会导致程序崩溃。
3.1 new/delete操作内置类型
cpp
void Test()
{
// 动态申请一个int类型的空间
int* ptr1 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr2 = new int(10);
// 动态申请10个int类型的空间,并且进行初始化
int* ptr3 = new int[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
delete ptr1;
delete ptr2;
delete[] ptr3;
}
注意 :申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],注意:匹配起来使用。
3.2 new/delete操作自定义类型
对于自定义类型,malloc/free只是简单地开辟空间和释放空间,而new/delete
除了会开辟空间和释放空间,还会调用构造函数进行初始化和调用析构函数来清理内存(避免内存泄漏)。
析构函数用于释放对象内部动态分配的其他资源(如在类的成员函数中使用new分配的内存),确保对象在被销毁时能够正确地清理自己的资源。
cpp
#include<iostream>
using namespace std;
class A {
public:
A(int i = 5)
{
_p = (int*)malloc(sizeof(int) * i);
cout << "A()" << endl;
}
~A()
{
free(_p);
cout << "~A()" << endl;
}
private:
int _a = 1;
int* _p;
};
int main()
{
A* a1 = (A*)malloc(sizeof(A));
free(a1);
A* a2 = new A;
delete a2;
return 0;
}
通过调试可以看到,malloc和free函数没有调用构造函数和析构函数。
继续往下执行可以看到,new调用了构造函数,在delete之前会先调用析构函数将对象内部动态分配的其它资源先释放掉,再释放对象本身。
还可以创建一个A类型对象的指针数组,并且对它们进行初始化。
cpp
A* a3 = new A[10];
A* a4 = new A[5]{ 1, 2, 3, 4, 5 };
a3是A类型对象的指针数组(也可以说是指向A类型对象数组的指针),它是一个指针,指向一个包含10个A类型对象的数组的第一个对象,对a3解引用(*a3)得到的是A类型对象数组的第一个对象。
a4同样也是A类型对象的数组,但不同的是,该数组的每个对象都显示调用了构造函数进行初始化,如果不显示调用构造函数,则编译器会自动调用默认构造函数进行初始化(如果没有默认构造则会报错)
。
我们创建一个类类型对象,可以采用两种方式:A a;
和 A* a = new A;
那么以上两种创建对象的方式有什么区别呢?
1.栈上的对象
A a; 这种方式是在栈上创建一个对象。 这意味着对象a会在声明它的函数或代码块的生命周期内自动创建和销毁。当函数返回或者代码块结束时,对象a会自动调用其析构函数进行清理,不需要显示删除。
- 堆上的对象
A* a = new A; 这种方式是在堆上创建一个对象。 new操作符会分配一块内存来存储对象,并返回指向这块内存的指针。对象的生命周期不再局限于声明它的函数或代码块,而是持续到显示使用delete操作符释放内存为止。如果忘记使用delete删除对象,就会造成内存泄漏。
具体区别如下:
内存位置 :栈上对象存储在栈内存中,而堆上对象存储在堆内存中。
生命周期管理 :栈上对象的生命周期由编译器自动管理,而堆上对象需要程序员手动管理。
内存大小限制 :栈内存通常有大小限制,而堆内存理论上可以更大(受限于操作系统和机器的内存)。
性能 :栈上分配和释放通常更快,因为栈内存是连续的,而堆内存分配可能涉及更复杂的内存管理策略。
异常安全:栈上对象在异常发生时会自动析构,而堆上对象需要手动管理,否则可能导致资源泄漏。
四. operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
cpp
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
以上是C++提供的全局函数operator new 和 operator delete的源代码。从中可以看出operator new是通过malloc函数来申请空间的,如果申请成功就直接返回;如果申请失败了就尝试执行用户应对空间不足的措施,如果用户没有提供就抛出异常。operator delete是通过free函数来释放空间的(operator new 是对 malloc 的封装,operator delete是对 free 的封装)。
接下来简单演示一下使用try catch来捕获异常和输出异常,因为64位系统的堆内存比较大,所以采用了32位来给大家演示:
cpp
#include<iostream>
#include<exception>
using namespace std;
void Func()
{
// 尝试分配一个非常大的数组,可能会导致堆上空间不足
int* p1 = new int[100000000]; // 申请一个非常大的数组
cout << "内存分配成功" << endl;
int* p2 = new int[100000000]; // 申请一个非常大的数组
cout << "内存分配成功" << endl;
int* p3 = new int[100000000]; // 申请一个非常大的数组
cout << "内存分配成功" << endl;
int* p4 = new int[100000000]; // 申请一个非常大的数组
cout << "内存分配成功" << endl;
int* p5 = new int[100000000]; // 申请一个非常大的数组
cout << "内存分配成功" << endl;
int* p6 = new int[100000000]; // 申请一个非常大的数组
cout << "内存分配成功" << endl;
}
int main() {
try {
Func();
}
catch (const exception& e) {
// 捕获std::bad_alloc异常
cerr << "内存分配失败: " << e.what() << endl;
}
return 0;
}
五. new和delete的实现原理
5.1 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete
申请和释放的是单个元素的空间,new[]和delete[]
申请和释放的是连续空间。- new在申请空间失败时会抛异常,malloc会返回NULL。
5.2 自定义类型
5.2.1 new的原理
1.调用 operator new 函数申请空间。
2.在申请的空间上执行构造函数,完成对象的构造。
3.
new = (operator new + 构造)
5.2.2 delete的原理
1.在空间上执行析构函数,完成对象中资源的清理工作。
2.调用 operator delete 函数释放对象的空间。
3.
delete = (析构 + operator delete)
5.2.3 new T[N] 的原理
1.调用
operator new[]
函数,在operator new[]
中实际调用operator new
函数完成N个对象空间的申请。2.在申请的空间上执行N次构造函数。
5.2.4 delete[] 的原理
1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
2.调用
operator delete[]
释放空间,实际在operator delete[]
中调用operator delete
来释放空间。
重点
在使用new[N]时,编译器通过N可以确定要调用多少次构造函数来初始化,但是调用delete[]时没有给出值,那么编译器如何知道要调用多少次析构函数呢?
先来看以下代码:
cpp
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* a = new A[10];
delete[] a;
return 0;
}
每个对象都有一个成员变量_a,为4字节,申请10个对象所需要40个字节。但是通过内存调试窗口发现一共申请了48个字节,那么多余的8个字节是用来干什么的?
再往上看,就会发现多开出来了8个字节。
内存分配中的记录机制
当使用
new[]
分配内存时,编译器除了分配足够的空间来存储N个对象,还会在这片内存的开头(或其他合适的位置)存储一些额外的信息。这些信息通常包括对象的数量N以及可能的其他元数据,如内存块的大小等。这个数量N信息在delete[]
被调用时会被用来确定需要调用多少次析构函数。
编译器会分配足够的内存来存储N个A类型对象,并且在这内存的开始处存储N的值。当调用delete[] a
时,编译器会读取存储在内存开始处的N值,并使用这个值来确定需要调用多少次析构函数。
这个过程是自动的,我们不需要手动指定数组的大小。这也是为什么使用delete[]
而不是delete来释放由new[]
分配的内存是重要的,因为delete不会读取和使用这个额外的大小信息,这将导致不正确的析构函数调用次数,可能会导致内存泄漏或者未定义行为。
总结 :
当我们调用new[]
创建一个对象数组时,会接收一个地址,这个地址是我们开辟好的连续空间的第一个位置的地址,但是编译器在这个位置之前又多开了空间来存储对象的个数,这样在调用delete[]
时就可以根据这个值知道调用多少次析构函数了。如果使用delete[]
来释放空间,那么会从a指向的空间再向前移动的位置开始释放空间,而不是从a指向的空间直接开始释放,如果从a指向的空间开始释放就会报错。
比如:
因为调用的是delete a,直接从a指向的位置开始释放空间,而不是从真正的第一个位置开始释放空间(因为申请的空间是连续的,不能分割释放),所以程序报错。
上述情况是我们显示写了析构函数的情况下,如果我们没有显示写析构函数,编译器自动生成的析构函数没有作用,会被优化掉。那么就不会调用析构函数,也不会另外开空间来存储调用次数,所以不会报错。
cpp
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* a = new A[10];
delete a;
return 0;
}
没有显示写析构函数时,编译器不会报错。
因为编译器自动生成的析构函数不起作用,会被优化掉,所以不会另外开辟空间去存储调用析构函数的次数。
可以参考博客:C++:深入理解operator new/operator delete
六. 定位new表达式
定义:
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。可以理解为:我先开辟好空间用来存储对象,但是不先初始化,等到需要的时候再调用构造函数来初始化对象(定位new)。
使用格式:
new (place_address) type
或者new (place_address) type(initializer-list)
注意:place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
cpp
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参,比如:new(p1)A(1);
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
七. malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
1.malloc和free是函数,new和delete是操作符。
2.malloc申请的空间不会初始化,new可以初始化。
3.malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
4.malloc的返回值为void*,在使用时必须强制转换,new不需要,因为new后跟的是空间的类型。
5.malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是会抛出 std::bad_alloc 异常,所以new需要捕获异常。
6.申请自定义类型对象时,malloc/free只会开辟空间和释放空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放。
7.使用 malloc 和 free 时,如果忘记释放内存,会导致内存泄漏。使用 new 和 delete 时,如果忘记调用 delete,同样会导致内存泄漏。
8.malloc 和 free 用于分配和释放普通数组,但它们不区分单个对象和数组。new[] 和 delete[] 专门用于分配和释放对象数组,它们会调用每个对象的构造和析构函数。
END
对以上内容有异议或者需要补充的,欢迎大家来讨论!