C++内存管理 C++模板
个人主页:小则又沐风
个人专栏:<数据结构>
<竞赛专栏>
<C语言>
<C++>
目录
一 C++的内存管理
1.C语言的内存管理
2.C++的内存管理
3.new和delete的实现原理
4. C++内存管理的特点
5. malloc/free和new/delete的区别
二 模板
1.初步认识
2.函数模板
(1)函数模板的概念
(2)函数模板的格式
(3)函数模板的原理
(4)函数模板的示例化
1.隐式实例化
2. 显式实例化:
(5)函数模板的匹配规则
1.函数模板是可以和非模板的函数同时存在的
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模 板
3.类模板
三 总结
座右铭
路虽远,行则将至;事虽难,做则必成
一 C++的内存管理
1.C语言的内存管理
在我们讲解C++的内存管理之前,我们先来回顾一下,C语言是如何进行内存管理的,我们来直接上代码
#include<stdio.h> #include<stdlib.h> int main() { int n = 10; int* p = (int*)malloc(sizeof(int) * n); if (p == NULL) { perror("malloc fail"); return 0; } free(p); p = NULL; return 0; }这是我们在C语言中经常使用的开辟空间的方法,但是这样使用起来难免会有点繁琐,我们来看一下我们在C++中是怎么进行空间的分配的.
2.C++的内存管理
在我们的C++中我们的关键词多了两个new和delete,下面我来讲解这两个关键字的使用的方式
new的作用就相当于我们在C语言中的malloc函数一样,但是这个new使用起来更加的顺手和方便
先来介绍一下我们new这个操作符
#include<iostream> using namespace std; int main() { int* p1 = new int; *p1 = 666; return 0;用new开辟出一个int的大小的空间,之后我们给这个空间赋值,
所以我们的new在分配空间的时候我们并不需要在new之前加上一个什么类型的指针了.
不仅仅如此我们还能这样写.
#include<iostream> using namespace std; int main() { int* p1 = new int; *p1 = 666; int* p2 = new int(666); cout << *p1 << endl; cout << *p2 << endl; return 0; }在这里我们我们不仅仅对p2开好了空间,我们还对他进行了初始化.
那么我们的new怎么开辟一个连续的空间呢?
#include<iostream> using namespace std; int main() { int* p1 = new int; *p1 = 666; int* p2 = new int(666); int* p3 = new int[10] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; return 0; }那么我们应该怎么释放空间呢?
在C语言中我们使用的是free,但是在C++中我们使用的是这个delete这个操作符
#include<iostream> using namespace std; int main() { int* p1 = new int; *p1 = 666; int* p2 = new int(666); int* p3 = new int[10] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; delete p1; delete p2; delete[]p3; return 0; }注意我们在释放连续的一段空间的时候我们使用了[]这个符号,这个不能省略.
下面是我们C++内存管理的示意图:
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用 new[]和delete[],注意:匹配起来使用
上面我们了解的是对于内置类型的内存管理,下面我们来看一下怎么对自定义类型进行空间管理;
我们先来创建一个类
#include<iostream> using namespace std; class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { A* a = new A; delete a; return 0; }我们来运行一下看看这个new是怎么为我们的自定义的类型开辟空间的.
这样看起来我们就明白了,在我们new的时候会调用我们的构造函数,在释放的时候会调用析构函数.
3.new和delete的实现原理
我们在使用new和delete这两个操作符的时候我们实际调用的是operator new和operator delete
不妨我们来看看这两个函数的实现
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void *p; while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // report no memory // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); }我们可以看到我们这个new实际是调用我们全局函数operator new而这个函数的底层也是通过malloc来实现的,但是这个如果开辟空间失败的会直接剖出异常.我们就不需要来判断了.
/* operator delete: 该函数最终是通过free来释放空间的 */ void operator delete(void *pUserData) { _CrtMemBlockHeader * pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData == NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead = pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); _free_dbg( pUserData, pHead->nBlockUse ); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; }delete的底层的逻辑也是这样,调用我们全局函数的operator delete 而这个函数实际上是依靠free实现的
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施 就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的
4. C++内存管理的特点
内置类型:
- 如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申 请空间失败时会抛异常,malloc会返回NULL
自定义类型:
new的原理
- 调用operator new函数申请空间
- .在申请的空间上执行构造函数,完成对象的构造
delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作
- . 调用operator delete函数释放对象的空间
new T[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对 象空间的申请
- 在申请的空间上执行N次构造函数
delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释 放空间
5. malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地 方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需 要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理释放
二 模板
1.初步认识
我们知道在我们C++中我们有一个库函数是swap,他的使用就不必我再介绍了吧,但是我们来模拟实现一下这个函数,我们可能会这样写
#include<iostream> using namespace std; namespace jzx { void swap(int& a, int& b) { int temp = a; a = b; b = temp; } } int main() { int a = 10; int b = 22; jzx::swap(a, b); cout << a << ' ' << b; return 0; }但是你有没有发现一个问题就是我们实现的函数只会准对我们的int类型进行交换,其他的类型就不支持了.
那我们很容易的想到我们可以使用函数的重载啊
namespace jzx { void swap(int& a, int& b) { int temp = a; a = b; b = temp; } void swap(double& a, double& b) { double temp = a; a = b; b = temp; } }但是这样我们需要什么类型就去重载一个这样的效率太低了,有什么方法能够一下子解决这个高度重复的工作,这时候我们就需要来了解一下模板
如果我们C++也能给我们一个模板,让我们的编译器来根据我们的需求造出不同的成品那就解决了我们上面的问题了.
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
2.函数模板
(1)函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生 函数的特定类型版本
也就是说我们先实现了全能的模板,后面的会根据我们传递的参数类型自动生成一个我们需要的函数
(2)函数模板的格式
template<class t1,class t2>
这上面是我们下面的模板将会用到的模板参数,它就相当于我们玩斗地主中的癞子牌,想当什么都行,但是一旦当作了一种类型就不能在改变了
返回值类型 函数名(参数列表){}
函数的主体还是没变的,只不过我们在这里改变了参数的类型,在这里我们会传我们的模板参数
就以我们的swap函数为例
#include<iostream> using namespace std; namespace jzx { /*void swap(int& a, int& b) { int temp = a; a = b; b = temp; } void swap(double& a, double& b) { double temp = a; a = b; b = temp; }*/ template<class T> void swap(T& a, T& b) { T temp = a; a = b; b = temp; } } int main() { int a = 10; int b = 22; jzx::swap(a, b); cout << a << ' ' << b << endl; double c = 2.3; double d = 3.5; jzx::swap(c, d); cout << c << ' ' << d << endl; return 0; }这样我们就是实现了swap函数的模板
注意:typename是用来定义模板参数关键字,也可以使用class或者使用typename
(3)函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。 所以其实模板就是将本来应该我们做的重复的事情交给了编译器
所以并不是我们解决了这个问题,只不过是我们把这个问题交给了编译器,他会根据我们传的参数实例化出函数.
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应 类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演, 将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
(4)函数模板的示例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化 和显式实例化
1.隐式实例化
隐式实例化就是让我们的编译器自己去推测他需要实例化的函数的类型
#include<iostream> using namespace std; namespace jzx { /*void swap(int& a, int& b) { int temp = a; a = b; b = temp; } void swap(double& a, double& b) { double temp = a; a = b; b = temp; }*/ template<class T> void swap(T& a, T& b) { T temp = a; a = b; b = temp; } } int main() { int a = 10; int b = 22; jzx::swap(a, b); cout << a << ' ' << b << endl; double c = 2.3; double d = 3.5; jzx::swap(c, d); cout << c << ' ' << d << endl; return 0; }就比如在这里我们的swap就是隐式实例化的表现,但是我们如果这么传呢?
int main() { int a = 10; int b = 22; jzx::swap(a, b); cout << a << ' ' << b << endl; double c = 2.3; double d = 3.5; jzx::swap(a, d); cout << c << ' ' << d << endl; return 0; }我们会发现我的编译器无法对我们的函数模板进行实例化,因为我们传了两个不同的类型上去,编译器不知道她到底应该实例化成哪一种的类型.这样的解决方式有两种
- 我们通过强制的类型转化,把我们传递的参数修改成一种的类型
int main() { int a = 10; int b = 22; jzx::swap(a, b); cout << a << ' ' << b << endl; double c = 2.3; double d = 3.5; jzx::swap(a,(int)d); cout << c << ' ' << d << endl; return 0; }我们将我们的d转换成了int类型,但是我们还会发现我们的代码是编译错误,这是因为我们进行强制的类型转化后我们会成一个临时变量,但是我们的临时变量具有常性,我们就必须使用const引用了.
但是我们使用了const的话我们怎么实现值的转换?
2. 显式实例化:
在函数名后的<>中指定模板参数的实际类型
- 另一种的方式就是显式实例化
namespace jzx { /*void swap(int& a, int& b) { int temp = a; a = b; b = temp; } void swap(double& a, double& b) { double temp = a; a = b; b = temp; }*/ template<class T> void swap(const T& a,const T& b) { /* T temp = a; a = b; b = temp;*/ } } int main() { int a = 10; int b = 22; jzx::swap(a, b); cout << a << ' ' << b << endl; double c = 2.3; double d = 3.5; jzx::swap<int>(a,d); cout << c << ' ' << d << endl; return 0; }因为我们显示示例化了就告诉我们的编译器你需要给我实例化出的函数,但是在这里也需要我们的类型转换,就会出现上述的问题,所以我们拿着这个swap函数来讲解是不合适的,大家就了解一下用法
(5)函数模板的匹配规则
1.函数模板是可以和非模板的函数同时存在的
#include<iostream> using namespace std; namespace jzx { template<class T1,class T2> T1 add(const T1& a, const T2& b) { return a + b; } template<class T> T add(const T& a, const T& b) { return a + b; } int add(int a, int b) { return a + b; } } int main() { int i1 = 2; int i2 = 3; jzx::add(i1, i2); return 0; }我们来看看这个add调用的是哪一个函数
在这里调用的是我们非模板函数,很容理解啊,因为有现成的啊
那么如果我们这么写呢?
jzx::add<int>(i1, i2);
这里就属于是强制工作了
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模 板
#include<iostream> using namespace std; namespace jzx { template<class T1,class T2> T1 add(const T1& a, const T2& b) { return a + b; } template<class T> T add(const T& a, const T& b) { return a + b; } int add(int a, int b) { return a + b; } } int main() { int i1 = 2; int i2 = 3; jzx::add<int>(i1, i2); double b1 = 1.1; double b2 = 2.2; jzx::add(i1, b2); return 0; }总而言之,就是有现成的就不会模板实例化,没有的才会实例化出一个适配的函数.
3.类模板
类模板和我们的函数模板十分的类似,我们可以想到在我们是使用不同的类型的栈的时候我们是这样使用的stack<类型>这样不就是模板吗???
templateclass T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
下面我来模拟实现一个简单栈的模板
#include<iostream> using namespace std; namespace jzx { template<class T> class stack { public: stack(size_t capacity=4) { _arr = new T[capacity]; _capacity = capacity; _size = 0; } void push_back(const T& x); ~stack() { delete[]_arr; _capacity = _size = 0; } private: T* _arr; size_t _size; size_t _capacity; }; template<class T> void jzx::stack<T>::push_back(const T& x) { //扩容 _arr[_size] = x; _size++; } } int main() { jzx::stack<int>t1; jzx::stack<double>t2; return 0; }三 总结
本部分系统梳理了 C++ 内存管理与模板两大核心知识。内存管理模块对比了 C 与 C++ 的内存管理方式,解析了 new/delete 的底层实现原理,明确了其与 malloc/free 的核心区别,总结了 C++ 内存管理的特点,为安全高效的内存操作奠定基础。模板模块从基础认知切入,详解了函数模板的概念、格式、原理与实例化方式(隐式 / 显式),明确了函数模板与非模板函数的匹配规则,同时涵盖类模板相关内容,体现了模板的泛型编程思想,实现代码复用与类型安全.
今天的内容结束了,今天讲解的仅仅是模板的基础的内容之后会讲解进阶的部分的.
谢谢大家的观看!!!!
