📝前言: 上篇文章【C++高潮:类与对象】我们对C++的类与对象的知识点进行了讲解。 这篇文章我们在C语言内存管理的基础上探讨一下C++内存的管理: 1,C/C++内存分布 2,C语言内存管理 3,C++内存管理方式 4,operator new与operator delete 5,new和delete的实现原理 6,定位new表达式 7,malloc/free和new/delete的区别
先来看一段代码(思考下面的变量都放在什么区域):
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);
}
在C/C++中,程序内存区域主要分为以下几个部分:
localVar
、num1
、char2
、pchar3
、ptr1
、ptr2
、ptr3
都存放在栈中。malloc
、calloc
、realloc
分配的内存以及new
操作符分配的内存都在堆上,像ptr1
、ptr2
、ptr3
指向的内存就是在堆上分配的。globalVar
、staticGlobalVar
、staticVar
。const char* pchar3 = "abcd";
中的字符串常量"abcd"
就在代码段。这里有个很有意思的问题:* char2
和* pchar3
分别储存在哪里?
答案:
* char2
在栈,* pchar3
在代码段。 因为,char2
代表数组首元素的地址,* char2
代表的是数组char2
的首元素,这个首元素储存在char2
中,所以在栈;pchar3
指的是字符串abcd
中a
的地址,* pchar3
代表的是abcd
中的a
,因为abcd
在代码段,所以在代码段。
在C语言里,我们使用malloc
、calloc
、realloc
来分配动态内存,用free
来释放内存。
void Test()
{
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3);
}
malloc
:分配我们自行指定的字节数的内存空间,不会对内存进行初始化,且需要自行判断是否分配成功。calloc
:会在分配内存后,将内存初始化为0。它接受两个参数,第一个是元素个数,第二个是每个元素的大小。realloc
:用于调整已经分配的内存块的大小,可以扩大或缩小。如果原内存块后面有足够的空间,就直接在原内存块上进行扩展;否则,会重新分配一块新的内存,并把原内存的数据复制过去。在使用realloc
时,如果p2
和p3
指向的地址不同,就需要释放原来的p2
,否则会造成内存泄漏。
malloc
在glibc中的实现较为复杂,它基于内存池和链表等数据结构来管理内存。简单来说,它会在堆上寻找合适的空闲内存块进行分配,如果没有足够大的空闲块,可能会向操作系统申请更多内存。
C++在C语言内存管理的基础上,提出了自己的内存管理方式:通过new
和delete
操作符进行动态内存管理。
new
操作符用于在堆上动态分配内存,并可以同时对对象进行初始化。它有多种使用形式:
int* ptr = new int(42); // 分配一个 int 类型的对象,并初始化为 42
int* arr = new int[5]; // 分配一个包含 5 个 int 类型元素的数组
delete
操作符用于释放由 new
分配的内存。同样有对应于单个对象和数组的不同使用形式:
delete ptr; // 释放由 new 分配的单个对象的内存
delete[] arr; // 释放由 new[] 分配的数组的内存
注意:new
和delete
搭配使用,new[]
和delete[]
搭配使用
使用实例:
#include<iostream>
using namespace std;
// 内存管理
class A {
public:
// 构造函数,初始化 _n 和动态分配数组
A(int n = 1)
:_n(n)
, _arr(new int[5] {1, 2, 3, 4, 5})
{
cout << "A 类对象构造,_n = " << _n << endl;
}
// 析构函数,释放动态分配的内存
~A() {
delete[] _arr;
cout << "A 类对象析构,_n = " << _n << endl;
}
// 打印数组的第 5 个元素
void Print() {
cout << *(_arr + 4) << endl;
}
private:
int _n;
int* _arr;
};
int main() {
// 使用 new 创建 A 类对象的数组
A* arr = new A[3]{ 1, 2, 3 }; // 分别将1,2,3依次传入3个A对象的构造函数
// 调用每个对象的 Print 函数
for (int i = 0; i < 3; ++i) {
arr[i].Print();
}
// 释放动态分配的数组内存
delete[] arr;
return 0;
}
输出结果:
A 类对象构造,_n = 1
A 类对象构造,_n = 2
A 类对象构造,_n = 3
5
5
5
A 类对象析构,_n = 3
A 类对象析构,_n = 2
A 类对象析构,_n = 1
malloc
/free
:malloc
返回的是 void*
类型的指针,需要手动进行类型转换才能赋值给其他类型的指针。例如:int* ptr = (int*)malloc(sizeof(int));
在 C++ 中,如果忘记进行类型转换,可能会导致编译错误。而且,malloc
本身并不关心所分配内存的类型,只是简单地分配指定大小的字节数。
new
/delete
:new
操作符会根据指定的类型自动计算所需的内存大小,并且返回的指针类型是正确的,无需手动进行类型转换。例如:int* ptr = new int;
malloc
/free
:malloc
只是分配一块未初始化的内存区域,分配后的内存中包含的是之前存储的任意值(即垃圾数据)。如果需要对分配的内存进行初始化,需要额外的代码来完成。例如:int* ptr = (int*)malloc(sizeof(int));
if (ptr != NULL) {
*ptr = 0; // 手动初始化
}
new
/delete
:new
操作符可以在分配内存的同时对对象进行初始化。对于内置类型,可以使用括号指定初始值;对于自定义类型,会调用相应的构造函数进行初始化。例如:int* ptr = new int(0); // 初始化 int 类型对象为 0
class MyClass {
public:
MyClass() { std::cout << "Constructor called" << std::endl; }
};
MyClass* obj = new MyClass; // 调用构造函数初始化对象
malloc
/free
:当处理自定义类型时,malloc
和 free
只是简单地分配和释放内存,不会调用对象的构造函数和析构函数。这意味着对象的资源管理和初始化工作需要程序员手动完成,否则可能会导致资源泄漏或未定义行为。例如:#include <stdlib.h>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "Constructor called" << std::endl; }
~MyClass() { std::cout << "Destructor called" << std::endl; }
};
int main() {
MyClass* obj = (MyClass*)malloc(sizeof(MyClass)); // 仅分配内存,不调用构造函数
// 手动释放内存,不调用析构函数
free(obj);
return 0;
}
new
/delete
:对于自定义类型,new
会自动调用对象的构造函数进行初始化,delete
会自动调用对象的析构函数进行资源清理。这确保了对象的生命周期管理是正确的,减少了因忘记调用构造函数或析构函数而导致的错误。例如:#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "Constructor called" << std::endl; }
~MyClass() { std::cout << "Destructor called" << std::endl; }
};
int main() {
MyClass* obj = new MyClass; // 分配内存并调用构造函数
delete obj; // 调用析构函数并释放内存
return 0;
}
malloc
/free
:malloc
在内存分配失败时会返回 NULL
,因此在使用 malloc
分配内存后,需要手动检查返回值是否为 NULL
,以处理内存分配失败的情况。例如:int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
// 处理内存分配失败的情况
}
new
/delete
:new
操作符在内存分配失败时会抛出 std::bad_alloc
异常。可以使用 try-catch
块来捕获并处理这个异常,使代码的错误处理更加结构化和清晰。例如:try {
int* ptr = new int;
} catch (const std::bad_alloc& e) {
std::cerr << "Memory allocation failed: " << e.what() << std::endl;
}
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数。
实际上,operator new
是一个加强版的可抛异常的malloc
,底层也是用malloc
分配内存,operator delete
是一个加强版的free
,底层也是free
来释放内存
new
在底层调用operator new
全局函数来申请空间,delete
在底层通过operator delete
全局函数来释放空间。
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void *p;
while ((p = malloc(size)) == 0)
{
if (_callnewh(size) == 0)
{
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
return (p);
}
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK);
__TRY
{
pHead = pHdr(pUserData);
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
}
__FINALLY
{
_munlock(_HEAP_LOCK);
}
__END_TRY_FINALLY
return;
}
// free的实现
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
对于内置类型,new
和malloc
,delete
和free
基本类似,不同的是new/delete
申请和释放单个元素空间,new[]
和delete[]
申请连续空间,并且new
申请失败时抛异常,malloc
返回NULL
。
new
的原理:先调用operator new
函数申请空间,然后在申请的空间上执行构造函数,完成对象的构造。delete
的原理:先在空间上执行析构函数,完成对象中资源的清理工作,然后调用operator delete
函数释放对象的空间。new T[N]
的原理:调用operator new[]
函数(实际在operator new[]
中调用operator new
函数完成N个对象空间的申请),然后在申请的空间上执行N次构造函数。delete[]
的原理:在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理,然后调用operator delete[]
释放空间(实际在operator delete[]
中调用operator delete
来释放空间)。定位new表达式
是在已分配的原始内存空间中调用构造函数初始化一个对象。定位 new
表达式会自动调用相应的构造函数,但需要手动调用析构函数来正确销毁对象。
class A
{
public:
A(int a = 0): _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 在 p1 所指向的内存位置上调用 A 类的默认构造函数来构造一个 A 类的对象(如果要给构造函数传参可以写成:new(p1)A(1))
p1->~A(); // 手动调用 A 类的析构函数,因为使用定位 new 构造的对象不会自动调用析构函数
free(p1); // 使用 free 函数释放之前 malloc 分配的内存
A* p2 = (A*)operator new(sizeof(A)); // 使用 operator new 分配内存并使用定位 new 构造对象
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
它的使用格式是new (place_address) type
或者new (place_address) type(initializer - list)
,place_address
必须是一个指针,initializer - list
是类型的初始化列表。
定位new表达式一般配合内存池使用,因为内存池分配出的内存没有初始化,对于自定义类型的对象,需要用定位new表达式显式调用构造函数进行初始化。
malloc
和free
是函数,new
和delete
是操作符。malloc
申请的空间不会初始化,new
可以初始化。malloc
申请空间时,需要手动计算空间大小并传递,new
只需在其后跟上空间的类型即可,如果是多个对象,在[]
中指定对象个数。malloc
的返回值为void*
,使用时必须强转,new
不需要,因为new
后跟的是空间的类型。malloc
申请空间失败时,返回的是NULL
,因此使用时必须判空,new
不需要,但new
需要捕获异常。malloc/free
只会开辟空间,不会调用构造函数与析构函数,而new
在申请空间后会调用构造函数完成对象的初始化,delete
在释放空间前会调用析构函数完成空间中资源的清理释放。