目录
operator new 和 operator delete函数
C/C++内存分配
在C语言动态内存管理章节已经了解到内存的分类,包括下面四个区域:
- 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段--存储全局数据和静态数据。
- 代码段--可执行的代码/只读常量
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);
}
在上面代码中,globalVar
为全局变量,与静态变量staticGlobalVar
和staticGlobalVar
,存放在数据段(静态区),num1
和char2
均为局部数组,所以均放在内存的栈区。对于指针类型pChar3
,ptr1
,ptr2
和ptr3
均为局部变量,所以均存放在栈区,但是pChar3
指向的内容是字符串的第一个元素的地址,该地址在内存的代码段(常量区),其余三个指针均指向在内存堆区开辟的空间
📌
注意尽管char2
中存储的是字符串,但是该字符串只是存放在代码段区域的字符串的拷贝,所以依旧在栈区
C++内存管理
C++内存管理介绍
在C语言中,使用malloc/calloc/realloc
进行动态内存空间的开辟,但是这三种方式中只有calloc会对数据区域进行初始化,并且初始化值为0,并且使用free
宏对动态分配的空间进行释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
int* ptr = (int*)malloc(sizeof(int) * 2);
assert(ptr);
int* ptr1 = (int*)calloc(2, sizeof(int));
assert(ptr1);
int* tmp = (int*)realloc(ptr, sizeof(int) * 20);
assert(tmp);
//此处不需要释放ptr原来指向的空间
ptr = tmp;
free(ptr);
free(ptr1);
return 0;
}
在C++中,仍然可以使用malloc/calloc/realloc
进行动态内存分配和free
进行空间释放,但是更推荐使用new
进行动态内存分配以及delete
进行空间释放,所以上面的代码可以转化为下面的代码
#include <iostream>
using namespace std;
int main()
{
int* ptr = new int[2];
int* ptr1 = new int[2] {0};
delete[] ptr;
delete[] ptr1;
return 0;
}
C++内存管理使用
C++内存管理基本语法
在C++中,使用new
关键字进行内存空间的申请
//单个空间开辟
指定类型的指针 = new 需要开辟的空间中的数据类型;
指定类型的指针 = new 需要开辟的空间中的数据类型(初始值);
//连续空间开辟
指定类型的指针 = new 需要开辟的空间中的数据类型[需要开辟的空间个数];
指定类型的指针 = new 需要开辟的空间中的数据类型[需要开辟的空间个数]{每一个空间初始化值};
在C++中,使用delete
关键字进行内存空间的释放
//单个空间释放
delete 指向需要释放的空间的指针;
//连续空间释放
delete[] 指向需要释放的空间的指针;
📌
注意,new
和delete
一定要匹配使用,单个空间开辟就使用单个空间的释放,连续空间的开辟就使用连续空间的释放,更不能new
和free
等交叉使用
对于内置类型来说,如下面代码
#include <iostream>
using namespace std;
int main()
{
//单个空间开辟和释放
int* ptr = new int;//不初始化
cout << *ptr << endl;
int* ptr1 = new int(10);//初始化为10
cout << *ptr1 << endl;
delete ptr;
delete ptr1;
//连续空间开辟和释放
int* ptr2 = new int[5];//不初始化
int* ptr3 = new int[5] {1, 2, 3, 4, 5};//初始化
for (int i = 0; i < 5; i++)
{
cout << ptr2[2] << " ";
}
cout << endl;
for (int i = 0; i < 5; i++)
{
cout << ptr3[i] << " ";
}
delete[] ptr2;
delete[] ptr3;
return 0;
}
输出结果:
-842150451
10
-842150451 -842150451 -842150451 -842150451 -842150451
1 2 3 4 5
对于内置类型来说,如果使用free
进行释放时,并不会出现报错
#include <iostream>
using namespace std;
int main()
{
//单个空间开辟和释放
int* ptr = new int;//不初始化
cout << *ptr << endl;
int* ptr1 = new int(10);//初始化为10
cout << *ptr1 << endl;
free(ptr);
free(ptr1);
//连续空间开辟和释放
int* ptr2 = new int[5];//不初始化
int* ptr3 = new int[5] {1, 2, 3, 4, 5};//初始化
for (int i = 0; i < 5; i++)
{
cout << ptr2[2] << " ";
}
cout << endl;
for (int i = 0; i < 5; i++)
{
cout << ptr3[i] << " ";
}
free(ptr2);
free(ptr3);
return 0;
}
输出结果:
-842150451
10
-842150451 -842150451 -842150451 -842150451 -842150451
1 2 3 4 5
尽管也正常输出,但是依旧不建议使用free
对new
开辟的空间进行释放
对于自定义类型来说,分两种情况:
- 自定义类型中的成员变量仅为内置类型
- 自定义类型中的成员变量有指针指向的动态内存分配的空间
#include <iostream>
using namespace std;
//成员变量仅为内置类型
class test
{
private:
int _num;
public:
test(int num = 1)
:_num(num)
{
cout << "test()" << endl;
}
~test()
{
cout << "~test()" << endl;
}
};
int main()
{
test* ptr = new test;
cout << endl;
test* ptr1 = new test(2);
cout << endl;
test* ptr2 = new test[5]{ 1,2,3,4,5 };
cout << endl;
delete ptr;
cout << endl;
delete ptr1;
cout << endl;
delete[] ptr2;
return 0;
}
输出结果:
test()
test()
test()
test()
test()
test()
test()
~test()
~test()
~test()
~test()
~test()
~test()
~test()
在上面的代码中,对于单个对象空间的分配时,会调用一次构造函数,而对连续空间的对象空间分配时,会调用对象个数次构造函数。对于空间释放,当只有一个对象时,只调用一次析构函数,当有连续空间的对象时,则会调用对象个数次析构函数
如果将上面代码中的delete
改为free
,则不会调用析构函数
#include <iostream>
using namespace std;
//成员变量仅为内置类型
class test
{
private:
int _num;
public:
test(int num = 1)
:_num(num)
{
cout << "test()" << endl;
}
//~test()
//{
// cout << "~test()" << endl;
//}
};
int main()
{
test* ptr = new test;
cout << endl;
test* ptr1 = new test(2);
cout << endl;
test* ptr2 = new test[5]{ 1,2,3,4,5 };
cout << endl;
free(ptr);
cout << endl;
free(ptr1);
cout << endl;
free(ptr2);
return 0;
}
输出结果:
test()
test()
test()
test()
test()
test()
test()
但是此时编译器会报警告:
“ptr”使用“new”分配,但使用“free”删除
“ptr2”使用“new []”分配,但使用“free”删除
“ptr1”使用“new”分配,但使用“free”删除
在上面的代码中,使用new
为自定义类型的对象开辟空间时会调用构造函数,但是释放空间时使用free
编译器会给出警告,如果没有删除析构函数,那么程序将运行终止,所以不要使用free
和delete
交叉使用
另外对于下面的代码来说
#include <iostream>
using namespace std;
//成员变量仅为内置类型
class test
{
private:
int _num;
public:
test(int num = 1)
:_num(num)
{
cout << "test()" << endl;
}
~test()
{
cout << "~test()" << endl;
}
};
int main()
{
test* ptr = new test;
cout << endl;
test* ptr1 = new test(2);
cout << endl;
test* ptr2 = new test[5]{ 1,2,3,4,5 };
cout << endl;
delete ptr;
cout << endl;
delete ptr1;
cout << endl;
delete ptr2;
return 0;
}
因为ptr2
指向的空间是连续的对象空间,此时使用对单个空间释放形式的delete
时,程序也会终止运行,所以单个空间开辟使用单个空间释放,多个空间开辟使用多个空间释放
#include <iostream>
using namespace std;
//对应有资源申请的内置类型来说
class test
{
private:
int* _a;
public:
test()
:_a(nullptr)
{
_a = new int[5];
cout << "test()" << endl;
}
~test()
{
delete[] _a;
cout << "~test()" << endl;
}
};
int main()
{
//单个空间开辟和释放
test* ptr = new test;
cout << endl;
delete ptr;
cout << endl;
//多个空间开辟和释放
test* ptr1 = new test[5];
cout << endl;
delete[] ptr1;
cout << endl;
return 0;
}
输出结果:
test()
~test()
test()
test()
test()
test()
test()
~test()
~test()
~test()
~test()
~test()
在上面的代码中,类成员变量的类型为指针类型,在构造函数中,为该指针分配了5个连续的int
类型空间,在析构函数中,释放这5个连续的空间。当类实例化对象时,对于单个空间的开辟和释放会调用一次构造函数和一次析构函数,而对于连续空间的开辟和释放会调用连续空间个数次的构造函数和析构函数
#include <iostream>
using namespace std;
//对应有资源申请的内置类型来说
class test
{
private:
int* _a;
public:
test()
:_a(nullptr)
{
_a = new int[5];
cout << "test()" << endl;
}
~test()
{
delete[] _a;
cout << "~test()" << endl;
}
};
int main()
{
//单个空间开辟和释放
test* ptr = new test;
cout << endl;
free(ptr);
cout << endl;
return 0;
}
对于单个空间的释放来说,不会出现程序崩溃,尽管没有调用析构函数释放类对象成员变量指向的内存空间,但是当程序结束时,该空间会得到释放,此时也即内存泄漏
#include <iostream>
using namespace std;
//对应有资源申请的内置类型来说
class test
{
private:
int* _a;
public:
test()
:_a(nullptr)
{
_a = new int[5];
cout << "test()" << endl;
}
~test()
{
delete[] _a;
cout << "~test()" << endl;
}
};
int main()
{
//多个空间开辟和释放
test* ptr1 = new test[5];
cout << endl;
free(ptr1);
cout << endl;
return 0;
}
对于多个连续空间的释放来说,因为free
只能释放一个空间,如果直接理解会认为释放掉了连续空间中的第一个空间而并未释放后面的空间导致程序内存泄漏错误。但是实际上并不是,因为内存泄漏只有在没有足够内存时才会导致程序崩溃,编译器是不会检查内存泄露的。而真正导致程序崩溃的原因是VS在此处做出的优化
而之所以free
释放空间时会报错,就是因为返回的起始位置并不是开辟的空间的真正起始位置从而导致程序崩溃,free
释放空间时必须从开辟的空间的实际起始位置开始,而不能释放部分空间,而delete
(不是delete[]
)的底层设计也是free
(只是做了一些异常检查),所以调用delete
而不是delete[]
也会导致程序崩溃,而delete[]
不崩溃是因为在释放连续空间时会先向前移动找到存储空间个数的值,再通过该值确定调用析构函数的次数,最后销毁指向连续空间的指针
总结:
对于内置类型来说,因为内置类型开辟的空间如果使用free
也是直接释放指针指向的空间,只是不会调用析构函数,一般情况下不会出现问题,但是不推荐使用free
。建议对单个空间用delete
,连续空间用delete[]
对于自定义类型来说,使用free
比较容易产生内存泄漏,所以不论是成员变量是内置类型,还是成员变量是涉及资源申请的,都建议使用delete
和delete[]
operator new 和 operator delete函数
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数,new
在底层调用operator new
全局函数来申请空间,delete
在底层通过operator delete
全局函数来释放空间
operator new
实际也是通过malloc
来申请空间,operator delete
最终是通过free
来释放空间的
📌
注意operator new
和 operator delete
函数不是运算符重载
内置类型:
对于operator new
函数来说
如果是单个空间,那么在申请空间时使用new
,底层会调用malloc
函数开辟单个空间,但是不同于malloc
函数,operator new
如果空间申请失败会抛出异常,而不是返回空指针
如果是连续空间,则在申请空间时new[]
,底层会调用malloc
函数开辟连续的空间
对于operator delete
函数来说
如果是单个空间,那么在释放空间时使用delete
,底层会调用free
函数释放单个空间,但是不同于free
宏,operator delete
会在释放时检查一些可能存在的问题
如果是连续空间,则在申请空间时delete[]
,底层会调用free
函数开辟连续的空间
自定义类型:
对于operator new
函数来说
如果是单个空间,则在申请空间时使用new
,会调用operator new
申请空间,再在申请的空间上调用构造函数创建对象
如果是多个连续空间,则在申请空间时使用new[]
,会调用operator new
申请连续的空间,再在申请的连续空间上调用申请空间个数次的构造函数创建对象
对于operator delete
函数来说
如果是单个空间,则在释放空间时使用delete
,释放空间时先调用类的析构函数,再调用operator delete
函数释放指向单个存储对象空间的指针
如果是连续空间,则在释放空间时使用delete[]
,释放空间时先调用类的析构函数,调用次数为创建的对象的个数,最后调用operator delete
函数释放指向存储对象的连续空间的指针
定位new
表达式(placement-new
)
定位new
表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
基本语法
new (开辟空间的起始地址) 类型或者new (开辟空间的起始地址) 类型(初始值)
使用场景
定位new
表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new
的定义表达式进行显式调构造函数进行初始化
#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()
{
// p1现在指向的只不过是A类大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
// 在空间中调用构造函数创建对象
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();// 析构函数可以显式调用
free(p1);
A* p2 = (A*)operator new(sizeof(A));// 也可以直接使用operator new函数开辟空间
// 在空间中调用构造函数创建对象
new(p2)A(10);
p2->~A();
delete p2;
return 0;
}
malloc/free和new/delete
相同点
malloc/free
和new/delete
的共同点是:都是从堆上申请空间,并且需要用户手动释放
不同点
malloc
和free
是函数,new
和delete
是操作符malloc
申请的空间不会初始化,new
可以初始化malloc
申请空间时,需要手动计算空间大小并传递,new
只需在其后跟上空间的类型即可, 如果是多个对象,[]
中指定对象个数即可malloc
的返回值为void*
, 在使用时必须强转,new
不需要,因为new
后跟的是空间的类型malloc
申请空间失败时,返回的是NULL
,因此使用时必须判空,new
不需要,但是new
需要捕获异常- 申请自定义类型对象时,
malloc/free
只会开辟空间,不会调用构造函数与析构函数,而new
在申请空间后会调用构造函数完成对象的初始化,delete
在释放空间前会调用析构函数完成空间中资源的清理