内存管理

内存空间的分布

在这里插入图片描述

1、:又叫堆栈,非静态局部变量/函数参数/返回值等等, 栈中数据是从高地址向底地址存储的。

2、内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共
享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)

3、:用于程序运行时动态内存分配,堆中数据是从底地址想高地址存储的。

4、数据段:存储全局数据和静态数据。

5、代码段:可执行的代码/只读常量

C语言动态内存管理方式

申请空间的方式:malloc、calloc、realloc

相同点:

  都是C语言中从堆中申请内存,申请成功的空间都需要用户去手动释放,否则就是内存泄漏,野指针

  返回值都是void*,再使用是必须要进行强制类型转换

  如果申请失败,都会返回NULL,在使用的时候都必须要进行判空操作

不同点:

  malloc:参数是所需要空间大小的字节数,申请成功就降地址返回

  calloc:参数个数为两个,第一个是所需要的元素个数,第二个是每个元素所需要的字节数,对它申请的空间都初始化为0

  realloc:(void* p, size_t size),他最主要的方式就是对现有的空间§进行调整(size),如果p是空,他执行的行为就是malloc,申请size空间返回去。不为空分为两种情况:如果要缩小空间,对空间进行处理后就返回原来空间的首地址,但是只能用调整后的空间大小;如果要扩大空间,如果是扩大一点点并且在内存中可以继续向后延伸,执行完延伸操作后返回原空间的首地址,否则就开辟一段新空间,将原空间内容拷贝到新空间中,然后将就空间释放,返回新空间地址。

malloc

  在申请空间的时候,会在这段空间前面添加一个结构体,用来记录这段空间的信息(大小等等),系统在释放这段空间的时候就会去查看这个结构体,然后就可以知道要释放多少空间。

  申请类类型对象空间的时候,不会去调用构造函数,并且在释放的时候,不会调用析构函数,会造成内存泄漏。

c++中动态内存管理方式

new/delete:用来申请和释放对象(内置类型)空间

new[]/delete[]:用来申请时释放一段连续空间

new和delete的原理

new T 的原理:

  申请空间:通过调用void* operator new(sizeof(T))函数来实现
  使用malloc申请空间

  1、申请成功:返回类型指针。
  2、申请失败:检测是否提供空间不足的因对措施,如果提供了就执行相应的措施,然后继续申请,否则就抛出bad_alloc类型异常。
  调用构造函数初始化对象
delete的原理
  调用void operator delete(void* p)
  1、调用类中的析构函数,将p所指向的资源进行释放
  2、释放空间。
  new T[N]的原理
  1、申请空间:调用 void* operator new[] (字节数),调用operator new函数完成N个对象空间的申请
  2、调用N次构造函数
  delete[]的原理
  1、调用N次析构函数
  2、释放空间:调用void* operator delete[],调用operator delete来释放空间
  由于每次使用malloc都要在空间中额外申请一个结构,所以STL中就设置了一个类似于内存池的机制(空间配置器),一次性申请一大块空间,每次申请空间就不是直接去堆上申请,而是从空间配置器中获取空间,不用的时候将空间归还给空间配置器。
定位new表达式
  定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。new(空间地址)类类型。定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
  在释放对象的时候不能直接使用delete需要先调用该对象的析构函数,然后将空间归还,在程序运行结束后将所用的空间释放。

面试题

设计一个类只在堆上创建对象

  将类中的构造函数声明成私有的,然后给出一个静态的成员函数来new这个对象,但是还有一个问题就是拷贝构造,将拷贝构造函数禁止掉就可以:在C++98中是将拷贝构造函数声明成私有的,然后只声明不实现,在C++11中是也是将拷贝构造函数声明成私有的并且只声明不实现,但是在后面添加了=delete关键字,这样就将系统中默认生成的拷贝构造函数删除了,通过这两种方式,这个类中就不会有拷贝构造函数了。

class HeapOnly
{
public:
	static HeapOnly* CreatObject()
	{
		return new HeapOnly;
	}
private:
	HeapOnly()
	{}

	HeapOnly(const HeapOnly&) = delete;
};

设计一个对象只在栈上创建对象

  同样的,将void* operator new(size_t size)设置为私有的,但是这样做虽然不能再堆上创建对象,但是可以在全局作用域中创建对象,另一种方法就是将构造函数设置为私有的,但是这样在类外就无法创建对象,这时在类中给出一个静态的成员函数,让这个成员函数返回一个该类的无名对象就可以了。这个函数只能按照值的方式返回,因为空间是在栈上,返回一个引用肯定是会出现问题的,所以也不能去返回地址,但是创建一个对象就会调用一次拷贝构造,效率就低了,但是编译器会对我们的代码进行优化,返回一个无名对象就不会去调用拷贝构造函数,但是这种方法的效率比较低。

class StackOnly
{
public:
	static StackOnly CreatObject()
	{
		//返回一个无名对象
		return StackOnly();
	}

	
private:
	StackOnly()
	{}
private:
	void* operator new(size_t size) = delete;
	void  operator delete(void* p) = delete;

};

单例模式

设计模式

  设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的是为了代码可重用性、让代码更容易被他人理去解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式

  一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全
局访问点,该实例被所有程序模块共享。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

