栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++ |【总结归纳三本书籍系列】一文透彻资源管理,动态内存分配....

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++ |【总结归纳三本书籍系列】一文透彻资源管理,动态内存分配....

前言

本章内容结合了三本C++书籍归纳总结,讲述了动态内存分配,智能指针等用法以及使用中的注意事项。

文章目录
    • 前言
    • 一、回顾内存中是如何分区的
      • 1.1 程序运行前,内存中的变化
      • 1.2 程序运行后,内存中的变化
      • 1.3 malloc内存块
    • 二、 动态内存
      • 2.1 内存分配
      • 2.2 new与delete的搭配
        • 2.2.1 new
        • 2.2.2 delete
        • 2.2.3 使用operator new/delete的原因
        • 2.2.4 自定义operator new/delete(案例)
      • 2.3 使用动态内存的三大原因
      • 2.4 new_handle
      • 2.5 =delete与=default
    • 三、智能指针
      • 3.1 shared_ptr类
        • 3.1.1 shared_ptr计数器
        • 3.1.2 make_shared函数
        • 3.1.3 智能指针的自动销毁
        • 3.1.4 share_ptr与new结合使用
        • 3.1.5 shared_ptr与动态数组
      • 3.2 unique_ptr
        • 3.2.1 与shared_ptr的不同
        • 3.2.2 unique删除器
      • 3.3 weak_ptr
      • 3.4 allocator类

一、回顾内存中是如何分区的

程序执行过程分为四个区域:

  • 代码区:存放函数体二进制代码,由系统管理;
  • 全局区:存放全局变量和静态变量以及常量;
  • 栈区:由编译器自动分配释放,存放函数的参数值以及局部变量;
  • 堆区:由程序员分配和释放,若不释放,则系统结束后会自动回收;
1.1 程序运行前,内存中的变化

当程序编译成功后,即生成了exe可执行文件,在未开始执行该程序前:
代码区:

  • 存放cpu执行的机器命令;
  • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要内存中有一份代码即可;
  • 代码区是只读的,原因是防止程序意外地修改了它的指令;

全局区:

  • 存放静态变量和全局变量;
  • 全局区包含了常量区,存放常量(不包括局部常量);
  • 该区域的数据在程序结束后由系统释放;
1.2 程序运行后,内存中的变化

由编译器自动分配释放,存放函数的参数值,局部变量;

  • 不要返回局部变量的地址,栈区开辟的数据由编译器自动释放;
1.3 malloc内存块

使用malloc会增加额外的不必要的开销;上下会带上总数为8字节的cookie;

  • 由于是16的倍数,则字节数不够会自动填充间隔,进一步造成空间上的浪费
  • 例如:申请一块12字节,则使用malloc会造成

二、 动态内存

由上述回顾可知,程序中有一个内存池供程序员自行分配管理。在C语言中使用可malloc和free对该内存的申请和释放,而在C++中简化了该语句使用new和delete对内存的申请和释放;

2.1 内存分配

内存泄漏

若分配的内存不使用且不将该内存归还,则会造成内存泄漏;
应用开发的常见资源有:

  • 文件描述符;
  • 互斥锁;
  • 数据库连接;
  • sockets;
2.2 new与delete的搭配 2.2.1 new

在自由空间分配的内存是无名的,故new无法为其分配的对象命名,单返回一个指向该对象的指针;

当使用new创建一个对象时:

  • 内存被分配出来operator new
  • 针对此内存会有一个/多个构造函数被调用;
int *p = new int(); // 默认初始化为0
int *p1 = new int;  // 默认初始化,未定义

当申请一个const对象

const必须在进行动态分配的时候就进行初始化;

const int *p = new const int(1);

bad_alloc

当new的内存分配的空间没有达到要求时,会抛出异常,而bad_alloc会阻止抛出异常;

int *p = new (nothrow) int;
2.2.2 delete

当delete释放一个对象时:

  • 针对此内存会有一个/多个析构函数被调用;
  • 内存释放通过operator delete;
  • 编译器不能辨别该指针是否被释放;
  • 数组中释放按照逆序释放;

动态内存管理易出错

  • 没有释放内存造成内存泄漏;
  • 使用已释放的对象;
  • 释放过的内存再次释放;

注意delete的释放单一对象和数组对象的区别

