目录
三、operator new与operator delete函数
1、operator new 与operator delete函数
2、operator new 与operator 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" ;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 );}A . 栈 B . 堆 C . 数据段 D . 代码段globalVar 在哪里? ____ staticGlobalVar 在哪里? ____staticVar 在哪里? ____ localVar 在哪里? ____num1 在哪里? ____char2 在哪里? ____ * char2 在哪里? ___pChar3 在哪里? ____ * pChar3 在哪里? ____ptr1 在哪里? ____ * ptr1 在哪里? ____
C/C++程序在执行时,将内存大致划分为四个区域:
代码区 | 存放函数体的二进制代码,由操作系统进行管理 |
数据区 | 存放全局变量和静态变量以及常量 |
栈区 | 由编译器自动分配释放,存放函数的参数值,局部变量等 |
堆区 | 由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收 |
当我们程序运行前可以分为两个区域分别是:
代码区:
存放 CPU 执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
全局变量和静态变量存放在此.
数据区还包含了常量区, 字符串常量和其他常量也存放在此.
该区域的数据在程序结束后由操作系统释放
当程序运行开始:
栈区:
- 由编译器自动分配释放, 存放函数的参数值,局部变量等
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
- 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
- 在C++中主要利用new在堆区开辟内存
很明显我们平时最有可能频繁使用到的是栈区和堆区,那么栈和堆有什么区别哪?
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在 32 位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的
碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
二、内存管理方式
1、C语言中动态内存管理方式
❤malloc/calloc/realloc和free
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}
relloc是针对之前的所申请的空间再次申请空间,malloc和calloc则是形式不同单作用相同的空间申请函数。(经过前面的学习,我们知道C++是兼容C语言的,那么既然C语言中已经有了动态开辟空间的函数那为什么C++还要搞自己的空间管理方法 )
2、C++内存管理方式
1. new/delete操作内置类型
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请3个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
}
注意:申请和释放单个元素的空间,使用 new 和 delete 操作符,申请和释放连续的空间,使用 new[] 和 delete[]
2. new和delete操作自定义类型
class Test
{
public:
Test()
: _data(0)
{
cout<<"Test():"<<this<<endl;
}
~Test()
{
cout<<"~Test():"<<this<<endl;
}
private:
int _data;
};
void Test2()
{
// 申请单个Test类型的空间
Test* p1 = (Test*)malloc(sizeof(Test));
free(p1);
// 申请10个Test类型的空间
Test* p2 = (Test*)malloc(sizoef(Test) * 10);
free(p2);
}
void Test2()
{
// 申请单个Test类型的对象
Test* p1 = new Test;
delete p1;
// 申请10个Test类型的对象
Test* p2 = new Test[10];
delete[] p2;
}
注意:在申请自定义类型的空间时, new 会调用构造函数, delete 会调用析构函数,而 malloc 与 free 不会 。
3.new和delete的实现原理
如果申请的是内置类型的空间, new 和 malloc , delete 和 free 基本类似,不同的地方是: new/delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续空间,而且 new 在申请空间失败时会抛异常,malloc会返回 NULL 。
new 的原理1. 调用 operator new 函数申请空间2. 在申请的空间上执行构造函数,完成对象的构造delete 的原理1. 在空间上执行析构函数,完成对象中资源的清理工作2. 调用 operator delete 函数释放对象的空间new T[N] 的原理1. 调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请2. 在申请的空间上执行 N 次构造函数delete[] 的原理1. 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理2. 调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间
三、operator new与operator delete函数
1、operator new 与operator delete函数
/*
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 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)
2、operator new 与operator delete的类专属重载
void* operator new(size_t sz)
{
void *ptr = malloc(sz);
return ptr;
}
void operator delete(void *ptr)
{
free(ptr);
}
void* operator new[](size_t sz)
{
void *ptr = malloc(sz);
return ptr;
}
void operator delete[](void *ptr)
{
free(ptr);
}
class Test
{
public:
Test(int data = 0):m_data(data)
{
cout<<"Test::Test()"<<endl;
ptr = new int[10];
}
~Test()
{
cout<<"Test::~Test()"<<endl;
delete []ptr;
}
public:
void* operator new(size_t sz)
{
cout<<"Test::operator new"<<endl;
void *ptr = malloc(sz);
return ptr;
}
void operator delete(void *ptr)
{
cout<<"Test::operator delete"<<endl;
free(ptr);
}
private:
int m_data;
int *ptr;
};
void main()
{
Test *pt = new Test; //new操作符
delete pt;
Test *pta = new Test[10];
delete []pta;
}
以上代码关于专属重载可以理解为调用new(delete)时,若已在类内定义,则调用类内的,类内没有则调用全局的,全局的没有则调用系统的。
3、定位new表达式(placement-new)
class Test
{
public:
Test()
: _data(0)
{
cout<<"Test():"<<this<<endl;
}
~Test()
{
cout<<"~Test():"<<this<<endl;
}
private:
int _data;
};
void Test()
{
// pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
Test* pt = (Test*)malloc(sizeof(Test));
new(pt) Test; // 注意:如果Test类的构造函数有参数时,此处需要传参
}
特例:(让new指哪打哪)
void* operator new(size_t sz, int *ar, int pos)
//分配函数的第一个参数必须是“size_t”类型
{
return &ar[pos];
}
void main()
{
int ar[10] = {0};
new(ar) int(1);//此处不论括号里是什么数使用多少次new,其操作的对象永远a[0]
//为解决这个问题上面对new进行了重载
new(ar, 3) int(8);
new(ar, 8) int(8);
}
补前:赋值运算符重载
(参考剑指offer 第二版)
当面试官要求应聘者定义一个赋值运算符函数时,他会在检查应聘者 写出的代码时关注如下几点:
是否把返回值的类型声明为该类型的引用,并在函数结束前返回实 例自身的引用(*this)。只有返回一个引用,才可以允许连续赋值。 否则,如果函数的返回值是void,则应用该赋值运算符将不能进行 连续赋值。假设有3个CMyString的对象:strl、str2和str3,在程序中语句strl=str2=str3将不能通过编译。
是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制构造函数。把参数声明为引用可以避免这样的无谓消耗,能提高代码的效率。同时,我们在赋值运算符函数内不会改变传入的实例的状态,因此应该为传入的引用参数加上const关键字。
是否释放实例自身已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,则程序将出现内存泄漏。
判断传入的参数和当前的实例(*this)是不是同一个实例。如果是同一个,则不进行赋值操作,直接返回。如果事先不判断就进行赋值,那么在释放实例自身内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
经典解法
class String
{
public:
String(const char *str = "")
{
m_data = new char[strlen(str)+1];
strcpy(m_data, str);
}
String(const String &s)
{
m_data = new char[strlen(s.m_data)+1];
strcpy(m_data, s.m_data);
}
String& operator=(const String &s)
{
if(this != &s)
{
delete []m_data;
m_data = new char[strlen(s.m_data)+1]; //空间不足, 异常不安全
strcpy(m_data, s.m_data);
}
return *this;
}
~String()
{
delete []m_data;
m_data = nullptr;
}
private:
char *m_data;
};
void main()
{
String s("abcxyzhfkjDKFDKJFKAFKADJFKLDJFKLJFKLJFKLJFKLJALKFJAKLFJLKAFJL");
String s1 = s;
String s2("Hello");
s2 = s1;
}
假如s的空间非常大,可能会导致申请空间时空间不足导致申请失败,而在申请之前已经将s2原有的空间释放了,则此时不但赋值失败,而且会造成数据丢失在前面的函数中,我们在分配内存之前先用delete释放了实例m_pData的内存。如果此时内存不足导致new char抛出异常,则m_pData将是一个空指针,这样非常容易导致程序崩溃。
也就是说,一旦在赋值运算符函数内部抛出一个异常,CMyString的实例不再保持有效的状态,这就违背了异常安全性(Exception Safety)原则。
要想在赋值运算符函数中实现异常安全性,我们有两种方法。一种简单的办法是我们先用new分配新内容,再用 delete 释放已有的内容。这样只在分配内容成功之后再释放原来的内容,也就是当分配内存失败时我们能确保String的实例不会被修改。我们还有一种更好的办法,即先创建一个临时实例,再交换临时实例和原来的实例。下面是这种思路的参考
class String
{
public:
String(const char *str = "")
{
m_data = new char[strlen(str)+1];
strcpy(m_data, str);
}
String(const String &s)
{
m_data = new char[strlen(s.m_data)+1];
strcpy(m_data, s.m_data);
}
String& operator=(const String &s)
{
if(this != &s)
{
//String tmp(s.m_data);
String tmp(s);
char *ptmp = tmp.m_data;
tmp.m_data = m_data;
m_data = ptmp;
}
return *this;
}
~String()
{
delete []m_data;
m_data = nullptr;
}
private:
char *m_data;
};
void main()
{
String s("abcxyz");
String s1 = s;
String s2("Hello");
s2 = s1;
}
在这个函数中,我们先创建一个临时实例tmp,接着把 tmp.m_data和实例自身的m_data 进行交换。由于tmp是一个局部变量,但程序运行到if的外面时也就出了该变量的作用域,就会自动调用 tmp的析构函数,把 tmp.m_data 所指向的内存释放掉。由于tmp.m_data指向的内存就是实例之前m_data的内存,这就相当于自动调用析构函数释放实例的内存。
在新的代码中,我们在String的构造函数里用new分配内存。如果由于内存不足抛出诸如bad_aloc等异常,但我们还没有修改原来实例的状态,因此实例的状态还是有效的,这也就保证了异常安全性。
总结:malloc/free和new/delete的区别
malloc/free 和 new/delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:1. malloc 和 free 是函数, new 和 delete 是操作符2. malloc 申请的空间不会初始化, new 可以初始化3. malloc 申请空间时,需要手动计算空间大小并传递, new 只需在其后跟上空间的类型即可4. malloc 的返回值为 void*, 在使用时必须强转, new 不需要,因为 new 后跟的是空间的类型5. malloc 申请空间失败时,返回的是 NULL ,因此使用时必须判空, new 不需要,但是 new 需要捕获异常6. 申请自定义类型对象时, malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化, delete 在释放空间前会调用析构函数完成空间中资源的清理