C++入门第五篇--内存管理

简介: C++入门第五篇--内存管理

前言:

在这篇文章之前,想先说说我目前的状态,目前大一开学已经快2个月了,对于计算机的热爱让我在这两个月里在计算机方面迅速学习了大量的知识点和系统方面的知识,也让我从高考的阴霾中稍微走出来了一点,但上周参加学校的ACM选拔赛深深的打击了我,15道题我仅仅做出来3道,而且还是那种最简单的那种,看着那些来自南方大省和来自浙江,江西,河南的同学迅速得到了答案和解题方法,作为一个来自东北小城的我来说备受打击,有时候在想自己高考后的暑假直到现在的一系列铺垫是否值得?或许自己只是把自己想得太好了,或许自己不过是一个在普通不过的人了,做游戏的梦想,做出一款走向世界的竞技类游戏的梦想,在现实的打击中一点一点的被消磨,我突然之间找不到我选择计算机的理由,或者说,当初或许听家里人的话,选择一所地域和排名更好的学校不是更好,为什么为了计算机而愿意舍弃很多东西呢?有时候羡慕很多人,羡慕他们的人生,羡慕他们不用坐在电脑前对着代码反复的调试和面察,未来到底是怎样的呢?我没法从眼前的生活中看到一点迹象,或许,梦想就是用来消磨的,或许明明都知道结果,但却依旧掩耳盗铃,为了一片虚无而度过之后的日子。有些人可能只需要走一步就会找到答案,有些人穷尽了一生,直到临死之前都不知道身在何方,但,他们的共同点在于,一直向前走而不停下脚步,我们常说人生是一场羁旅,逐渐褪去的是青涩,是稚嫩,是激情,友情,爱情,亲情,直到最后,当你永远离开这个世界的瞬间,你已经抛下了一切执念和遗憾,安然等待生命的消逝。所以,让我们继续吧,我已经为自己选择了一条道路,就已经没法回头。

接下来,为大家带来C++的第五篇–内存管理

1.C/C++的动态内存过渡:

在C语言中,我们动态开辟使用的几个常见函数为malloc,calloc,realloc,free。我们首先先看一个常见的C语言动态开辟的错误:

int*arr1=(int*)malloc(sizeof(int)*2);
if(arr1==NULL)
{
perror("malloc failed");
eixt(-1);
}
int*arr2=(int*)realloc(arr1,sizeof(int),10);
if(arr2==NULL)
{
perror("realloc failed");
eixt(-1);
}
free(arr2);
free(arr1);

我们上述的代码对么?

显而易见,我既然在这里问你,它肯定是不对的,但我们要知道它哪里不对,这就涉及到我们的动态开辟内存函数的理解,**我们的realloc函数的开辟特点是,倘若空间足够,则realloc在arr1的基础上扩容,并且回收arr1以arr2作为新的指针指向,倘如空间不足,我们的arr2就会在新的位置开辟一块地址并且系统会回收之前开辟的空间,只留下新开辟扩容后的空间,故我们首先释放arr2,这是没问题的,因为是否回收与这块空间是没关系的,但接下来我们又释放arr1,arr1的空间要么在释放arr2的时候已经被释放掉了,要不然就是这块空间已经被回收,这个时候就会造成多次释放,从而报错。**所以,在这里告诉我们,我们需要对动态内存的理解更加准确不能模棱两可。

抛开我们的第一个错误不谈,下面让我们说说C语言动态开辟的一些笨拙的地方,

回想一下我们在构建一个顺序表的时候,我们动态开辟一段空间后,动态开辟的函数本身是不能对其初始化的,需要我们手动写Init函数来为其初始化,这就很麻烦,其次,由之前我们已经学到的类和对象的内容,对于复杂的对象,我们仅仅使用malloc等C语言函数是没法对其操作的,尤其是,malloc只能做到开辟,但没法自动对其初始化,而C++的动态开辟就完全可以解决这个问题,即new和delete关键字

2.new delete关键字:

!!!首先必须强调的一点:和malloc,realloc calloc不同,他们都是函数,但new delete是操作符而不是函数,这一点就有点类似sizeof()>!!!

常用的用法为:

1.new用法:

创建单个变量:类型arr1=new 类型;
创建多个变量:类型
arr1=new 类型[ ]

2.delete用法:

释放单个变量:delete 名字;
释放多个变量:delete[] 名字;

new和delete是匹配的,malloc/realloc/calloc与free是匹配的,在我们使用的时候,我们应当严格或者最好按照对应的匹配去使用,而不是随意的释放随意的开辟,在后面我们会强调为什么,在这里我们先记下来作为一个潜在的规则适用。

