文章目录
📖前言
本章将介绍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.正常使用的代码如下:
structListNode{
ListNode* _next;int _val;ListNode(int val =0):_next(nullptr),_val(val){}};
ListNode*BuyListNode(int x){structListNode* node =(structListNode*)malloc(sizeof(structListNode));assert(node);
node->_next =NULL;
node->_val = x;return node;}intmain(){int* p1 =newint;//一个对象int* p2 =newint[10];//多个对象int* p3 =newint(3);//new一个int对象,初始化成10//int* p4 = new int[10](10); - 不能这样,是错的int* p4 =newint[10]{10};//new10个int对象,初始化成{}中的值//C++11支持的语法 - 初始化的是第一个delete p1;delete[] p2;//释放的时候要匹配//不匹配不一定会内存泄漏,但是有可能会崩溃//建议一定要匹配delete p3;delete[] p4;//BuyListNode是开空间加初始化structListNode* n1 =BuyListNode(1);//new是
ListNode* n2 =newListNode(2);//会去调用该类的构造函数return0;}
释放的时候要匹配,不然有可能出问题:
3.new 和 delete 的特点:
C++内存管理和C语言中内存管理的区别:不在于内置类型,而是在于自定义类型。
- malloc/free 和 new/delete 的区别在于
- malloc/free是函数,new/delete是关键字
- 在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
(1)基本使用1:
classStack{public:Stack(int capacity =10){
cout <<"Stack(int capacity = 10)"<< endl;
_a =newint[capacity];
_capacity = capacity;
_top =0;}~Stack(){
cout <<"~Stack()"<< endl;delete[]_a;
_capacity =0;
_top =0;}voidPush(int x){}private:int* _a;int* _top;int _capacity;};intmain(){//Stack st;
Stack* ps1 =(Stack*)malloc(sizeof(Stack));assert(ps1);
Stack* ps2 =new Stack;//开空间+调用构造函数初始化free(ps1);delete ps2;//调用析构函数清理资源+释放空间return0;}
- ps1和ps2是两个指针,指向一段动态开辟的空间
- new会开空间进行初始化,调用构造函数初始化
- ps1都不好初始化,因为类中的成员变量是私有的
(2)基本使用2:
用两个栈实现队列:
classStack{public:Stack(int capacity =10){
cout <<"Stack(int capacity = 10)"<< endl;
_a =newint[capacity];
_capacity = capacity;
_top =0;}~Stack(){
cout <<"~Stack()"<< endl;delete[]_a;
_capacity =0;
_top =0;}voidPush(int x){}private:int* _a;int* _top;int _capacity;};classMyQueue{private:
Stack _pushT;
Stack _popT;};intmain(){
MyQueue* obj =new MyQueue;delete obj;return0;}
- new创造一个MyQueue的对象,并且调用其构造函数,因为MyQueue这个类只有自定义类型,直接调用它的默认构造函数,默认构造
- delete一个MyQueue对象的时候,调用其析构函数,因为该没有显示写析构函数,所以只能调用默认的析构函数
总结:
- 之前C语言释放这种类型的时候要先将MyQueue对象中的两个栈先释放掉,再去释放队列这个对象否则会发生内存泄漏。
- 而C++直接delete就可以了,是因为它去调用了MyQueue对象的析构函数,析构函数自己去一层一层的释放了空间
1.3 C++开空间失败了的情况:
- 在我们之前学的C语言中,我们知道,当malloc开辟空间失败了之后,会返回一个空指针,所以用malloc之后,我们要对返回的指针进行判空。
- 但是C++中的new是不需要判空的,在其开辟失败的时候会抛异常。
在堆上开一个G,大概率会开失败。
voidfunc(){//malloc失败,返回空指针
Stack* ps1 =(Stack*)malloc(sizeof(Stack));assert(ps1);//new失败,抛异常
Stack* ps2 =newStack(4);// 开空间+调用构造函数初始化void* p0 =malloc(1024*1024*1024);
cout << p0 << endl;void* p1 =malloc(1024*1024*1024);
cout << p1 << endl;void* p2 =newchar[1024*1024*1024];
cout << p2 << endl;}intmain(){//捕获异常try{func();}catch(const exception & e){
cout << e.what()<< endl;}return0;}
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。
使用:
intmain(){//跟malloc功能一样,但是失败以后抛异常
Stack* ps2 =(Stack*)operatornew(sizeof(Stack));operatordelete(ps2);
Stack* ps1 =(Stack*)malloc(sizeof(Stack));assert(ps1);free(ps1);
Stack* ps3 =new Stack;//call operator new//call Stack构造函数//面向对象编程不再用返回值的方式来处理,它们更喜欢抛异常return0;}
跟malloc功能一样,但是失败以后抛异常,不用检查失败。
- operator new 和 operator delete没有直接价值的,它们是由间接价值的,是nwe的底层原理。
- new的底层原理是调用operator new 和构造函数。
我们来看一下汇编:
由汇编可见上述结论。
总结见下图:
使用delete的时候一定要要匹配去使用,不然有可能会崩溃
intmain(){int* p =newint[10];//对于内置类型不涉及调用构造函数//要调用operator malloc和malloc机制一样的delete p;//调用operator delete 也就是调用free
Stack* stArray =new Stack[10];//要调用十次构造函数和十次析构函数delete stArray;//要调用十次析构函数,这里只调用一次,崩了和底层实现的逻辑有关//不匹配可能没问题,可能会有问题//所以一定要匹配return0;}
我们来看一下汇编:
- new T[N]的原理:
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对 象空间的申请。
- 在申请的空间上执行N次构造函数。
- delete[]的原理:
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
- 调用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表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
intmain(){
Stack* obj =(Stack*)operatornew(sizeof(Stack));//针对一个空间,显示调用构造函数初始化//obj->Stack(); -- 构造函数又调不动,不让显示调用,是自动调用的//bbj->_top = 0; -- 私有成员不能访问//这是用到一个定位newnew(obj)Stack(4);//等价于Stack* obj = new Stack(4);//用new直接就调用堆了return0;}
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 模板的引入:
我们如何实现一个通用的交换函数?
- 经过我们之前的学习,我们知道可以使用函数重载:
使用函数重载虽然可以实现,但是有一下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错,那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
模板的处理是在编译阶段处理的。
- C++提出的编程思想叫泛型编程,不再是针对某种类型,能适应广泛类型,跟具体类型无关的代码。
- 而泛型编程所用的东西叫做 — 模板
- 模板分为:函数模板 和 类模板
2.2 函数模板的使用:
交换函数模板的实现:
//函数模板可以自动推导//template<class T>template<typenameT>voidSwap(T& left, T& right){
T tmp = left;
left = right;
right = tmp;}intmain(){int a =0, b =1;double c =2.2, d =3.3;//调用的不是同一个函数swap(a, b);swap(c, d);return0;}
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
看一下反汇编:
- 编译器是根据参数的类型通过模板推导出所需要的函数。
- 上图可见,两个函数所调用的函数地址不一样,所以不是调用同一个函数
- 模板函数,具体需要什么编译器实例化出来什么样子
- 函数模板是没有地址的
函数模板可以自动推导:
std C++ 标准库中就有交换函数的模板,以后用到交换直接用库里的模板即可。
2.3 类模板的使用:
在之前的数据结构中我们学习过:栈
用C语言实现栈:
//C语言实现栈typedefint STDatetype;classStack{public:Stack(STDatetype capacity =10){
_a =new STDatetype[capacity];
_capacity = capacity;
_top =0;}~Stack(){delete[]_a;
_capacity =0;
_top =0;}voidPush(int x){}private:
STDatetype* _a;
STDatetype* _top;int _capacity;};intmain(){
Stack st1;//一个栈存储int
Stack st2;//一个栈存储double//C语言需要手动修改typedef的内容return0;}
C语言的缺陷:
C语言的方式实现栈的话,要改变栈里面存储数据的类型,只能通过typedef来改变,但是只能是st1和st2两个栈存储的类型都是同一种类型的,若是一个栈存Int类型的数据,一个栈存double类型的数据是不可以的,这就是C语言的缺陷。
C++用类模板解决了这个问题:
//类模板 - 模板参数一般习惯用大写,不一定只能用Ttemplate<classT>classStack{public:Stack(int capacity =10){
_a =new T[capacity];
_capacity = capacity;
_top =0;}~Stack(){delete[]_a;
_capacity =0;
_top =0;}voidPush(T x){}private:
T* _a;int* _top;int _capacity;};intmain(){
Stack<int> st1;//一个栈存储int
Stack<double> st2;//一个栈存储double//C语言需要手动修改typedef的内容return0;}
2.4 模板和函数的联系和区别:
一定说函数模板是推演的,类模板就是一定是指定的吗?
答案是也不一定。
见如下代码:
//模板参数 -- 很多用法和函数参数是很像的//模板参数 -- 传递的是类型//函数参数 -- 传递的时对象值template<classT=char>
T*func(int a)//T的类型推不出来了{returnnew T[n];}intmain(){//函数模板的显式实例化int* p1 =func<int>(10);double* p2 =func<double>(10);return0;}
这个时候就要显式实例化了,因为这个T的类型是不能被推导出来的。
用模板替换的过程叫做实例化
1、函数模板的类型一般是编译器根据实参传递给形参,推演出来的,如果不能自动推演,那么我们就需要显示实例化,指定模板参数。
2、类模板的类型显示实例化,明确指定的
模板参数可以是typename也可以是class,切记不能是struct。
模板参数是可以有缺省参数,template< class T = char>
2.5 函数模板/类模板的声明和定义分离:
函数模板声明和定义的分离:
//声明的时候给模板参数 template<typenameT>voidSwap(T& left, T& right);//定义时候也给模板参数template<typenameT>voidSwap(T& left, T& right){
T tmp = left;
left = right;
right = tmp;}
如果每个函数模板的声明定义都分离,那么每个函数定义前都要加上声明模板参数。
类模板声明和定义的分离:
//声明的时候给模板参数 template<typenameT>classVector{public:Vector<T>(size_t capacity =10);private:
T* _pDate;
size_t _size;
size_t _capacity;};//定义的时候也给模板参数template<typenameT>
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<classT>
T Add(const T& left,const T& right){return left + right;}intmain(){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);return0;}
2.7 多个模板参数:
直接见代码:
代码1:
//多个模板参数template<classK,classV>voidFunc(const K& key,const V& value){
cout << key <<":"<< value << endl;}intmain(){Func(1,1);Func(1,1);Func<int,char>(1,'A');//只能连着指定,除非有却省模板参数return0;}
代码2:
template<classK=char,classV=char>voidFunc(){
cout <<sizeof(K)<< endl;
cout <<sizeof(V)<< endl;}intmain(){//类比函数的参数去学习,一个是类型,一个是变量/对象Func<int,int>();Func<int>();Func();return0;}
- 缺省是从右往左的,可以全缺省,也可以半缺省
- 因为传参是从左往右传的
- 和函数一样去理解就行了
版权归原作者 yy_上上谦 所有, 如有侵权,请联系我们删除。