当前位置: 首页 > news >正文

【C++】内存管理 + 初识模板

文章目录

  • 📖前言
  • 1. C/C++内存管理
      • 1.1 C语言的内存管理回顾:
      • 1.2 C++的内存管理:
      • 1.3 C++开空间失败了的情况:
      • 1.4 operator new 和 operator delete:
      • 1.5 定位new:(了解)
  • 2. 模板
      • 2.1 模板的引入:
      • 2.2 函数模板的使用:
      • 2.3 类模板的使用:
      • 2.4 模板和函数的联系和区别:
      • 2.5 函数模板/类模板的声明和定义分离:
      • 2.6 模板的隐式类型转换:
      • 2.7 多个模板参数:

📖前言

本章将介绍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
  • realloc

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开空间 + 调用构造函数初始化

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

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;
}

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

3.new 和 delete 的特点:

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

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

(1)基本使用1:

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都不好初始化,因为类中的成员变量是私有的

(2)基本使用2:

用两个栈实现队列:
在这里插入图片描述

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;
};

class MyQueue
{
private:
	Stack _pushT;
	Stack _popT;  
};

int main()
{
	MyQueue* obj = new MyQueue;
	delete obj;

	return 0;
}

在这里插入图片描述

  • new创造一个MyQueue的对象,并且调用其构造函数,因为MyQueue这个类只有自定义类型,直接调用它的默认构造函数,默认构造
  • delete一个MyQueue对象的时候,调用其析构函数,因为该没有显示写析构函数,所以只能调用默认的析构函数

总结:

  • 之前C语言释放这种类型的时候要先将MyQueue对象中的两个栈先释放掉,再去释放队列这个对象否则会发生内存泄漏。
  • 而C++直接delete就可以了,是因为它去调用了MyQueue对象的析构函数,析构函数自己去一层一层的释放了空间

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

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

在堆上开一个G,大概率会开失败。

void func()
{
	//malloc失败,返回空指针
	Stack* ps1 = (Stack*)malloc(sizeof(Stack));
	assert(ps1);

	//new失败,抛异常
	Stack* ps2 = new Stack(4); // 开空间+调用构造函数初始化

	void* p0 = malloc(1024 * 1024 * 1024);
	cout << p0 << endl;

	void* p1 = malloc(1024 * 1024 * 1024);
	cout << p1 << endl;


	void* p2 = new char[1024 * 1024 * 1024];
	cout << p2 << endl;


}