由于单一对象的内存布局一般不同于数组的内存布局;

  • 数组的内存通常会记录数组的大小,便于delete释放空间时,析构调用的次数;
  • 释放数组对象时,需添加[]告诉编译器该对象为数组对象;
delete 对象;
delete[] 数组对象;	

空悬指针:

当指针被释放后,被称为空悬指针;

  • 为了保险起见,将释放后的内存赋值nullptr即不指向任何对象;
2.2.3 使用operator new/delete的原因

用来检测运用上的错误

使用new可能出现的问题:

  • 若使用new,可能出现delete失败,而导致内存泄漏;
  • new的内存,若出现多次delete将会导致未定义行为;

而使用operator new:

  • 可将会持有一串动态分配的地址,而operator delete将会容易检测出错误用法;

为了强化效能

operator new不但可用于长时间执行的程序,也可用于少于1秒的程序

为了收集使用上的统计数据

可轻松收集分配区块的带下分布情况,寿命分布情况,倾向哪个次序分配和归还,运用型态是否随时间改变;

2.2.4 自定义operator new/delete(案例)

在许多情况下需要自定义(全局性/class专属)来提高程序对内存操作的效率;

  • 为了检测运用错误;
  • 为了收集动态分配内存值使用统计信息;
  • 为了增加分配和归还的速度;
    • 自定义将针对与某特定类型的对象设计的;
  • 为了降低缺省内存管理带来的空间额外开销;
    • 泛用型内存管理器内部会使用其他内存开销;
  • 为了弥补缺省分配器中的非最佳齐位;
    • 泛用型内存管理器的动态分配采取的齐位并不确定;
  • 为了将相关对象成簇集中;
    • 将数据集中成簇在尽可能少的内存页中;
  • 为了获得非传统的行为;
    • 实现一些功能,而不使用传统的编译器定义的功能;

自定义如何能节约内存地址的空间

#include


class MyTestOper
{
public:
	
	MyTestOper(int x) : i(x){};
	
	
	int get_i(){ return i; }

	
	void* operator new(size_t);

	
	void operator delete(void *);
private:
	MyTestOper *next;
	static MyTestOper* freeP;
	static const int chunkNum;		// 内存块的大小
private:
	int i;
};

MyTestOper* MyTestOper::freeP = 0;
const int MyTestOper::chunkNum = 24;


void *MyTestOper::operator new(size_t size)
{
	MyTestOper *p = 0;
	if (!freeP)	// 判断内存是否为空
	{
		size_t chunk = size * chunkNum;			// 块的大小
		freeP = p = reinterpret_cast(new char[chunk]);
		
		int x = 0;
		for (; p != &freeP[chunkNum - 1]; ++p)
			p->next = p + 1;
		p->next = 0;	// 此时p指向尾部
	}

	p = freeP;				// 将p指向头部
	freeP = freeP->next;	// 而freeP指向下一个
	return p;
}


void MyTestOper::operator delete(void * delP)
{

	(static_cast(delP))->next = freeP;
	freeP = static_cast(delP);
}



class MyTestT
{
public:
	
	MyTestT(int x) : i(x){};

	
	int get_i(){ return i; }

private:
	MyTestT *next;
	static MyTestT* freeP;
	static const int chunkNum;		// 内存块的大小
private:
	int i;
};


void main()
{
	std::cout << "--------------重载的结果--------------" << std::endl;
	std::cout << sizeof(MyTestOper) << std::endl;
	size_t const N = 10;
	MyTestOper *p[N];

	for (int i = 0; i < N; ++i)
	{
		p[i] = new MyTestOper(i);
		std::cout << p[i] << std::endl;
	}
	for (int i = 0; i < N; ++i)
	{
		delete p[i];
	}

	std::cout << "--------------不重载的结果--------------" << std::endl;
	std::cout << sizeof(MyTestT) << std::endl;
	MyTestT *pt[N];

	for (int i = 0; i < N; ++i)
	{
		pt[i] = new MyTestT(i);
		std::cout << pt[i] << std::endl;
	}
	for (int i = 0; i < N; ++i)
	{
		delete pt[i];
	}

	system("pause");
}

2.3 使用动态内存的三大原因
  • 程序员不确定需要使用多少个对象;
  • 程序员不清楚所需对象的数据类型;
  • 程序间需要多个对象共享数据;
