前言:
本篇将介绍c/c++的内存空间结构与c++中对内存进行管理的用法,包括new,delete,operator new与operator delete,定位new以及与c中malloc和free的区别等,到stl容器的底层实现篇将会对内存操作进行模拟实现,会进一步加深对内存管理的理解。
目录
3.operator new与operator delete函数(不是运算符重载)
总结: 文章对c++的内存管理进行了简单的分析与介绍,异常,内存池等内容将在后面介绍。如有错误,请指正!
1.new与delete操作符
开空间:
//开空间
int* p1 = (int*)malloc(sizeof(int));
if (p1 == nullptr)
perror("malloc fail");
int* pp1 = new int;
free(p1);
delete pp1;
- new后需要使用delete进行内存释放(调用析构函数),不然一样会造成内存泄漏。
- new后面跟要开空间的类型。
- 与malloc相比,方便的是不用手动检查开辟空间失败的问题,这与底层实现有关。
开10个int空间:
//开10个int空间
int* p2 = (int*)malloc(sizeof(int) * 10);
if (p2 == nullptr)
perror("malloc fail");
int* pp2 = new int[10];
free(p2);
delete[] pp2;
- 使用new int [10]意思就是开辟10个int空间,申请一个10个int的数组。
- 注意new开辟的空间为10,是一组对象,所以销毁时要用配套的delete []进行所有元素的销毁即调用每一个元素的析构,也就是delete只会调用第一个数组元素的析构函数。对于基本类型,没有析构函数,二者没有区别。
初始化:
//初始化
int* p3 = new int(10);
int* p4 = new int[10] {1, 2, 3, 4};
delete p3;
delete[] p4;
p3是开辟或者说申请一个int,初始化为10;p4是开辟10个int,初始化后面给的数据,其它的初始化为0。
对于自定义类型:
class A
{
public:
A(int a)
:_a(a)
{
cout << "A()" << endl;
}
private:
int _a;
};
int main()
{
//开空间
int* p1 = (int*)malloc(sizeof(int));
if (p1 == nullptr)
perror("malloc fail");
int* pp1 = new int;
free(p1);
delete pp1;
//开10个int空间
int* p2 = (int*)malloc(sizeof(int) * 10);
if (p2 == nullptr)
perror("malloc fail");
int* pp2 = new int[10];
free(p2);
delete[] pp2;
//初始化
int* p3 = new int(10);
int* p4 = new int[10] {1, 2, 3, 4};
delete p3;
delete[] p4;
A* paa = new A(2);
return 0;
}
new对于自定义类型,会调用它的构造函数进行初始化:
new之后可以传参,调用构造函数初始化:
注意如果没有默认构造,直接new是不行的;如果构造没有参数列表,new时传参也是不行的,都会报错。
new后面可以使用free释放,但是有时会出错,建议都配套使用,这与底层实现有关。
2.c/c++内存分布
这里补充复习一点内存的知识:
内存空间分布:
- 栈又叫做堆栈,用来存储非静态局部变量/函数参数/返回值等等,栈是向下增长的(也就是从高地址到低地址)。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享共享内存,做进程间通信(linux中的进程地址空间)。
- 堆用于程序运行时动态内存分配,堆是可以向上增长的。
- 数据段用来存储全局数据和静态数据。
- 代码段用来储存可执行的代码/只读常量。
来看3个例子,运用一下:
char char1[] = "abcd";
const char* pChar2 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int));
第一个,对数组名解引用*char,是存放在栈区还是常量区(代码段)?虽然字符串是存放在常量区的,但是数组存放在栈区,这里是将存放在常量区的字符串拷贝到栈区上,且这个字符数组是可以修改的。
第二个,我们要知道const修饰的是pChar2这个指针指向的空间,而pChar2这个指针是存放在栈区的;因为const修饰的是指针指向的空间,所以这块空间是在常量区的,所以这个字符数组不能修改。
第三个,ptr1是指针,是临时变量,存放在栈上的,而解引用*ptr1是代表指针指向的空间,这块空间是动态开辟的,所以是在堆上的。
3.operator new与operator delete函数(不是运算符重载)
首先强调new和delete是进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层调用operator delete全局函数来释放空间。
//失败抛异常
int* p1 = (int*)operator new(sizeof(int));
//失败返回nullptr
int* p2 = (int*)malloc(sizeof(int));
if (p2 == nullptr)
perror("malloc fail");
我们要知道,operator new是malloc的封装,operator delete是free的封装。
new对于内置类型,会直接调用operator new分配内存(也就是调用封装在operator new中的malloc);对于自定义类型,会先调用operator new分配内存,然后再调用构造函数初始化。new[]对于简单的类型,会直接计算出大小进行开空间,对于自定义类型,会在指针的前size_t个字节(不一定是4字节,应该是在指针前size_t个字节(32位是4,64位是8)大小写入数组大小,析构也是析构的指针大小减去这个size_t字节大小指向的空间,也就是说多开的用来存储数组大小的也要释放)写入数组的大小,然后调用对应次的构造函数,也就是说会额外存储数组的大小。
delete对于内置类型会直接调用free函数释放掉开辟的内存,对于自定义类型,会先调用析构函数清理掉new开辟空间中的资源,再调用operator delete也就是调用封装在其中的free释放掉这块开辟的空间。
为什么要封装呢?
机制在于失败了的处理不一样,malloc失败会直接返回空,而new或者delete失败则会抛异常(异常篇再分析)。
对于这样的,new时会先调用operator new []再调用operator new,只是多了一层封装,然后开辟空间,同时开辟空间时会在指针的前4个字节记录数组大小n,在构造时则会构造数组大小n次;delete时也是一样,先调用析构函数,会取出之前的数组大小,然后析构n次,再调用operator delete[],再调用operator delete,也就去调用free释放指针减size_t指向的这块空间(指针开始是指向new开辟的空间的,前面的存储数组大小这块空间也要释放)。
我们来看看如果不匹配使用操作符一定会报错的场景:
class A
{
public:
A()
:_a(nullptr)
{
_a = new int;
cout << "A()" << endl;
}
~A()
{
delete _a;
}
private:
int* _a;
};
int main()
{
A* p1 = new A[10];
delete[] p1;//正确
delete p1;//报错
free(p1);//报错
return 0;
}
根据之前的知识,我们就能分析:
第一个正确,调用delete[]时先调用析构,取出之前存储的数组的大小n,析构n次,然后再调用operator delete[],再调用operator delete,也就是调用free释放空间。
第二个报错,原因是没有使用配套的delete[],此时释放指针指向空间的位置就不对了:
前面的空间没有被释放,导致了内存泄露。
第三个报错,没有配套使用,释放的空间位置不对。但是当我们不写析构函数时,编译器觉得析构函数不需要调用,就不会开这4个字节了,所以拿free或者delete释放也就没有问题,但是还是实际还是要配套使用。
4.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在释放空间前会调用析构函数完成空间中资源的清理。
5.new失败后的抛异常
malloc失败后,开不到2G跳出循环。
new失败,抛异常,在异常篇会补充。
6.定位new
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type (initializer-list)
place_address为一个指针,initialize-list为类型的初始化列表。
class A
{
public:
A(int a=1)
:_a(a)
{
}
private:
int _a;
};
int main()
{
A aa;
A* p1 = (A*)malloc(sizeof(A));
if (p1 == nullptr)
{
perror("malloc fail");
}
//对一块已有的空间初始化
new(p1)A(1);
return 0;
}
那直接new不比使用malloc再定位new香吗?定位new适用于节省效率的场景,用于内存池申请的内存没有初始化:
先简单了解一下,
直接new就是直接去操作系统的堆去申请空间,然后再返回这块空间,访问慢;
而有了内存池,直接去内存池申请,申请到返回这块空间,如果没有申请到,就去
操作系统的堆上去申请大块内存,再返回给内存池,这样能提升效率。
类比去山下打水就是直接去操作系统申请空间,而去内存池是在家里的蓄水池里打水,
不够了再下山打水。