int main()
{
    //捕获异常
	try
	{
		func();
	}
	catch (const exception & e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

在这里插入图片描述


1.4 operator new 和 operator delete:

operator new 和 operator delete,是C++标准库中的库函数,不是符号重载,C++中设计反常的地方。

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。

使用:

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;
}

跟malloc功能一样,但是失败以后抛异常,不用检查失败。

  • operator new 和 operator delete没有直接价值的,它们是由间接价值的,是nwe的底层原理。
  • new的底层原理是调用operator new 和构造函数。

我们来看一下汇编:
在这里插入图片描述

由汇编可见上述结论。


总结见下图:
在这里插入图片描述
使用delete的时候一定要要匹配去使用,不然有可能会崩溃

int main()
{
	int* p = new int[10];//对于内置类型不涉及调用构造函数
	//要调用operator malloc和malloc机制一样的

	delete p;//调用operator delete 也就是调用free

	Stack* stArray = new Stack[10];//要调用十次构造函数和十次析构函数
	delete stArray;//要调用十次析构函数,这里只调用一次,崩了和底层实现的逻辑有关

	//不匹配可能没问题,可能会有问题
	//所以一定要匹配

	return 0;
}

在这里插入图片描述
我们来看一下汇编:
在这里插入图片描述

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

既然这里是对应的那么不匹配会出现什么问题呢?

有些不匹配不会报错,而有些不匹配就要报错,所以建议是匹配的,如上代码,就是不匹配的情况,就会报错。
在这里插入图片描述

  • 对于内置类型:
  • int* p = new int[10];对于内置类型不涉及调用构造函数
  • 要调用operator malloc和malloc机制一样的
  • 对于自定义类型:
  • Stack* stArray = new Stack[10];要调用十次构造函数和十次析构函数
  • delete stArray;要调用十次析构函数,这里只调用一次,崩了和底层实现的逻辑有关,便宜指针不对就会报错,了解即可

不匹配可能没问题,可能会有问题,所以一定要匹配。


1.5 定位new:(了解)

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

int main()
{
	Stack* obj = (Stack*)operator new(sizeof(Stack));
	//针对一个空间,显示调用构造函数初始化
	
	//obj->Stack(); -- 构造函数又调不动,不让显示调用,是自动调用的
	//bbj->_top = 0; -- 私有成员不能访问

	//这是用到一个定位new
	new(obj)Stack(4);

	//等价于Stack* obj = new Stack(4);
	//用new直接就调用堆了

	return 0;
}
  • new(obj)Stack(4);
  • 等价于Stack* obj = new Stack(4);

使用格式:

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

使用场景:

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

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


2. 模板

2.1 模板的引入:

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

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

使用函数重载虽然可以实现,但是有一下几个不好的地方

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错,那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

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

  • C++提出的编程思想叫泛型编程,不再是针对某种类型,能适应广泛类型,跟具体类型无关的代码。
  • 而泛型编程所用的东西叫做 — 模板
  • 模板分为:函数模板 和 类模板

2.2 函数模板的使用:

交换函数模板的实现:

//函数模板可以自动推导
//template<class T>
template<typename T>
void Swap(T& left, T& right)
{
	T tmp = left;
	left = right;
	right = tmp;
}

int main()
{
	int a = 0, b = 1;
	double c = 2.2, d = 3.3;
	//调用的不是同一个函数
	swap(a, b);
	swap(c, d);

	return 0;
}

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

看一下反汇编:

在这里插入图片描述

  • 编译器是根据参数的类型通过模板推导出所需要的函数。
  • 上图可见,两个函数所调用的函数地址不一样,所以不是调用同一个函数
  • 模板函数,具体需要什么编译器实例化出来什么样子
  • 函数模板是没有地址的

函数模板可以自动推导:
在这里插入图片描述

std C++ 标准库中就有交换函数的模板,以后用到交换直接用库里的模板即可。

在这里插入图片描述

2.3 类模板的使用:

在之前的数据结构中我们学习过:栈

用C语言实现栈:

//C语言实现栈
typedef int STDatetype;
class Stack
{
public:
	Stack(STDatetype capacity = 10)
	{
		_a = new STDatetype[capacity];
		_capacity = capacity;
		_top = 0;
	}

	~Stack()
	{
		delete[]_a;
		_capacity = 0;
		_top = 0;
	}

	void Push(int x)
	{}
private:
	STDatetype* _a;
	STDatetype* _top;
	int _capacity;
};

int main()
{
	Stack st1;//一个栈存储int
	Stack st2;//一个栈存储double
	//C语言需要手动修改typedef的内容

	return 0;
} 

C语言的缺陷:

C语言的方式实现栈的话,要改变栈里面存储数据的类型,只能通过typedef来改变,但是只能是st1和st2两个栈存储的类型都是同一种类型的,若是一个栈存Int类型的数据,一个栈存double类型的数据是不可以的,这就是C语言的缺陷。

C++用类模板解决了这个问题:

//类模板 - 模板参数一般习惯用大写,不一定只能用T
template<class T>

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = new T[capacity];
		_capacity = capacity;
		_top = 0;
	}

	~Stack()
	{
		delete[]_a;
		_capacity = 0;
		_top = 0;
	}

	void Push(T x)
	{}
private:
	T* _a;
	int* _top;
	int _capacity;
};

int main()
{
	Stack<int> st1;//一个栈存储int
	Stack<double> st2;//一个栈存储double
	//C语言需要手动修改typedef的内容

	return 0;
}

2.4 模板和函数的联系和区别:

一定说函数模板是推演的,类模板就是一定是指定的吗?

答案是也不一定。

见如下代码:

//模板参数 -- 很多用法和函数参数是很像的
//模板参数 -- 传递的是类型
//函数参数 -- 传递的时对象值
template<class T = char>
T* func(int a)//T的类型推不出来了
{
	return new T[n];
}

int main()
{
	//函数模板的显式实例化
	int* p1 = func<int>(10);
	double* p2 = func<double>(10);

	return 0;
}

这个时候就要显式实例化了,因为这个T的类型是不能被推导出来的。

用模板替换的过程叫做实例化

1、函数模板的类型一般是编译器根据实参传递给形参,推演出来的,如果不能自动推演,那么我们就需要显示实例化,指定模板参数。

2、类模板的类型显示实例化,明确指定的

模板参数可以是typename也可以是class,切记不能是struct。

模板参数是可以有缺省参数,template< class T = char>


2.5 函数模板/类模板的声明和定义分离:

函数模板声明和定义的分离:

//声明的时候给模板参数    
template<typename T>
void Swap(T& left, T& right);

//定义时候也给模板参数
template<typename T>
void Swap(T& left, T& right)
{
	T tmp = left;
	left = right;
	right = tmp;
}

如果每个函数模板的声明定义都分离,那么每个函数定义前都要加上声明模板参数。

类模板声明和定义的分离:

//声明的时候给模板参数                                                                       
template<typename T>
class Vector
{
public:
	Vector<T>(size_t capacity = 10);
private:
	T* _pDate;
	size_t _size;
	size_t _capacity;
};

//定义的时候也给模板参数
template<typename T>
Vector<T>::Vector<T>(size_t capacity)
	:_pDate(new T[capacity])
	,_size(0)
	,_capacity(capacity)
{}

类外定义要指定类域。

