C++内存管理与模板
文章目录
- C++内存管理与模板
- 前言:
- 一.new和delete基本用法
- 二.底层实现
- 三.定位new
- 四.模板
- 4.1函数模板
- 4.2调用选择
- 4.3类模板
- 4.4声明定义分离
前言:
C++的内存管理和C语言中动态内存分配是相似的,在这一篇我们会学到更符合面向对象的new和delete;
学习模板知识,是我们迈入STL的最后一步,届时可以飞速转为C++选手;
一.new和delete基本用法
new后面加类型会开辟类型大小的空间,p1维护一个整型的空间,使用圆括号进行初始化;单纯用new在堆上开辟的空间使用delete释放。
parr维护一个5个double元素的数组,使用{}为数组按顺序初始化,未完全初始化的默认给0;使用new[]开辟空间的使用delete[]释放,具体原因后面讲。
以上对内置类型进行堆空间分配和释放和C语言使用malloc和free本质上没有区别,只是用法上不太一样,比如不需要计算类型大小、无需强转。
new和delete解决的问题不在于内置类型,而是自定义类型,请看下面代码:
new在为类对象开辟空间的时候,会调用类对象的构造函数初始化对象,这是为了符合“刚创建的对象最好一开始进行初始化”,因此new有两个功能:1.开辟空间;2.调用构造函数;
对于自定义类型会去调用构造函数,对于内置类型不会,因为没有构造函数可以掉,在C++中很多都是针对自定义类型的,这是面向对象的特征。
delete在释放new为类对象开辟的空间之前会先调用析构函数进行资源清理,然后再释放对象,也就是说delete的功能是:1.清理资源;2.释放空间;
接下来我们讲一个复杂一点的:创建栈对象
class Stack
{
public:Stack(int capacity = 4){cout << "Stack(int capacity = 4)" << endl;_a = new int[capacity];//new开辟空间失败不返回空指针_top = 0;_capacity = capacity;}~Stack(){cout << "~Stack()" << endl;delete[] _a;_a = nullptr;_top = _capacity = 0;}private:int* _a;int _top;int _capacity;
};int main()
{Stack* ptr = new Stack;delete ptr;return 0;
}
到这里,new和delete使用我们讲清楚了,那么new和delete到底是什么呢?它是如何完成这些工作的,让我们来看看new和delete的底层。
二.底层实现
new和delete是C++里的两个操作符,基本用法就是上面讲的。在C语言中malloc是用来开辟空间的,但是如果失败返回空指针不符合C++面向对象的惯常做法,此时祖师爷封装了这么一对全局函数:
operator new的主逻辑是调用C库中的malloc函数,然后对malloc开辟空间失败的情况进行封装,从返回空指针变成更符合面向对象的抛异常。
因此读者可以这么理解:operator new本质就是一个封装版的malloc,对malloc开辟空间失败的情况做了处理。
讲到这里,相信大家已经猜到operator delete的本质是free了。没错,operator delete单纯是用来配对operator new,和free本质上没有区别。
读者:好,博主,我知道了!这是两个全局函数,那它和new、delete这两个操作符有什么关系呢?
当我们使用new这个操作符的时候,翻译成汇编看到:编译器会自动去调用operator new这个函数,还有调用构造函数的。delete也是,会先调用析构函数,再调用operator delete。
到这里,new和delete再也不是很神秘的东西了,底层的operator new 和 operator delete就是C语言中的malloc和free。
我们看到如果在使用new[]的时候,调用operator new[]这个函数,但是进到这个函数内部,立马调用operator new,因此这个带[]的new函数仅是为了对称。
开辟5个栈对象,总大小应该是60才对,为什么是64呢?
我们看ptr的指针是指向E67C的,E678是5,我们连蒙带猜的把这个整型空间算在ptr里的话就算是指向一块64字节大小的空间。
没错,实际上对于new[]的时候,会多开辟一个整型空间用来存储对象个数。用途是:用来给delete[]使用,这也是delete[]的[]里不需要写析构对象个数的原因。
那么到这里我们就能明白为什么new/delete、new[]/delete[]要搭配使用了,因为如果使用new[]和delete,此时delete并不像deletep[]一样会释放指针-4的位置,那么就会导致开辟的空间部分释放,运行报错!
同理,malloc/free与new/delete最好也不要混着使用,老老实实配套使用就不会坑自己。
三.定位new
int main()
{//以下这两句组合相当于: Stack* p1 = new Stack(10);//1.开辟空间Stack* p1 = (Stack*)operator new(sizeof(Stack));//2.调用构造 定位new:显示调用构造函数的方法new(p1)Stack(10);//以下两句组合等同于:delete p1;//3.清理资源 C++中可以显示调用析构函数p1->~Stack();//4.释放空间operator delete(p1);return 0;
}
定位new的使用是:new(指针)类型(初始化列表)这样,就可以显示调用构造函数了,在内存池技术中发挥用武之地。
以上就是C++中的内存管理学习,我们接着往下进入模板的学习!
四.模板
4.1函数模板
在生活中,处处有模板的身影,比如语文答题模板、活字印刷技术等等很多。模板的出现,让相似的问题得以快速的解决,在计算机中,也有模板的身影:
//这两个交换函数除了类型以外,没有任何不同
void Swap(int& left, int& right)
{int tmp = left;left = right;right = tmp;
}void Swap(double& left, double& right)
{double tmp = left;left = right;right = tmp;
}
对于这种只有类型不同,逻辑一样的函数需要写两个函数,祖师爷早就看不惯了:
//函数模板
template<typename T>
void Swap(T& left, T& right)
{//模板参数TT tmp = left;left = right;right = tmp;
}int main()
{int a = 10, b = 20;double c = 1.1, d = 2.2;Swap(a, b);Swap(c, d);return 0;
}
模板参数T指的是类型,使用tmplate<typename 类型名>类型名可以任意起,因为是定义类型的,博主的习惯是取为T(type)。那么这个模板有什么用?请看下面代码:
从上图看出不管是int还是double调用,调试的时候都是进入函数模板,表面上看起来像走了模板函数一样。
但实际上Swap(a,b)的时候,函数模板通过实参的类型,编译器推断模板参数T是int,然后编译器生成了一份整型交换的Swap函数并调用,这就是函数模板的参数推演。
所以Swap(a,b)和Swap(c,d)调用的不是同一个函数(它们地址不同),是调用编译器确定的函数参数T照函数模板生成的那份函数代码。
可以使用多个模板参数,多个模板参数的定义和多个函数参数的的定义是一样的,typename和class可以互换,博主习惯使用class。举个例子就是下面的代码:
template<class T1, class T2>
4.2调用选择
当模板和现有的函数存在时,如果现有的函数是合适的,那么就不会调用编译器通过模板生成的,而是调用现成的。
没有现成的函数就让编译器通过模板生成一个,调用生成的函数。此外,如果没有函数模板,能通过隐式类型转换符合参数类型的现成函数可以凑合被调用。
以上就是编译器根据函数模板生成的与现成函数的调取情况,可能不同编译器的具体情况不太一样,博主使用的是VS2019。
前面编译器根据实参推演生成的函数都是被动的,我们可以主动给模板参数类型,让编译器生成对应的函数,下面请看显示实例化:
这种函数的参数与模板参数没有关系或者函数没有参数的,就不能通过传递实参让编译器根据实参的类型,推演生成对应类型的函数代码,而要程序员自己实例化。
以上是实例化的方式,也就是语法规则,记住就好了。
4.3类模板
类模板是用来对只有类型不同的代码简化方法,请看下面代码:
template<class T>
class Stack
{
public:Stack(int capacity = 4){_a = new T[capacity];_top = 0;_capacity = capacity;}~Stack(){delete[] _a;_a = nullptr;_top = 0;_capacity = 0;}
private:T* _a;int _top;int _capacity;
};int main()
{Stack<int> st1;//整型栈Stack<double> st2;//double类型栈return 0;
}
当我们定义了一个栈的类模板后,对于存储不同数据类型的栈不再需要写两份(只有数据类型不同)代码了,通过显示类模板实例化,让编译器为我们生成即可!
4.4声明定义分离
template<class T>
class Stack
{
public://Stack是类名,构造函数是和类名相同的Stack(int capacity = 4);~Stack(){delete[] _a;_a = nullptr;_top = 0;_capacity = 0;}
private:T* _a;int _top;int _capacity;
};//声明定义分离的时候,模板参数要跟上
template<class T>
Stack<T>::Stack(int capacity)
{_a = new T[capacity];_top = 0;_capacity = capacity;
}int main()
{//它们是同一个类模板实例化出来的栈,但不是同一个类型Stack<int> st1;//Stack<int>是一个类型 存放整型的栈Stack<double> st2;//Stack<double>是另一类型 存放double的栈return 0;
}
类模板中,声明和定义分离,定义指明类型(Stack<T>)这是因为模板参数T在成员函数中是有作用的,即使在某些成员函数中没有使用到T,也需要加上,不能单靠Stack::。
ok,以上就是C++中内存管理和模板的知识学习,希望读者读完有所收获。