饿汉模式

  饿汉模式就是在程序运行之前将所有的资源一次性创建好,在之后用的时候就会很方便。由于静态的变量是在进入主函数之前就创建好了,所以要定义一个静态变量,然后将构造函数和拷贝构造设置为私有的,拷贝构造函数要只声明不定义,并在类中给出一个创建对象的静态方法,这个静态方法只能是引用返回,如果值返回,就会调用拷贝构造,但是拷贝构造调不了,即使可以调就会创建一个对象,这样就和单例模式相冲突。虽然这种方式在程序运行之后不用再去创建资源,直接调用就可以,但是如果这份资源很大,那么程序就会启动的很慢。

class Singleton 
{
public:
	static Singleton* GetInstance()
	{
		return &p;
	}

private:
	//构造函数私有
	Singleton()
	{}
	//防止被拷贝
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
private:
	static Singleton p;
};

Singleton Singleton::p;

  使用场景

  在多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞
争,提高响应速度更好。

懒汉模式

  实现过程:
  懒汉模式就是在需要的时候才会去创建 构造函数和拷贝构造函数都要设为私有的,同样的在类中给出一个创建静态的方法,并且在类中创建一个静态的指针,在调用这个方法时如果这个静态的指针是空的,就为这个指针new一个对象,返回去。如果是在多线程的环境中,就会出现多个线程同时去申请资源,这时候就要多线程中常用的加解锁操作,如果每次都要先加锁在检测,这时候所有的线程都会阻塞在加锁这里,等解锁后才可以继续操作,所以使用DCL双检锁,这时如果有一个线程在进行加解锁操作,另一个线程也过来了,这时候资源已经申请好了,这个后来的线程就可以直接返回,不用阻塞等待之前的线程。但是这样的版本仍然是有问题的,如果线程A正在进行对象的的创建,线程B过来了,但是A现在只是申请了空间,还没有进行实例化,但是B检测到这个对象不为空,直接返回去使用,就会出问题,所以要将静态对象加一个volatile来限定一下,告诉系统每次取变量里面的信息的时候不要从寄存器中取,要到内存里面取,这样就禁止了编译器对创建对象的次序进行优化(申请空间->构造对象->赋值—>申请空间->赋值->构造对象)。虽然改进了这么多,但是这个代码还存在问题,那就是没有释放空间,可能会存在内存泄漏。如果要释放,就要保证所有的线程已经用完了这份资源,但是不能在类中直接给出一个静态的释放函数,这样有可能会忘记调用这个函数,最好的方法就是内嵌一个内部类来实现释放。

#include <mutex>
#include <thread>

using namespace std;

class Singleton1
{
public:
	volatile Singleton1* GetInstance()
	{
		if (p == nullptr)//要采用DCL双检锁,让其他的线程可以不用等,直接返回。
			//这样如果编译器对代码进行了优化,将创建对象的顺序重新调整,直接返回就会出错。
		{
			m_tex.lock();//如果这里只加这一个锁,然后去判断,其他线程会阻塞在这里等待解锁。
			if (p == nullptr)
				 p = new Singleton1;
			m_tex.unlock();
		}

		return p;
	}

	//在释放资源的时候要保证所有线程已经将这份资源用完,但是不能直接在类中给出一个释放资源的函数,有可能忘记调用这个函数
	//最好的方法是在类中内嵌一个类负责资源释放
	class Clean
	{
	public:
		~Clean()
		{
			if (Singleton1::p)
			{
				delete Singleton1::p;
				Singleton1::p = nullptr;
			}
		}

	};

	static Clean c;

private:
	Singleton1()
	{}
	Singleton1(const Singleton1&) = delete;
	Singleton1& operator=(const Singleton1&) = delete;
private:
	static  Singleton1 volatile *p;
	static mutex m_tex;


};
//为对象添加volatile关键字,告诉系统取变量里面的信息的时候从内从中取,这样就禁止了编译器对变量的创建顺序进行优化
//但是这样还不够,就是没有释放空间,会造成内存泄漏。
volatile Singleton1* Singleton1::p = nullptr;
mutex Singleton1::m_tex;
Singleton1::Clean c;

  使用场景

  如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。

内存泄漏

堆内存泄露

内存泄漏的危害

  内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏检测

  堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆区中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄露

  指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

避免内存泄漏

如何避免内存泄漏

1、工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

2、采用RAII思想或者智能指针来管理资源

3、有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

4、出问题了使用内存泄漏工具检测。

malloc/free和new/delete的区别

共同点

都是从堆上申请空间,并且需要用户手动释放。

不同点

1、malloc/free是函数,new/delete是操作符。

2、malloc需要用户区手动计算空间的大小,new直接再后面跟上空间的类型就好。

3、malloc返回的是void*指针,需要用户根据需要转换,new返回的是类型指针。

4、malloc/free只是开辟所需的空间,不会调用类类型中的构造函数和析构函数,new会会调用构造函数对对象进行初始化,delete再释放空间的时候会调用析构函数对空间中的资源惊醒清理。

5、malloc申请空间失败返回的是NULL,new申请失败会抛出一个bad alloc类型的异常。

6、malloc不会对申请的空间进行初始化,new可以对申请的空间进行初始化。

7、new/delete比malloc/free的效率低一点,因为他底层封装的是malloc/free。

如何一次性再堆上申请4G的内存

1、将编译器设置为64位,如果是32位,一共就4G的内存,不可能全申请了,64位中有8G的内存,用户最多可以申请4G。

2、代码如下:

#include <iostream>

using namespace std;

int main()
{
    void* p = new char[0xffffffff];
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值