3.对new和delete的本质理解和底层剖析:

new和delete倘若只是用来开辟内置类型,那样直接用malloc free不是更好么?

new和delete是可以用来为自定义类型的对象来动态开辟的,而这也是它最重要以及区分开malloc free的本质区别。

new在对自定义类型动态开辟空间的时候,它首先会为对象的类成员开辟一块空间,包括配置的类的默认成员函数,然后,它会调用这个类的默认构造函数,对其成员进行初始化,这样,new就完成创建了一个已经完成了初始化的类的对象的动态开辟。
而delete在对自定义类型进行释放的时候,也是先调用此对象的析构函数,然后再释放掉这个类,而不是仅仅直接释放,这样,对于自定义类型,我们就不用担心最后的释放处理不干净的原因。

在C++中对这两个步骤分别设置的对应的函数:

如下:

4.内部解析:动态开辟operator new与释放operator delete,以及显示调用的初始化–定位new表达式:

1.operator new和operator delete

**在C++中,有两个函数operator new和operator delete用来专门创建和释放动态空间,不过注意:operator new和operator delete与new 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)
     {
         static const std::bad_alloc nomem;
         _RAISE(nomem);
     }
return (p);
}
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; 
     }

我们在实现过程中发现,operator new和operator delete是通过malloc和free的来实现的,而operator new和operator free与malloc 与free的用法基本相同,其写法也相同,如下:

类型 名字=(类型)operator new(sizeof(类型));
operator delete(名字);
*

2.定位new表达式:

在C++中,析构函数可以显式调用,但构造函数时不能显式调用的,不过,通过定位new表达式,我们就可以显式调用构造函数,将我们的类显式初始化:

基本的用法如下:

new(前面创建的空间的指针)+创建对象的方式

如下:

new(a)A 这样,这里后面基本使用的是匿名构造,因为我已经拿指针接收了,说白了已经让匿名对象的生命周期延长了

如下:

#include<iostream>
using namespace std;
class A
{
private:
  int _a;
  char _c;
  double _b;
  static int s;
  class B
  {
  private:
    int s = 0;
  public:
    void print()
    {
      cout << 1 << endl;
    }
  };
public:
   A(int a=10,int c=20,int b=30)
    : _a(a),
      _b(b),
      _c(c)
  {
    cout << 1 << endl;
    s++;
  }
   ~A()
   {
     cout << 1 << endl;
   }
   void static Func()
   {
     cout << "hello world" << endl;
   }
};
  int A::s = 100;//这表示,这个全局变量是由全部这个类所共享的,故要加访问限定符
int s = 0;
int main()
{
  A* a = (A*)operator new(sizeof(A));
  new(a)A(20, 25, 21);
  return 0;
}

讲到这里,我们把operator函数和定位new结合,我们就是实现了一个这样的功能:首先为一块对象创建一个空间,然后对这个对象显式调用它的构造函数,听到这里,是不是很熟悉?这不就是我们new的作用么?同理,让我们迁移到delete,如下:

A* a = (A*)operator new(sizeof(A));
  new(a)A(20, 25, 21);
  a->~A();
operator delete(a);

我们首先调用析构函数清理对象a的资源,然后调用operator delete释放掉a的这块资源,这不就是delete的功能么?

故我们在这里总结出一个很重要的结论:!!!new和delete本质上就是把operator和构造析构结合起来使用,它封装了这两个函数,从而构成了一个既能动态开辟又能初始化的很便捷的操作符!!。

但是,我们已经需要学会这两种方式组合用来开辟,因为有的时候我们申请的空间不一定来自堆区,也有可能是内存池,那个时候我们使用这种二合一的方法才能灵活的从各个地方申请空间而不是仅限于堆区。

我们用下面的图片来演示这个过程:

5.new和delete的错误报告方式:

和malloc动态开辟一旦失败直接返回NULL不同,new开辟失败会抛异常,我们一般用try catch语句来捕获处理,异常会引起执行跳跃,这一点有点类似goto语句,直接跳到catch的位置,若不用catch捕获,则程序会直接终止掉且没有任何征兆。一般我们会在catch里利用日志对错误进行记录,而且,捕获只会捕获try里面的,而且一旦程序正常是不会进入catch里面的,只有程序异常的时候才会进入到catch语句中。

如下:

try
{
   char*a=new char[100000000000000];
}
catch(const exception&e)
{
    cout<<e.what()<<endl;
}

这里由于空间开辟过大,故直接程序异常跳入catch语句内部,从而实现了C++new对异常的捕获。

6.内存泄漏问题:

