【C++】内存管理 + 初识模板详细讲解

前言:
本章将介绍C

1. C/C++内存管理

1. 1 C语言的内存管理回顾:

在我们之前学C语言的过程中,已经接触过了动态内存管理,我们当时用的是使用C语言的方式。

  • malloc
    void * malloc (size_t size)
  • 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针,空间是未经初始化的。
  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
    同时配合着free函数一起使用,申请 — 释放空间

void free (void * ptr);

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

  • calloc
    void * calloc (size_t num, size_t size);

  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

  • ralloc
    void * realloc (void ptr, size_t size);

  • ptr 是要调整的内存地址

  • size 调整之后新大小

  • 返回值为调整之后的内存起始位置。

  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间.

情况1:原有空间之后有足够大的空间,就地扩容。
情况2:原有空间之后没有足够大的空间,异地扩容,需要改变ptr指针

1. 2 C++的内存管理:

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

1.new 和 malloc的区别:

  • 对于内置类型而言,用malloc和new,除了用法不同,没有本质区别
  • 它们区别在于自定义类型
  • malloc只开空间,new开空间 + 调用构造函数初始化
  • delete 调用析构函数清理资源+释放空间

图解:
在这里插入图片描述
正确使用的代码如下:

struct ListNode
{
	ListNode* _next;
	int _val;

	ListNode(int val = 0)
		:_next(nullptr)
		,_val(val)
	{}
};
ListNode* BuyListNode(int x)
{
	struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
	assert(node);
	node->_next = NULL;
	node->_val = x;
	
	return node;
}
int main()
{
	int* p1 = new int;//一个对象
	int* p2 = new int[10];//多个对象

	int* p3 = new int(3);//new一个int对象,初始化成10
	//int* p4 = new int[10](10); - 不能这样,是错的
	
	int* p4 = new int[10]{ 10 };//new10个int对象,初始化成{}中的值
	//C++11支持的语法 - 初始化的是第一个
	delete p1;
	delete[] p2;
	//释放的时候要匹配
	//不匹配不一定会内存泄漏,但是有可能会崩溃
	//建议一定要匹配
	delete p3;
	delete[] p4;
	//BuyListNode是开空间加初始化
	struct ListNode* n1 = BuyListNode(1);
	//new是
	ListNode* n2 = new ListNode(2);//会去调用该类的构造函数
	return 0;
}

释放的时候要匹配,不然有可能出问题

2.new 和 delete 的特点:

C++内存管理和C语言中内存管理的区别:不在于内置类型,而是 在于自定义类型。

  • malloc/free 和 new/delete 的区别在于:
    答:malloc/free是函数,new/delete是关键字
    在申请自定义类型的空间时,new会调用构造函数delete会调用析构函数,而malloc与free不会。

  • new 函数没有原地扩容的功能,如果要扩容,只能重新New来申请。

代码实现:

class Stack
{
public:
	Stack(int capacity = 10)
	{
		cout << "Stack(int capacity = 10)" << endl;
		_a = new int[capacity];
		_capacity = capacity;
		_top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[]_a;
		_capacity = 0;
		_top = 0;
	}
	void Push(int x)
	{}
private:
	int* _a;
	int* _top;
	int _capacity;
};

int main()
{
	//Stack st;
	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
	%%没办法进行初始化,因为成员变量是私有化的。
	assert(ps1);
	Stack* ps2 = new Stack;//开空间+调用构造函数初始化
	free(ps1);
	delete ps2;//调用析构函数清理资源+释放空间
	return 0;
}

注意:

  • ps1和ps2是两个指针,指向一段动态开辟的空间
  • new会开空间进行初始化,调用构造函数初始化
  • ps1都不好初始化,因为类中的成员变量是私有的

1.3 C++开空间失败了的情况:

  • 在我们之前学的C语言中,我们知道,当malloc开辟空间失败了之后,会返回一个空指针,所以用malloc之后,我们要对返回的指针进行判空。
  • 但是C++中的new是不需要判空的,在其开辟失败的时候会抛异常

