码迷,mamicode.com
首页 > 编程语言 > 详细

C++内存管理(一)

时间:2020-07-14 00:39:23      阅读:70      评论:0      收藏:0      [点我收藏+]

标签:std   释放   构建   调用   car   因此   信息   info   inf   

C++内存管理(一)

这将会是一个系列的教程,以我个人的理解和网上的一些资料(包括侯捷老师的视频)来总结C++内存管理的详细内容。有错误之处,请大佬们多多指出,欢迎批评。

C++支持对内存创建的四个操作

C++支持内存创建的操作总共有四个,分别是:new,operator new,malloc,allocator。对应的释放内存的操作有 free()deleteoperatordelete allocator<T>::deallocate()

分配 释放 类型 可否重载
malloc() free() C函数 不可
new delete C++表达式 不可
operator new() operator delete() C++函数
allocator::allocate() allocator::deallocate() C++函数 可自由设计用来搭配容器

我们可以看到上面的表格罗列出了C++面向程序员提供的四种可以对内存的操作,那么我们先把目光放在前3种操作,看看他们之间的关系是什么,又有什么不同,我们一起来看一下。

new、delete、free()、malloc以及operator new,operator delete

new 是C++的关键字,当使用它的时候,它总是一个表达式。我们要深入new 使用的时候发生了什么,首先我们就先要知道new在被使用的时候需要完成两个操作 :

  • 在堆上分配空间
  • 使用构造函数初始化内存。

new基本的使用方法如下:
A *P = new A

编译器大致会把这句话转换成下面的代码:

  A* pc; /*假如申请的类型是Class A*/
  try
  {      
     void* mem = operator new(sizeof(A)); //申请内存空间
     pc = static_cast<A*>(mem); 
     pc->A::A();       //调用构造函数
     return mem; 
  }
  catch (std::bad_alloc)
  {
       //若allocation失败就不执行构造函数
  }

我们来分析一下,首先,我们发现编译器在new使用的时候,实际上使用了operator new()函数来申请内存。因为我们有申请内存的动作,所以可能会产生内存分配失败的结果,因此我们使用了try catch语句来将申请内存的代码包裹住以捕获失败的异常。在void* mem = operator new(sizeof(A));代码的下几行,编译器会调用申请内存的对象的构造函数。这里,我们可以看出编译器就实现了我们上面说的两个步骤,申请内存然后调用构造函数。

接着我们再深入看一下 operator new()函数的源代码,如下所示 :

void * operator new(size_t size, const std::nothrow t&)_THROW0()
{
      void *p;
      while(p = malloc(size)) == 0)
      {
            _TRY_BEGIN
                  if(_callnewh(size) == 0) break;
                        _CATCH(std::bad_alloc) return(0);
            _CARCH_END
      } 
      return (p);
}

我们可以看到上面的代码,operator new()函数的内部实际上是调用了malloc()函数来申请内存,所以我们可以知道真正申请内存的动作是依靠调用malloc()函数来实现的。因此,我们再看上面代码的流程,首先是while循环里面的条件语句p = malloc(size)) == 0,它表示我们调用malloc()函数来申请大小为size的内存空间,并让指针P指向它的地址。如果申请成功了,那么指针p != 0 我们的代码就会跳出循环返回指针p,否则进入循环调用_callnewh(size)函数,这个函数的作用是它会主动释放系统内存,释放的大小为我们要申请内存的大小,这样当我们再次执行循环条件进行内存申请的时候内存申请就会成功。

delete

我们用new完成了申请内存,那么当我们使用delete释放内存的时候又会发生什么呢?我们首先要清楚,delete关键词被使用的时候要完成的两个动作:

  • 先析构
  • 再释放内存

就拿上面我们申请的A类型
当我们用 delete p的时候,代码会被编译器转为以下这种意思:

p->~A();      //析构
operator delete(p);//释放内存

我们可以发现当使用delete的操作的时候系统会默认先执行对象的析构函数,再执行operator delete()函数,接着让我们再看一下operator delete() 的函数源码:

void __cdecl operator delete(void *p)_ THROW0()
{
      free(P);
}

你会发现c++在delete的底层是调用free()来实现内存的释放。

所以到目前为止我们已经能够清晰的理出operator new/deletefree()malloc()new/delete 三者之间的关系了
new delete使用的时候,c++会调用operator new()operator delete()函数,然后在 operator new()operator delete()的函数中它们分别调用 malloc()free()函数来完成内存申请和内存释放。所以真正完成申请内存动作和释放内存动作的函数是malloc()free()

创建数组的内存 Array new 和 Array delete