没什么多说的,对于一些长期使用运行的程序来说,内存泄漏的问题很大,会让程序越来越卡,故我们必须要避免出现内存泄漏,对于开辟的内存我们一定要及时释放。

7.动态开辟的匹配问题:

前面我提到过,我们使用什么开辟,就要对应着使用什么来匹配释放,即malloc开辟就最好用free释放,new开辟就匹配着使用delete来释放,那为什么要这样处理呢?

让我们看下面的代码:

代码一:

int*arr1=(int*)malloc(sizeof(int));
free(arr1);
int*arr1=new int;
free(arr1);
或者//delete arr1;

在这种情况下,程序是都不会报错的。让我们接着看下面的程序:

代码二:

第一段程序段A
class A                           
{
public:
A(int a=0)
:_a1(a)
{}
~A()
{
cout<<"~A()"<<endl;
}
private:
int _a1;
};
另一段程序B:
class A                           
{
public:
A(int a=0)
:_a1(a)
{}
private:
int _a1;
};

对于程序A和程序B来说当我们A*p2=new A[10],

在释放的时候,针对情况一,我们使用free(p2),delete(p2)都会报错,只有常规的delete[] p2才能正常程序进行,但对于程序B来说,上述的三种形式是都可以的,都不会报错,那这是为什么呢?

问题的本质其实在于指针的问题,当我们开辟一个多个对象的空间时,由于有析构函数的存在,我们需要额外存储一个int的空间来存储我们开辟了多少个对象,为后面的delete[]的个数做准备,释放时它会向前偏移4个字节,即p2-1,从头指,然后调用[]里面给的析构次数,再用operator delete释放掉全部的空间,这一步对于多对象的动态开辟是必须的,因为你要告诉编译器要析构多少次,它就是根据最前面的int变量来判断的,否则编译器不知道析构多少次自然会报错。故我们用free(p2),delete(p2)不对其实就是指针的位置不对,应当向前移动一位再free即可,即free(p2-1),这样我们手动调整指针前移一位,即可识别个数的同时也能释放干净。

而对于第二种默认析构来说,编译器认为没必要调用析构了,故也就没必要生成最前面的int存储个数,故指针的指向就是对的。不需要前移,直接按照operator delete的方式释放即可了,故变成了正常的free释放,故怎样写程序都是不会报错的

**且注意,其实对于内置类型,由于不涉及构造和析构的过程,故我们怎样开辟和释放都是可以的,**没用硬性区别,但我的建议是能规范就规范,能匹配为何不匹配使用非要标新立异?

存储多对象的内存方式如下:

故,开辟和释放以后直接匹配就好了,不要弄很多幺蛾子来证明自己的水平多高超,反而是一种很愚蠢的错误

8.最关键!!:malloc/free new/delete的区别:

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地

方是:

我们分为两个方面来回答:

第一个是用法上

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,
    如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需
    要捕获异常
    第二个是底层的逻辑上
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new
    在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成
    空间中资源的清理
    这个很关键,一定要理解的记下来,考试或者面试的时候很常考的一个问题

总结:

以上便是全部的C++内存管理的内容,在我看来,C++无时无刻不在体现它对于自定义类型的重视,在实际的情况中自定义很常见,故能够很好的处理自定义类型的语言确实对我们的开发很有效,它能解决很多的语法限制。

目录
相关文章
|
5天前
|
安全 编译器 程序员
【C++初阶】C++简单入门
【C++初阶】C++简单入门
|
5天前
|
存储 编译器 C语言
【C++】C\C++内存管理
【C++】C\C++内存管理
【C++】C\C++内存管理
|
4天前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
6天前
|
存储 Java C语言
【C++】C/C++内存管理
【C++】C/C++内存管理
|
10天前
|
安全 编译器 C++
C++入门 | 函数重载、引用、内联函数
C++入门 | 函数重载、引用、内联函数
18 5
|
10天前
|
存储 安全 编译器
C++入门 | auto关键字、范围for、指针空值nullptr
C++入门 | auto关键字、范围for、指针空值nullptr
30 4
|
3天前
|
存储 监控 算法
掌握Java内存管理:从入门到精通
在Java的世界里,内存管理是程序运行的心脏。本文将带你走进Java内存管理的奥秘,从基础概念到高级技巧,一步步揭示如何优化你的Java应用。准备好迎接挑战,让我们共同揭开高效内存使用的面纱!
|
4天前
|
安全 编译器 C语言
|
4天前
|
存储 编译器 程序员
C++从遗忘到入门
本文主要面向的是曾经学过、了解过C++的同学,旨在帮助这些同学唤醒C++的记忆,提升下自身的技术储备。如果之前完全没接触过C++,也可以整体了解下这门语言。
|
1月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
100 14