1.4 operator new 和 operator delete:

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。
new在底层调用operator new全局函数来申请空间,delete在底层通过,operator delete全局函数来释放空间。

  • operator new 是封装了malloc,malloc失败了就抛异常。
  • operator delete 也进行了封装抛异常检查等,最终调用了_free_dbg,而C语言的free其实一个宏函数,它也是调用了_free_dbg,所以也能理解成operator delete 封装了free。

注意:可以重载全局的**operator new (operator delete)**的行为来控制new 和delete的行为

int main()
{
	//跟malloc功能一样,但是失败以后抛异常
	Stack* ps2 = (Stack*)operator new(sizeof(Stack));
	operator delete(ps2);

	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
	assert(ps1);
	free(ps1);

	Stack* ps3 = new Stack;
	//call operator new
	//call Stack构造函数
	//面向对象编程不再用返回值的方式来处理,它们更喜欢抛异常
	return 0;
}

1.5 重载operator new与operator delete

一般情况下不需要对 operator new 和 operator delete进行重载,除非在申请和释放空间时候有某些特殊的需求。
比如:在使用new和delete申请和释放空间时,打印一些日志信息,可以简单帮助用户来检测是否存在内存泄漏。
在这里插入图片描述
也可以重载一个类专属的operator new (必须是Static),当使用类new时,会有限匹配类自己专属的Operator new.

同时也需要注意,这里的operator new和 operator delete
函数是静态的,要加上static。因为在new一个对象的时候我们手上是没有对象的,也就是说无法通过对象调用一般的函数。加上static就可以不通过对象而直接调用这两个函数了。这是一个细节

  • new的底层原理是调用operator new 和构造函数。

  • new T[N]的原理:

1.调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。
2.在申请的空间上执行N次构造函数。

  • delete[]的原理:

1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
2.调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。

1.6 定位new:(了解)(常用于给内存池申请出来的空间初始化)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:

  • new (place_address) type或者new (place_address) type(initializer-list)
  • place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

代码示例:

int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
free(p1);
}

内存泄漏指的是:指针丢了,不是内存丢了。
普通的内存泄露不怕,进程只要正常结束的,申请的内存会还给操作系统。

6 内存空间分布
在这里插入图片描述

2. 模板

2.1 模板的引入

我们如何实现一个通用的交换函数?

  • 经过我们之前的学习,我们知道可以使用函数重载。

使用函数重载虽然可以实现,但是有一下几个不好的地方:
1.重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
2.代码的可维护性比较低,一个出错可能所有的重载均出错

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

模板的处理是在编译阶段处理的。

  • C++提出的编程思想叫泛型编程,不再是针对某种类型,能适应广泛类型,跟具体类型无关的代码
  • 而泛型编程所用的东西叫做 — 模板
    在这里插入图片描述

2.2 函数模板

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本

1. 函数模板格式

template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){ }

注意:typename是用来定义模板参数关键字,也可以使用class

代码示例:

template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);

**底层原理:**它们调用的肯定不是同一个函数。虽然它们看起是来调用了同一个函数(这是编译器优化的结果),但是它们调用的是模板自动生成的相应函数。
在这里插入图片描述

2. 函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化
1.隐式实例化:让编译器根据实参推演模板参数的实际类型

2.显式实例化:在函数名后的<>中指定模板参数的实际类型,如:// 显式实例化
Add< int >(a, b);

3. 模板参数的匹配原则

1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

2.对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数

2.3 类模板

1.类模板的定义格式

template<class T1, class T2, …, class Tn>
class 类模板名 { // 类内成员定义 };

代码示例:

// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{
public :
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
// 使用析构函数演示:在类中声明,在类外定义。
~Vector();
void PushBack(const T& data)void PopBack()// ...
size_t Size() {return _size;}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};

%%  注意:类模板中函数放在类外进行定义时,需要加模板参数列表
**template <class T>**
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}

在这里插入图片描述

2 类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类
类模板实例化,只能显示实例化。

// Vector是类模板名称,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

注意事项:

模板不支持分离编译,声明和定义需要放在同一个文件中,.hpp中

尾声
看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值