前言:
😘我的主页:OMGmyhair-CSDN博客
目录
一、C/C++内存分布
二、C语言中动态内存管理方式:malloc/calloc/realloc/free
malloc:
calloc:
realloc:
free:
三、C++内存管理方式
1.用new/delete操作内置类型
2.用new/delete操作自定义类型
3.operator new和operator delete函数
一、C/C++内存分布
二、C语言中动态内存管理方式:malloc/calloc/realloc/free
malloc:
在c语言中,我们可以使用malloc进行动态申请内存:
int main()
{
int* p = (int*)malloc(sizeof(int) * 2);
return 0;
}
从上面我们可以看到我们申请了8个字节大小的空间。malloc函数的返回值是void*类型的指针,指向已经开辟好的空间的首地址。我们可以通过强转,转为自己需要的类型。我们用malloc申请到的空间,里面没有初始化,值是不确定的。
如果申请空间失败,将会返回空指针。
calloc:
int main()
{
//malloc:
int* p = (int*)malloc(sizeof(int) * 2);
//calloc:
int* pc = (int*)calloc(2, sizeof(int));
return 0;
}
在上面代码中,我们用calloc申请了2个大小为int的空间。calloc函数的返回值是void*类型的指针,指向已经开辟好的空间的首地址。它与malloc的区别在于,calloc申请到的空间每个比特位都会初始化为0。
realloc:
int main()
{
//malloc:
int* p = (int*)malloc(sizeof(int) * 2);
//calloc:
int* pc = (int*)calloc(2, sizeof(int));
//realloc:
pc = (int*)realloc(pc, sizeof(int) * 4);
return 0;
}
realloc可以用来重新申请空间,第一个参数是原空间的地址,第二个参数是新内存块的大小。
如果在原空间的地址上不能往后继续申请内存(后面的位置被占用了),那么realloc会重新开辟一块新内存空间,将原空间上的数据搬到新内存空间,并且对原空间进行释放。新内存空间对于原空间如果更大了,那么多出来的那部分是没有进行初始化的。
如果第一个参数是空指针,那么此时realloc的作用类似于malloc。
当realloc申请空间失败,会返回空指针,但是原空间依旧有效且数据还在。我们用realloc申请巨大的空间来模拟申请空间失败的情况:
int main()
{
int* p = (int*)calloc(2, sizeof(int));
cout<<"realloc前p的地址:" << p << endl;
int* pp = p;
p = (int*)realloc(p, sizeof(int) * 1024*1024*1024*1024);
cout <<"realloc后p的地址:" << p << endl;
cout << "原空间的第一个数:" << pp[0] << endl;
return 0;
}
结果:
所以我们在使用realloc的时候,要小心申请失败的情况:
int main()
{
int* p = (int*)calloc(2, sizeof(int));
int* ppr = (int*)realloc(p, sizeof(int) * 24);
if (ppr != NULL)
{
p = ppr;
}
return 0;
}
free:
如果ptr没有指向由calloc、realloc、malloc开辟的空间,那么产生的结果是不确定的。
但如果ptr是空指针,不会做任何事情。
需要注意的是,free后不会改变ptr的值,ptr依旧指向那片空间,只是此时你在去使用这块空间是非法的。
此处插播一条知识点,为什么在32位环境下指针大小是4个字节?而在64位环境下指针大小是8个字节?
举个例子,当行李箱上假设有3位密码,每一位的范围是从0~9,我们要用多少位数可以表示全部的密码呢?答案是
一个字节8个比特位。
首先,指针装的是地址,在32位系统下,内存地址空间大小是。当我们表示地址时,每一位的范围是0~1,一共32个比特位,地址也就需要用32个比特位去表示,也就是4个字节。
那么64位环境下就更好理解了。在64位系统下,内存地址空间大小是。当我们表示地址时,每一位的范围是0~1,一共64个比特位,地址也就需要用64个比特位去表示,也就是8个字节。
三、C++内存管理方式
1.用new/delete操作内置类型
int main()
{
//动态申请一个int大小的内存空间
int* p1 = new int;
//动态申请一个int大小的内存空间,并且初始化为1
int* p2 = new int(1);
//动态申请一个3个int大小的内存空间
int* p3 = new int[3];
//动态申请一个3个int大小的内存空间,并且初始化为0、1、2
int* p4 = new int[3] {0, 1, 2};
delete p1;
delete p2;
delete[]p3;
delete[]p4;
return 0;
}
当我们申请/释放单个元素的空间时,使用new/delete。当我们申请或者释放连续的空间时,使用new[]或者delete[]。注意要搭配起来使用,如果不搭配使用的后果在文章的后面会讲到。
2.用new/delete操作自定义类型
在这里,new/delete和malloc/free的区别就明显体现出来了。
class A
{
public:
A(int a1 = 1, int a2 = 2)
:_a1(a1)
,_a2(a2)
{
cout << "A(int a1 = 1, int a2 = 2)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
cout << "malloc:" << endl;
A* ma = (A*)malloc(sizeof(A));
cout << "-------------------------------------" << endl;
cout << endl << "new:" << endl;
A* na = new A;
cout << "-------------------------------------" << endl;
cout << endl << "free:" << endl;
free(ma);
cout << "-------------------------------------" << endl;
cout << endl << "delete:" << endl;
delete na;
cout << "-------------------------------------" << endl;
return 0;
}
看看运行结果:
可以看到对比malloc,new会去调用自定义类型的构造函数。而对比free,delete会去调用自定义类型的析构函数。
对于内置类型而言new/delete和malloc/free是几乎一样的。
3.operator new和operator delete函数
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
总结一下:
4.new和delete的实现原理
(1)内置类型
如果是对内置类型进行申请空间,new/delete和malloc/free基本类似。不同的地方在于,new/delete申请和释放的是单个元素空间,new[]/delete[]申请和释放的是连续的空间,而且new在申请空间失败时会抛出异常而malloc是返回空指针。
int main()
{
//抛异常
try
{
while (1)
{
int* p = new int[1024 * 1024];
int* p1 = new int[1024 * 1024];
}
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
(2)自定义类型
new和delete对于自定义类型的原理,前面已经描述过,这里就不再赘述。
(2)-1 new [N]的原理
1.调用operator new[]函数,这里的operator new[]其实就是调用operator new实现对N个对象空间申请
2.在申请的空间上进行N次构造函数
(2)-2 delete[]的原理
1.在将要释放的对象空间上调用N个析构函数,实现对N个对象的资源清理。
2.调用operator delete[]释放空间,实际上就是调用operator delete来释放空间。
5.new/delete和new[]/delete[]错误搭配以及使用free进行释放new
(1)内置类型
int main()
{
int* p = new int[3];
delete p;
return 0;
}
这个代码有什么危险吗?会产生内存泄漏吗?
答案是什么都不会发生。
因为new[]归根结底还是malloc,delete归根结底也还是free。所以在这个场景下你甚至还能用free来释放空间(不推荐)。
(2)自定义类型
1.场景1
如果我们用free去释放new的空间,编译器不会报。我们也会发现free不会调用析构函数。如果我们在析构函数中有资源的释放,可能会造成内存泄漏。
2.场景2
class A
{
public:
A(int a1 = 1, int a2 = 2)
:_a1(a1)
,_a2(a2)
{
cout << "A(int a1 = 1, int a2 = 2)" << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A* p1 = new A[3];
delete p1;
return 0;
}
正常通过。(注意和场景3的对比)
3.场景3
class B
{
public:
B(int b1 = 1, int b2 = 2)
:_b1(b1)
, _b2(b2)
{
cout << "B(int b1 = 1, int b2 = 2)" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int _b1;
int _b2;
};
int main()
{
B* p1 = new B[3];
delete p1;
return 0;
}
查看运行结果:
对比与场景2,同样是用delete对new[]申请的资源进行释放,为什么B类运行时就产生了崩溃呢?
delete[]与delete的区别
我们通过比较可以发现,B跟A相比多了析构函数。当用new[]申请多个B类对象的空间时,new[]其实会在有析构函数的类前面多开出4个字节的空间来存储对象个数。因为你有析构函数,编译器怕你析构函数中有释放资源的操作,所以会记下对象的个数来多次调用析构函数避免内存泄漏。如果你没有析构函数,编译器认为没有释放资源,也就懒得给你另开空间记个数了:
A类和B类大小一样,都是8个字节,2个int成员变量。我们来看看new[]对A类开辟了多少空间,对B类又开辟了多少空间。
我们都知道不能对申请的空间只释放一部分。当B类有析构时,使用delete进行释放就是只对申请的空间释放一部分,导致了程序崩溃。所以我们在使用new/delete和new[]/delete[]时,一定要搭配使用,以免造成错误。
6.定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式如下:
在这里,我们用operator new或者malloc去开辟空间,这里不调用构造,用operator new不用像malloc一样检查返回值,因为operator new直接抛异常。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)operator new(sizeof(A));//开辟空间
new(p1)A(1);//调用构造函数
p1->~A();//调用析构函数
operator delete(p1);//释放空间
A* p2 = (A*)malloc(sizeof(A));//开辟空间
new(p2)A();//调用构造函数
p2->~A();//调用析构函数
free(p2);//释放空间
return 0;
}
你可能会觉得这不是脱裤子放屁吗?为什么不直接使用new一键开辟空间+调用构造函数呢?
这里要引入一个概念,池化技术——提高性能,像类似内存池、线程池、连接池都属于池化技术。而我们这里涉及的就是内存池。
我们可以将内存池比作一个受到管理的大湖,周围的人都需要到这里来打水。而山上有座寺庙,频繁用水经常排队。因此干脆专门给寺庙开了一个小水洼,这个小水洼供寺庙专用,当小水洼中的水不够了就可以去大湖要。而这里的内存池就是小水洼,堆就是大湖。
使用定位new表达式也是为了提高效率。
7.malloc/free和new/delete的区别
共同点:
都是从堆上申请空间,并且需要用户手动释放。
不同点:
如果这篇文章有帮助到你,请留下您珍贵的点赞、收藏+评论,这对于我将是莫大的鼓励!学海无涯,共勉!😘😊😗💕💕😗😊😘