2.4 new_handle

当内存使用不足,无法申请内存时会抛出一个std::bad_alloc exception的异常。但编译器设置,在抛出异常前,可指定调用new_handle。

  • 故我们可以使用new_handle对使用的内存进行检查,并即使清理被暂用且没有使用的内存从而让这次的申请成功;
  • 通过set_new_handle传入;
  • new_handle中一般实现这个两种处理方式:
    • 释放部分内存,让内存被申请成功;
    • 调用abort()或exit();
void new_handle()
{
	…………
}

new_handle set_new_handle(new handle p) throw();
2.5 =delete与=default

在C++类中可使用以上两种来对函数成员的限制;

  • 在一个类种编译器提供了以下几种默认函数:
    • 默认构造函数
    • 默认析构函数
    • 默认拷贝构造函数
    • 默认赋值函数
    • 移动构造函数
    • 移动拷贝函数

=delete:禁用该函数;
=default:使用编译器提供的默认函数;

三、智能指针

智能指针与普通指针的最大区别是能够自动释放内存;

3.1 shared_ptr类

定义

shared_ptr<数据类型> p;

当默认初始化时,智能指针中保存着一个空指针;

  • 使用方式与普通方式相同,可将其解引用即返回指向的对象;
3.1.1 shared_ptr计数器

当计数器为0时,智能指针将会自动释放;

何时计数器会递增

  • 当使用拷贝时shared_ptr p(q)p时q的拷贝,计数器会递增;
  • 当两个智能指针相互转换时,左边的指针对象会增加;
  • 当它作为参数传递时;
  • 当它作为函数返回值时;

何时计数器会递减

  • 当两个智能指针相互转换时,右边的指针对象会递减;
  • 当智能指针被销毁时;

独有操作:

  • make_shared p(参数):将会初始化一T数据类型的p对象;
  • shared_ptr p(q):拷贝操作,将q拷贝到p;
  • p = q:将q的智能指针转换为p;
  • unique():判断引用数是否为1,若为1,则返回true,否则返回flase;
  • use_count():返回该智能指针对象的引用数;
  • get():返回该智能指针;
  • swap(q, p):交换指针;
3.1.2 make_shared函数

是最安全的分配和使用动态内存的方法;

shared_ptr p = make_shared(参数);
auto p = make_shared(参数);
3.1.3 智能指针的自动销毁

当智能指针的计数器为0时,shared_ptr类会调用析构函数将其释放;

3.1.4 share_ptr与new结合使用

使用new返回的指针来初始化智能指针;

  • 一般初始化智能指针的指针必须指向动态内存;
  • 由于之智能指针内部的构造函数是explicit,不能进行隐式转换,必须使用直接初始化;
  • 由于不清楚对象何时销毁,则最好不使用内置指针来访问一个智能指针且不使用get()初始化或赋值;
shared_ptr p(new T(1));

当new与share_ptr作为参数时

// 函数声明
int func();
void test(std::shared_ptr p, int func);

由于智能指针内部的构造函数是explicit,故不能直接传入new创建的指针;

// 可考虑使用以下:
test(std::shared_ptr (new int), func());

第一个参数被分为俩部分:std::shared_ptr与new int;
由于编译器考虑高效的操作,故传入参数的执行顺序:

  • 执行new int;
  • 调用func;
  • 调用智能指针构造;

但需要考虑到,万一func函数内部出现异常,则此时将会引起内存泄漏,由于new int返回的指针遗失;


故将实参一先单独出来:

std::shared_ptr p(new int);
test(p, func());
3.1.5 shared_ptr与动态数组

该对象不支持管理动态数组,若要使用,则必须定义删除器,作为参数传入;

  • 若无提供,则该代码是未定义;
3.2 unique_ptr

它在某个时刻只能有一个unique_ptr指向一个给定对象;

3.2.1 与shared_ptr的不同
  • 不支持普通拷贝操作;
    • 但支持拷贝或赋值一个将要销毁的unique_ptr,用于参数传递以及函数返回值;
  • 不支持赋值操作;
  • 可通过内置函数对其进行转移;
    • relase():放弃对指针的控制权,返回指针,并将原指针置空;
    • reset():将其重置给其他指针;
3.2.2 unique删除器
del为释放T对象,func为del的函数
unique_ptr p(1, func);

