0


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

文章目录

📖前言

本章将介绍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]的原理:
  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表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

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 模板的引入:

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

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

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

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

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

  • 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;}
  • 缺省是从右往左的,可以全缺省,也可以半缺省
  • 因为传参是从左往右传的
  • 和函数一样去理解就行了
标签: c++

本文转载自: https://blog.csdn.net/m0_63059866/article/details/126605920
版权归原作者 yy_上上谦 所有, 如有侵权,请联系我们删除。

“【C++】内存管理 + 初识模板”的评论:

还没有评论