c++内存管理(一看就会,一学就睡)

目录

一、内存分布

二、内存管理方式

1、C语言中动态内存管理方式

❤malloc/calloc/realloc和free

2、C++内存管理方式

1. new/delete操作内置类型

2. new和delete操作自定义类型

3.new和delete的实现原理

三、operator new与operator delete函数

1、operator new 与operator delete函数

2、operator new 与operator delete的类专属重载

3、定位new表达式(placement-new)

总结:malloc/free和new/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++程序在执行时,将内存大致划分为四个区域:

代码区存放函数体的二进制代码,由操作系统进行管理
数据区存放全局变量和静态变量以及常量
栈区由编译器自动分配释放,存放函数的参数值,局部变量等
堆区由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

【说明】
1. 又叫堆栈,非静态局部变量 / 函数参数 / 返回值等等,栈是向下增长的。
2. 内存映射段 是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3. 用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段 -- 存储全局数据和静态数据。
5. 代码段 -- 可执行的代码 / 只读常量。

  

当我们程序运行前可以分为两个区域分别是:

代码区:
   存放 CPU 执行的机器指令
  ​ 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
  ​ 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
全局区:
   全局变量和静态变量存放在此.
  ​ 数据区还包含了常量区, 字符串常量和其他常量也存放在此.
​   该区域的数据在程序结束后由操作系统释放

当程序运行开始:

 栈区:

  • ​ 由编译器自动分配释放, 存放函数的参数值,局部变量等
  • ​ 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

堆区:

  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • ​ 在C++中主要利用new在堆区开辟内存

很明显我们平时最有可能频繁使用到的是栈区和堆区,那么栈和堆有什么区别哪?

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

空间大小:一般来讲在 32 位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的

碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

二、内存管理方式

1、C语言中动态内存管理方式

❤malloc/calloc/reallocfree

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++内存管理方式

C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此 C++ 又提出了自己的内存管理方式:通过 new delete 操作符进行动态内存管理

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. newdelete操作自定义类型

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 newoperator delete函数

1、operator new 与operator delete函数

new delete 是用户进行 动态内存申请和释放的操作符 operator new operator delete 是系统提供的 全局函数 new 在底层调用 operator new 全局函数来申请空间, delete 在底层 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)
通过上述两个全局函数的实现知道, operator new 实际也是通过 malloc 来申请空间 ,如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的。

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)

定位 new 表达式是在 已分配的原始内存空间中调用构造函数初始化ww一个对象
使用格式:
new (place_address) type 或者 new (place_address) type(initializer-list)
place_address 必须是一个指针, initializer-list 是类型的初始化列表
使用场景:
定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用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/freenew/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 在释放空间前会调用析构函数完成空间中资源的清理
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值