注意:模板是不支持声明和定义放在两个文件当中的,会出现链接错误。

原因是:分离的话,模板实例化不出对应的函数,但是编译时可以通过的,因为声明中有模板的声明,最后符号表重定位的时候,找不到对应的函数模板调用的地址。

补充:

  • 模板不支持声明和定义分别放到xxx.h和xxx.cpp中
  • 一般是要放到一个文件中。有些地方就会命名成
  • xxx.hpp,寓意就是头文件和定义实现内容合并一起.
  • 但是并不是必须是.hpp, .h也是可以的
  • 解决方案1:在template.cpp中针对于要使用的模板类型显示实例化

在这里插入图片描述

  • 解决方案2:在不要分离到两个文件中。直接写在xxx.hpp或xxx.h中

这样就能将函数实例化出来,在编译的时候就能call这个函数的地址了,就不需要链接的时候去找了。


2.6 模板的隐式类型转换:

直接见代码:

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);

	//Add(a1, d2); - 错误的写法,编译器推不出来函数(自身是不能矛盾的)

	//两种解决方法:
	
	//隐式类型转换
	Add<int>(a1, d2);
	Add<double>(a1, d2);

	//强转
	Add(a1, (int)d2);
	Add((double)a1, d2);

	return 0;
}

2.7 多个模板参数:

直接见代码:

代码1:

//多个模板参数
template<class K, class V>
void Func(const K& key, const V& value)
{
	cout << key << ":" << value << endl;
}

int main()
{
	Func(1, 1);
	Func(1, 1);
	Func<int, char>(1, 'A');//只能连着指定,除非有却省模板参数

	return 0;
}

代码2:

template<class K = char, class V = char>
void Func()
{
	cout << sizeof(K) << endl;
	cout << sizeof(V) << endl;

}

int main()
{
	//类比函数的参数去学习,一个是类型,一个是变量/对象
	Func<int, int>();
	Func<int>();
	Func();

	return 0;
}
  • 缺省是从右往左的,可以全缺省,也可以半缺省
  • 因为传参是从左往右传的
  • 和函数一样去理解就行了

相关文章:

  • 猿创征文|我的技术成长之路,一名Python学者在CSDN的蜕变
  • java基于ssm的高校人事员工工资管理系统
  • QML初学者教程
  • 速卖通详情接口接口调用示例
  • 记录Kettle连不上mysql8
  • 远程Debug远端服务器JVM配置
  • Java中的内部类,你真的理解吗
  • Home Depot 使用 SUSE Rancher 和 K3s 升级 2300 个零售边缘位置
  • 处方识别 易语言代码
  • 跟李沐学AI之多层感知机+深度学习计算
  • 基于MVC三层架构的图书管理系统(JavaWeb+Maven项目)
  • redis客户端错误定位
  • 使用 http-proxy 对网络请求进行代理
  • Dubbo-admin+Zookeeper 的环境搭建与 Could-not-extract-archive
  • JS——JS高阶部分相关知识点汇总
  • Apache的基本使用
  • gitlab-ci配置详解(一)
  • js中的正则表达式入门
  • Netty 框架总结「ChannelHandler 及 EventLoop」
  • Phpstorm怎样批量删除空行?
  • react-native 安卓真机环境搭建
  • Redux系列x:源码分析
  • Work@Alibaba 阿里巴巴的企业应用构建之路
  • 包装类对象
  • 测试如何在敏捷团队中工作?
  • 纯 javascript 半自动式下滑一定高度,导航栏固定
  • 高度不固定时垂直居中
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 如何设计一个微型分布式架构?
  • 使用 QuickBI 搭建酷炫可视化分析
  • 问题之ssh中Host key verification failed的解决
  • 一个SAP顾问在美国的这些年
  • 移动端 h5开发相关内容总结(三)
  • 再次简单明了总结flex布局,一看就懂...
  • 仓管云——企业云erp功能有哪些?
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • #HarmonyOS:软件安装window和mac预览Hello World
  • #include到底该写在哪
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • (6)添加vue-cookie
  • (Arcgis)Python编程批量将HDF5文件转换为TIFF格式并应用地理转换和投影信息
  • (C)一些题4
  • (搬运以学习)flask 上下文的实现
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (博弈 sg入门)kiki's game -- hdu -- 2147
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (附源码)ssm高校社团管理系统 毕业设计 234162
  • (附源码)计算机毕业设计大学生兼职系统
  • (更新)A股上市公司华证ESG评级得分稳健性校验ESG得分年均值中位数(2009-2023年.12)
  • (删)Java线程同步实现一:synchronzied和wait()/notify()
  • (原)记一次CentOS7 磁盘空间大小异常的解决过程
  • (原創) 物件導向與老子思想 (OO)
  • (转)从零实现3D图像引擎:(8)参数化直线与3D平面函数库
  • (轉貼)《OOD启思录》:61条面向对象设计的经验原则 (OO)