C++内存管理以及模板的引入
个人主页:欢迎大家光临——>沙漠下的胡杨
各位大帅哥,大漂亮
如果觉得文章对自己有帮助
可以一键三连支持博主
你的每一分关心都是我坚持的动力
☄: 本期重点:内存管理和模板的引用
希望大家每天都心情愉悦的学习工作。
☄: 本期重点:内存管理和模板的引用
内存的分区
针对内置类型:
针对自定义类型:
开辟空间失败的处理:
new/delete底层实现
operator new/delete的重载
总结new/delete原理:
定位new的表达式:
模板的引入
函数模板:
模板参数的匹配原则:
类模板的使用:
类模板的使用:
我们下期string再见!
内存的分区
我们所谓的内存管理最重要的是就是堆区的数据管理,然后我们堆区是需要手动开辟,手动的释放。
针对内置类型:
我们C语言的内存管理如下:
一般是使用malloc,calloc,realloc和free进行开辟和释放。
我们malloc就是开辟指定空间。
calloc是开辟空间并初始化为0。
realloc如果是空指针,那么和malloc没区别。
如果是一个有效的堆区的地址,会进行扩容。扩容也分为两种情况:
1.如果我们是申请地址过大,那么它就会重新进行开辟一个空间,并把原空间的数据拷贝下,再释放源空间地址,返回新的申请空间的地址。
2.如果我们申请比较小,可以在后面扩容,那么就会在后面直接进行扩容,并返回源空间的地址。
free就是进行释放我们在堆区申请的空间地址。
我们C++的内存管理就是通过new和delete进行动态内存管理的。
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:匹配起来使用。
针对自定义类型:
其实我们内置类型C和C++没什么区别,但是我们自定义类型有点区别,我们malloc出的对象是没有初始化的,但是我们的new的对象,会调用类的默认构造来进行初始化。
class A { public: A(int a = 123) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { //malloc开辟的空间 A* a1 = (A*)malloc(sizeof(A)); //new开辟的空间 A* a2 = new A; free(a1); //delete销毁空间 delete a2; return 0; }
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
开辟空间失败的处理:
我们malloc失败会返回空指针,所以我们的malloc失败要判断返回值,然后我们在根据返回值进行相关的操作。
而我们new失败则会进行抛异常,所以我们的new是需要捕获异常。
我们简单了解下怎么捕获异常。
int main() { try { char* tmp = new char[1024u * 1024u * 1024u * 2 - 1]; } catch (const exception& e) { cout << e.what() << endl; } return 0; }
new/delete底层实现
我们的new和delete的底层实现其实就是malloc和free然后我们进行了封装,并且我们在其中添加了抛异常的机制,进而实现出来的。
我们也能看出我们使用new时,也就是调用了operator new这个全局函数,接着我们也会调用operator delete,这两个全局函数不是对new进行重载,而是一个全局函数,只不过名字起的和重载有点像,其实不是重载。
operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
我们甚至可以通过operator new的形式来进行开辟空间。使用方法和malloc一样。operator delete和free也一样。
operator new/delete的重载
一般情况下是不需要重载的,特殊情况下,我们可以对operator new/delete进行函数重载,来完成一些我们所特殊需要的要求。比如我们打印出一些信息来判断我们的是否已经进行释放空间了。
比如我们进行如下操作:
我们进行重载下operator new/delete,并使用宏来进行简化调用,然后我们的打印一些文件信息,来帮助我们判断是否内存释放了。
// 重载operator delete void* operator new(size_t size, const char* fileName, const char* funcName, size_t lineNo) { void* p = ::operator new(size); cout << "new:" << fileName << "||" << funcName << "||" << lineNo << "||" << p << "||" << size << endl; return p; } // 重载operator delete void operator delete(void* p, const char* fileName, const char* funcName, size_t lineNo) { cout << "delete:" << fileName << "||" << funcName << "||" << lineNo << "||" << p << endl; ::operator delete(p); } //用宏进行operator new/delete进行简化 #ifdef _DEBUG #define new new(__FILE__, __FUNCTION__, __LINE__) #define delete(p) operator delete(p, __FILE__, __FUNCTION__, __LINE__) #endif int main() { A* p1 = new A; delete (p1); A* p2 = new A[4]; A* p3 = new A; delete (p3); return 0; }
C/C++提供了三个宏__FUNCTION__,_FILE_和_LINE_定位程序运行时的错误。程序预编译时预编译器将用所在的函数名,文件名和行号替换。当运行时错误产生后这三个宏分别能返回错误所在的函数,所在的文件名和所在的行号。
我们可以看出p1和p3是释放空间的,但是p2没有释放。
我们还可以重载一个类专属的operator new/delete,我们后面在讲解。
总结new/delete原理:
内置类型:
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
自定义类型:
new的原理
1. 调用operator new函数申请空间
2. 在申请的空间上执行构造函数,完成对象的构造
delete的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用operator delete函数释放对象的空间
new T[N]的原理
1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对 象空间的申请
2. 在申请的空间上执行N次构造函数
delete[]的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
定位new的表达式:
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式: new (place_address) type或者new (place_address) type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景: 定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
我们演示一个用malloc开辟的空间,然后我们使用new来进行调用构造(不传参数),最后我们使用free,释放空间,然后我们再来开辟一个空间,我们使用new传参数来进行调用析构,最后调用operator delete,完成调用析构函数和释放空间。
class A { public: A(int a = 123) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { A* ptr = (A*)malloc(sizeof(A)); A* ptr1 = (A*)malloc(sizeof(A)); new(ptr)A(); new(ptr1)A(222); ptr->~A(); free(ptr); operator delete (ptr1); return 0; }
模板的引入
我们先看下面的一些函数:
int Swap(int x, int y) { int tmp = x; x = y; y = tmp; } double Swap(double x, double y) { double tmp = x; x = y; y = tmp; } char Swap(char x, char y) { char tmp = x; x = y; y = tmp; }
这些函数的作用是一样的,都是交换两个变量的地址,但是我们还是要写相对应的函数重载来进行支持不同的函数参数的传递,这样我们才能够完成函数的功能,那么我们可不可以让这些相同的功能的函数只写一份呢?
我们可以类比下我们的活字印刷术,古人就是把一个个的字做成模具,然后进行印刷,进而达到可以快速的完成抄写功能。如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件 (即生成具体类型的代码)。我们C++中也支持模板,进而也引入泛型编程的理念。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
函数模板:
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
我们函数模板的格式是template<typename T1, typename T2....>其中我们的typename可以使用class(但是不能使用struct来代替)其中T1,T2 ... 就是类型。这个名字可以随意。
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器,说了那么多我们来看看到底怎么使用吧。
我们来完成一个简单的交换函数:
template <typename T> void Swap(T& left, T& right) { T tmp = left; left = right; right = tmp; cout<<"交换好了"<<endl; } int main() { int a = 10; int b = 20; double c = 1.23; double d = 3.21; char e = 'A'; char f = 'B'; Swap(a, b); Swap(c, d); Swap(e, f); cout << a << endl << b << endl; cout << c << endl << d << endl; cout << e << endl << f << endl; return 0; }
我们可以看看三次调用的函数是绝对不一样的,我们来调试下看下:
这样我们看出,一定是先使用模板来进行拓印出三个不同的函数,然后我们在进行分别对应函数参数来进行调用。所以我们的函数的函数的模板完成的工作也不过是按照一样的逻辑,只不过是把类型来进行替换了一下而已。
那么我们如果函数的参数不同那么应该怎么解决呢?
我们看报错信息可以看出,其实就是因为模板参数不对应,这个也就是模板的实例化过程中推演出错(隐式实例化),其实就是我们不知道该如何进行拓印啦,那我们如何解决呢?
1.要么我们在模板上面,再给一个模板参数。
template <typename T,class T1> void Swap(T& left, T1& right) { T tmp = left; left = right; right = tmp; cout << "交换好了" << endl; } int main() { int a = 10; int b = 20; double c = 1.23; double d = 3.21; Swap(a, d); Swap(c, b); cout << a << endl << b << endl; cout << c << endl << d << endl; return 0; }
2.要么我们就直接指定一个模板,也就是把模板进行实例化啦。(显示实例化)
template <typename T> void Swap(T left, T right) { T tmp = left; left = right; right = tmp; cout << "交换好了" << endl; } int main() { int a = 10; int b = 20; double c = 1.23; double d = 3.21; Swap<int>(a,d); Swap<double>(b, c); cout << a << endl << b << endl; cout << c << endl << d << endl; return 0; }
模板参数的匹配原则:
我们一定也会遇见模板和专门的某个类型函数同时出现场景,如下所示:
template <class T> T Add(T left,T right) { return (left + right); } int Add(int left,int right) { return left + right; } int main() { int a = 10; int b = 20; cout << Add(a, b) << endl; return 0; }
我们会调用那个函数呢?是模板实例化出的还是原来就有的呢?
通过上面我们可以知道,如果我们有现成的可以调用的函数,那么就不会进行函数模板的实例化产生函数啦,直接调用现成的,如果没有现成的,那么我们会调用模板实例化的函数。
我们的模板和同名函数同时存在时:
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
3. 模板函数不允许自动类型转换(隐式类型的转换),但普通函数可以进行自动类型转换。
类模板的使用:
我们的类模板的定义格式:
template<class T1, class T2, ..., class Tn> class 类模板名 { // 类内成员定义 };
比如我们简单以一个Stack的模板类型来演示下:
#include <iostream> #include <assert.h> using namespace std; template<typename T>//类模板 class Stack { public: Stack(size_t capaticy = 4)//默认构造 :_a(nullptr) , _top(0) , _capaticy(0) { if (_capaticy > 0) { _a = new T[capaticy]; _capaticy = capaticy; _top = 0; } } ~Stack()//析构 { delete[] _a; _a = nullptr; _capaticy = _top = 0; } void push(const T& x);//插入的声明 void Pop()//出栈定元素 { assert(_top > 0); --_top; } bool Empty()//判空 { return _top == 0; } T& Top()//访问栈顶元素 { assert(_top > 0); return _a[_top - 1]; } private: T* _a; size_t _top; size_t _capaticy; }; // 注意:类模板中函数放在类外进行定义时,需要加模板参数列表 template <class T>//类模板的声明 void Stack<T>::push(const T& x)//防止成为内联函数 { if (_top == _capaticy) { size_t newCapaticy = _capaticy == 0 ? 4 : 2 * _capaticy; T* tmp = new T[newCapaticy]; if (_a) { memcpy(tmp, _a, sizeof(T)*_top); delete[] _a; } _a = tmp; _capaticy = newCapaticy; } _a[_top] = x; ++_top; }
类模板的实现一般是不能定义和实现在不同的文件中的,一般是在头文件中定义和实现在同一个头文件中,如果其中函数不想变成内联函数时,我们可以在类里面定义,并在该文件的类外面实现,可以防止成为内联函数。
类模板的使用:
我们简单实现下栈的插入和遍历过程。
int main() { try//对new进行异常判断 { Stack<int> s1;//必须要实例化模板 s1.push(1); s1.push(2); s1.push(3); s1.push(4); s1.push(5); Stack<char>s2; s2.push('A'); s2.push('B'); s2.push('C'); s2.push('D'); s2.push('E'); while (!s1.Empty())//栈判空 { cout << s1.Top() << " "; s1.Pop(); } cout << endl; while (!s2.Empty())//栈判空 { cout << s2.Top() << " "; s2.Pop(); } cout << endl; } catch (exception& e)//捕获异常 { cout << e.what() << endl; } return 0; }
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
我们下期string再见!