数组的内存申请
基本使用方法如下:
A * p = new A[3];
数组的申请和之前单个元素的申请方式没有什么区别,底层依旧是调用malloc()函数,只不过申请的内存的大小变为了n个元素的大小。但它有一个独特的地方,它会在你申请好的堆内存中放入一个叫做cookie的东西并在里面写入数组长度的信息。(这一点会在后面的文章详细阐述,目前只要知道有这个东西就好了)如下图所示:
技术图片

数组的释放
基本的方法如下
delete [] p;
我们可以看到,数组的释放内存的方式比单个元素多了一个[]数组下标的标志,那么我们是否可以不加[],不加这个再释放数组会怎么样呢?
我们用以下代码做一下实验,首先我们看一下加了[]的代码

#include<iostream>
#include<stdlib.h>
class A {
public:
	A(int x) : id(x) { std::cout << "ctor.this = " << this << " id = " << id << std::endl; }
	A() :id(0) { std::cout << "ctor.this = " << this << " id = " << id << std::endl; }
	~A() { std::cout << "dtor.this = "  <<  this  << " id = " << id<< std::endl; }
private:
	int id;
};

int main() 
{
	A * p = new A[3];	

	std::cout << "析构开始调用" << std::endl;
	delete[]  p;
	system("pause");
	return 0;
}

输出信息
技术图片

我们可以看到new的时候地址是从小到大的,而delete[]的时候地址是以相反的顺序进行释放也依次调用了对象本身的析构函数。一切都很正常。

如果我们不加[]并且我们创建一个类型 A ,它有一个 int 类型的指针成员对象idid能够被构造函数的new一个额外的内存空间。代码如下

#include<iostream>
#include<stdlib.h>
#include<string>


class A {
public:
	A() :id(new int) { std::cout << "ctor.this = " << this << " id = " << id << std::endl; }
	~A() { 
		std::cout << "dtor.this = " << this << " id = " << id << std::endl;  
		delete id;
	}
private:
	int * id;
};

int main() 
{
	A * p = new A[3];
	delete p;//没有加[]
	system("pause");
	return 0;
}

如果我们在vs中运行上面这个代码,它会引起报错。很明显是因为我们没有加[]引起错误,这是为什么呢?其实我们前面说到,在数组申请内存的时候系统会在给你申请的内存中写一个c ookie 记录你创建数组元素的多少。但是如果你不加[]它不会读取这个cookie记录的数据 。他只会把你申请的数组的那块空间当做单个对象申请的一整片空间,也就是说他在释放的时候只会执行一次析构函数。如果你加了[]系统会读取那个 cookie 知道你申请的是个数组,并且根据元素的数量,一 一 的调用数组的每个元素的析构函数。
而我上面代码,我的id在每次构造的时候都会额外的申请一个int的内存空间,如果我delete之前没有加[]那么它就会只调用一次析构,那么其他元素id申请的内存空间就不会释放从而造成内存泄漏。
所以如果是系统内置类型如:intchar之类的申请了一片数组空间释放的时候只用delete而不用delete []并不会发生错误的,但如果是类对象并且它的析构函数会释放自己申请的一些额外的内存,用delete而不用delete []在一些老式的编译器他可能会让你运行起来而引起内存泄漏。导致你没有释放掉你类对象自己本身申请的内存。

placement new

它的效果就是在一块已经创建好的内存中调用构造函数,我们可以看一下代码例子:

class A {
public:
      A(int x) : id(x) { std::cout << "ctor.this = " << this << "id = " << id << std::endl; }
      A() :id(0) { std::cout << "ctor.this = " << this << "id = " << id << std::endl; }
      ~A() {}
private:
      int id;
};
int main()
{
      我们创建了一个类他有一个成员变量id 在创建对象的时候构造函数默认的把它初始化为0
       A * p = new A; 
      
      //接着我们用placement new 的代码方法,在一块申请好的内存上用new直接调用构造函数,查看输出信息,调用成功。
      A * p = new A;	
      p = new(p)A(2);
     
}

技术图片

注意:placement new 的使用必须基于一个已经创建好的堆上空间进行调用。
为什么使用placement new主要有以下几点:

  • 在已分配好的内存上进行对象的构建,构建速度快。
  • 已分配好的内存可以反复利用,有效的避免内存碎片问题
    我们还会在后面的文章详细的讲到placement new 的用法以及用途

我们来总结一下今天学习的主要内容

  • operator new/delete 和 new/delete 和free()/malloc()之间的关系
  • Array new/delete 数组的内存的创建和释放
  • placement new 的基本使用

C++内存管理(一)

标签:std   释放   构建   调用   car   因此   信息   info   inf   

原文地址:https://www.cnblogs.com/Alfredo/p/13289325.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!