在创建或者转移该类型对象时,必须提供一个指定类型可调用的删除器;

  • 但重载一个该对象的删除其将有可能回影响到unique_ptr类型以及如何构造该类型的对象;
3.3 weak_ptr

它是一种不会控制所指向对象生存期的智能指针,用于指向一个shared_ptr对象,但不会改变shared_ptr不会改变引用计数;

shared_ptr p = make_shared_ptr();
weak_ptr p2(p);

注意事项

由于指向是一个shared_ptr对象,则无法知道shared_ptr何时被释放。故提供了内置函数需要调用lock函数判断是否存在;

  • 若存在则返回shared_ptr对象。
3.4 allocator类

无默认构造的类,无法动态分配数组;

allocator会让我们将内存分配和对象构造分离开;

  • 当其分配内存时,会根据给定的对象类型来确定恰当的内存大小和对齐位置;
  • 为了使用allocate返回的内存,必须要用construct`构造对象;
allocator = p;
auto const q = p.allocate(n); // 分配一段原始的,为构造的的n个未初始化的T;

自定义allocator


#include 

class allocator
{
private:
	struct obj{
		struct obj *next;
	};
public:
	
	void *allocate(size_t);
	
	void deallocate(void*, size_t);

private:
	obj *freeP = nullptr;
	const int chunk = 5;	// 自定义内存块大小
};




void *allocator::allocate(size_t size)
{
	obj *p = 0;	
	
	if (!freeP)
	{
		size_t chunkNum = chunk * size;
		freeP = p = (obj*)malloc(chunkNum);
		
		for (int i = 0; i < chunk - 1; ++i)
		{
			p->next = (obj *)((char *)p + size);
			p = p->next;
		}
		p->next = nullptr;
	}

	p = freeP;
	freeP = freeP->next;
	return p;
}



void allocator::deallocate(void* p, size_t i)
{
	((obj*)p)->next = freeP;
	freeP = (obj*)p;
}


class MyTest
{
public:
	long L;
	int s;
	static allocator myAlloc;	

public:
	MyTest(long l) :L(l) {};
	static void* operator new(size_t size){ return myAlloc.allocate(size); }
	static void operator delete(void *p, size_t size) { return myAlloc.deallocate(p, size); }
};

allocator MyTest::myAlloc;

void main()
{
	MyTest *p[100];
	std::cout << "size:" << sizeof(MyTest) << std::endl;

	for (int i = 0; i < 23; ++i)
	{
		p[i] = new MyTest(i);
		std::cout << p[i] << " " << p[i]->L  < 

由于语句的几乎一样,在其他类中使用也无差别,故可以将其封装起来减少代码的量,提高程序编写效率;


#include 
#define MY_ALLOC()	
	public:
		void* operator new(size_t size){ return myAlloc.allocate(size); }
		void operator delete(void *p, size_t size) { return myAlloc.deallocate(p, size); }
	protected:
		static allocator myAlloc;

#define IMPLEMENT_ALLOC(name) allocator name::myAlloc;


class allocator
{
private:
	struct obj{
		struct obj *next;
	};
public:
	
	void *allocate(size_t);
	
	void deallocate(void*, size_t);

private:
	obj *freeP = nullptr;
	const int chunk = 5;	// 自定义内存块大小
};


void *allocator::allocate(size_t size)
{
	obj *p = 0;
	
	if (!freeP)
	{
		size_t chunkNum = chunk * size;
		freeP = p = (obj*)malloc(chunkNum);
		
		for (int i = 0; i < chunk - 1; ++i)
		{
			p->next = (obj *)((char *)p + size);
			p = p->next;
		}
		p->next = nullptr;
	}

	p = freeP;
	freeP = freeP->next;
	return p;
}


void allocator::deallocate(void* p, size_t i)
{
	((obj*)p)->next = freeP;
	freeP = (obj*)p;
}



class MyTest
{
	MY_ALLOC();
public:
	long L;
	int s;

public:
	MyTest(long l) :L(l) {};
};

IMPLEMENT_ALLOC(MyTest)

常用方法

  • allocate(n):分配n个内存;
  • deallocate(p, n):释放内存;
  • construct(p, args):传入构造参数;
  • destroy(p):调用析构函数,销毁;
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/433361.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号