文章目录
- 自学网站
- 写在前面
- C/C++内存分布
- C语言内存管理
- C++内存管理
- 操作内置类型
- 操作自定义类型
- operator new & operator delete
- new & delete实现原理
- 内置类型
- 自定义类型
- 定位new表达式
- 常见面试题
- malloc/free & new/delete区别
- 内存泄漏
- 练习题
写在前面
在C语言中我们学习过内存管理方面的知识,比如malloc(), calloc(), realloc(), free()等,今天我们来看看C++是如何进行内存管理的。
C/C++内存分布
看下面一段代码,问答问题:
int g_val = 1;
static int sg_val = 1;
void Test()
{
static int s_val = 1;
int val = 1;
int num1[10] = { 1,2,3,4 };
char char2[] = "abcd";
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);
}
假设内存被划分为4个区域,分别是:栈、堆、数据段和代码段,求下面变量地址分布:
- g_val:数据段
- sg_val:数据段
- s_val:数据段
- val:栈
- num1:栈
- char2:栈
- *char2:栈
- pchar3:栈
- *pchar3:代码段
- ptr1:栈
- *ptr1:堆
注意:这里其实很简单,不过需要注意的是区分char2和char3,char2是一个字符数组,在栈上开一块空间,将常量字符串中的内容拷贝到其中;pchar3是一个字符指针,存的是常量字符串的地址,指向这个常量字符串,所以pchar3属于指针变量,是在栈上开辟空间的,但它指向的地址"abcd"在常量区(代码段)。
C/C++程序中的内存划分:
对此说明:
- 栈又叫堆栈,非静态局部变量/函数参数/返回值等,栈是向下增长的;
- 堆用于程序运行时动态内存分配,堆是可以向上增长的;
- 数据段(静态区):存储全局数据和静态数据;
- 代码段(常量区):可执行的代码/只读常量。
C语言内存管理
自行复习C语言部分的动态内存分配。
面试题:
malloc/calloc/realloc的区别?
C++内存管理
我们知道C++是兼容C语言的,所以C语言的内存管理方式在C++中可以继续使用,但是有些地方使用起来会非常麻烦,所以C++又提出了自己的内存管理方式:通过new 和delete操作符进行动态内存管理。
操作内置类型
void Test()
{
//动态申请一个int类型的空间
int* ptr1 = new int;
//动态申请一个int类型的空间并初始化为10
int* ptr2 = new int(10);
//动态申请10个int类型的空间
int* ptr3 = new int[10];
//int* ptr4 = new int[10](10);错误写法
int* ptr5 = new int[10]{ 1,2,3,4 };//没初始化的部分默认为0
//注意哦,delete时要与上面的new匹配,否则可能会报错,建议一定要匹配
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr5;
}
对于内置类型而言,用malloc和new,除了用法上的不同,其他没什么区别,它们的区别在于自定义类型,malloc之开辟空间,new是开空间+调用构造函数初始化,还有一点是malloc失败返回空指针,new失败抛异常。
注意哦,申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和delete[ ]
操作自定义类型
class Date
{
public:
Date()
:_day(0)
{
cout << "Date()" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _day;
};
void Test1()
{
//申请单个Date类型的空间
Date* p1 = (Date*)malloc(sizeof(Date));
free(p1);
//申请10个Date类型的空间
Date* p2 = (Date*)malloc(sizeof(Date) * 10);
free(p2);
}
void Test2()
{
//申请单个Date类型的空间
Date* p1 = new Date;
delete p1;
//申请10个Date类型的空间
Date* p2 = new Date[10];
delete[] p2;
}
注意哦,在申请自定义类型的空间时,new是先开辟空间再调用构造函数,delete是先调用析构函数再释放空间,与malloc和free不同。
operator new & operator delete
new和delete是进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层调用operator delete全局函数来释放空间。
注意哦,operator new和operator delete是C++的库函数,不是运算符的重载,但是这里的operator不能省略。
关于operator new函数:
/*
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 new函数底层封装了malloc函数,唯一不同的是失败的时候抛异常。
operator new和malloc功能一样,只不过失败的时候抛异常,所以operator new开辟空间后,无须检查是否开辟成功。
Stack* ps1 = (Stack*)malloc(sizeof(Stack));
assert(ps1);//需要检查是否开辟成功
free(ps1);
Stack* ps2 = (Stack*)operator new(sizeof(Stack));
operator delete(ps2);
new的底层原理:
- 调用operator new函数开辟空间;
- 调用构造函数初始化。
Stack* ps3 = new Stack;
// 1.call operator new()
// 2.call Stack构造函数
关于operator delete函数:
/*
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)
好,现在我们已经看过了两个全局函数operator new和operator delete的底层实现,我们知道,operator new实际也是通过malloc来申请空间的,不同的是申请失败时抛异常,而operator delete是也通过free来释放空间的。
new & delete实现原理
内置类型
前面我们说过,如果申请的是内置类型的空间,那么new和malloc,delete和free基本一致,只不过new失败时抛异常,malloc失败时返回NULL,而且new/delete申请和释放的是单个元素的空间,new[ ]和delete[ ]申请和释放的是连续一块空间。
自定义类型
new的原理:
- 调用operator new函数申请空间;
- 调用构造函数完成初始化操作。
delete的原理:
- 调用析构函数完成资源清理;
- 调用operator delete函数释放空间。
new T[N]的原理:
- 调用operator new[ ]函数,在operator new[ ]中实际又调用了operator new函数完成了N个对象的空间申请;
- 调用N次构造函数完成N个对象的初始化操作。
delete [ ]的原理:
- 调用N次析构函数完成N个对象的资源清理;
- 调用operator delete[ ]释放空间,实际在operator delete[ ]中调用operator delete来释放空间。
定位new表达式
这部分内容了解即可。
定位new表达式是在已分配的原始内存空间中显式调用构造函数初始化一个对象。
使用格式:
new (place_address) type;
或者
new (place_address) type (initializer_list);
其中place_address必须是一个指针,initializer_list是类型的初始化列表。
比如:
Stack* obj = (Stack*)opeartor new(sizeof(Stack));
//针对已分配好的空间,显式调用构造函数初始化
new(obj)Stack(4);
//等价于Stack* obj = new Stack(4);//不过new是在堆上开辟空间的
使用场景:
定位new表达式在实际当中多是配合内存池来使用的,因为内存池分配出的内存没有初始化。
为什么要存在内存池呢?
不向堆申请空间,而是向内存池申请,可以认为内存池离你更近,申请和释放内存更快,提高效率。
好了,这部分内容就讲这么多,大家了解即可。
常见面试题
malloc/free & new/delete区别
这个在面试中经常被问到哦!
关于malloc/free & new/delete区别这个问题,不要死记硬背,记住它们在用法和底层上的区别,理解性记忆。
malloc/free和new/delete的共同点是:
都是在堆上申请空间的,并且都需要手动释放。
不同点:
- malloc和free是函数,new和delete是操作符;
- malloc申请的空间不会初始化,new申请的空间可以初始化;
- malloc申请空间时需要手动计算空间大小,new只需要在其后跟上空间的类型即可;
- malloc的返回值为void*,在使用时必须强转,new不需要;
- malloc申请空间失败时,返回NULL,因此使用时必须判空,new不需要,但是new需要捕获异常;
- 申请自定义类型对象的空间时,malloc/free只会开辟和释放空间,不会调用构造函数和析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。
内存泄漏
什么是内存泄漏?
注意哦:内存泄漏不是说内存丢了,内存怎么会丢呢,是指针丢了!
内存泄漏指的是因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是指应用程序分配某段程序后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:
长期运行的程序出现内存泄露,影响很大,如操作系统、后台服务等等,出现内存泄露会导致响应越来越慢,最终卡死。
常见的内存泄漏:
void MemoryLeak()
{
//1.内存申请忘记释放了
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
//2.异常安全问题 —— C++11时候说
int* p3 = new int[10];
Func();//这里的Func()抛异常导致delete[] p3未执行,p3没被释放
delete[] p3;
}
如何避免内存泄漏?
这就要求我们申请和释放内存的时候要如履薄冰。
具体的方法,我们会在C++11后面的智能指针部分详细讲解。
练习题
1、下面有关c++内存分配堆栈说法错误的是( )
A.对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制;
B. 对于栈来讲,生长方向是向下的,也就是向着内存地址减小的方向;对于堆来讲,它的生长方向是向上的,是向着内存地址增加的方向增长;
C.对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题;
D.一般来讲在 32 位系统下,堆内存可以达到4G的空间,但是对于栈来讲,一般都是有一定的空间大小的。
解析:
A.栈区主要存在局部变量和函数参数,其空间的管理由编译器自动完成,无需手动控制,堆区是自己申请的空间,在不需 要时需要手动释放;
B.栈区先定义的变量放到栈底,地址高,后定义的变量放到栈顶,地址低,因此是向下生长的(栈区是先使用高地址空间,再使用低地址),堆区则相反;
C.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题;
D.32位系统下,最大的访问内存空间为4G(注意哦,这4G指的不是堆区的空间),所以不可能把所有的内存空间当做堆内存使用,故错误
2、C++中关于堆和栈的说法,哪个是错误的:( )
A.堆的大小仅受操作系统的限制,栈的大小一般较小
B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题
C.堆和栈都可以静态分配
D.堆和栈都可以动态分配
解析:
A.堆大小受限于操作系统,而栈空间一般有系统直接分配;
B.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题;
C.堆无法静态分配,只能动态分配;
D.栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放
3、c++语言中,类ClassA的构造函数和析构函数的执行次数分别为( )
ClassA *pclassa = new ClassA[5];
delete pclassa;
A.5,1
B.1,1
C.5,5
D.程序可能崩溃
解析:
A.申请对象数组,会调用构造函数5次,delete由于没有使用[],此时只会调用一次析构函数,但往往会引发程序崩溃;
B.构造函数会调用5次
C.析构函数此时只会调用1次,要想完整释放数组空间,需要使用[]
4、设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )
C c;
void main()
{
A*pa=new A();
B b;
static D d;
delete pa;
}
A.A B C D
B.A B D C
C.A C D B
D.A C B D
解析:
首先手动释放pa, 所以会先调用A的析构函数,其次C B D的构造顺序为 C D B,因为先构造全局对象,在构造局部静态对象,最后才构造普通对象,然而析构对象的顺序是完全按照构造的相反顺序进行的,所以答案为 B
注意:先构造全局对象,再构造局部静态对象,最后才构造普通对象。
5、使用 char* p = new char[100]申请一段内存,然后使用delete p释放,有什么问题?( )
A.会有内存泄露
B.不会有内存泄露,但不建议用
C.编译就会报错,必须使用delete []p
D.编译没问题,运行会直接崩溃
解析:
A.对于内置类型,此时delete就相当于free,因此不会造成内存泄漏;
C.编译不会报错,建议针对数组释放使用delete[],如果是自定义类型,不使用方括号就会运行时错误;
D.对于内置类型,程序不会崩溃,但不建议这样使用。
标签:malloc,管理,int,C++,内存,operator,new,delete From: https://blog.51cto.com/u_15495449/5807815