C++ - 内存管理 #new/delete #operator new/operator delete #placement-new #异常

文章目录

前言

一、引言

二、C/C++内存管理

1、C/C++内存分布

2、C语言中动态内存管理方式

3、C++内存管理方式

3.1、new/delete 操作内置类型

3.2、new 和 delete 操作自定义类型

4、operator new 与 operator delete 函数

4.1 operator new 与 operator delete 函数(重点)

5、new 和 delete 的实现原理

5.1 内置类型

5.2、自定义类型

详细理解:

6、定位new 表达式(placement-new)

知识点汇总:

详细理解:

7、malloc/free  和 new/delete 的区别

三、异常

1、异常的概念

2、异常的抛出与捕获

详细理解:

3、异常 与 new

总结


前言

路漫漫其修远兮,吾将上下而求索;


一、引言

Q1:在系统之中是以什么单位来管理资源的呢?

  • 进程为单位来进行管理的;

其实每一个程序都是一个进程(对于Windows、Linux 是这样的)

Q2:在哪里可以看见进程呢?

任务管理器,如下:

以上图中的全部都是进程;用代码写好了一个程序,这些程序会被编译成指令,又会将这些指令加载到内存中去执行

Q:以什么样的方式去执行呢?

  • 进程的方式去执行;

可以将进程想象成一个一个的工人,而操作系统就像是一个工厂;系统之中有一个叫做系统调度的东西,会去调度该进程去运行(利用一些调度算法,eg.时间片排队……);同时也会有多个进程在同时运行,因为电脑是多核,即多个CPU,如下:

而进程之中又有多线程的概念;

我们的程序本质上主要是在处理各种数据eg. Adoble Acrobcat 是将在磁盘上的文件读出来,而文件又有各种格式(以图片显式还是以文件显式等),去处理各种各样的数据;倘若想要处理这些数据首先是要有空间来存储(eg.执行递归调用就是要建立栈帧),那么此时每个进程均拥有自己的一个进程地址空间,该进程在运行的时候对其进程地址空间进行区分(给进程分配的空间又叫做虚拟内存,即不是真实的内存;虚拟内存需要与物理内存进行映射;其中物理内存就是我们电脑中实际的内存,会通过MMU、页表映射……)此处不再深入讲解,这部分的知识涉及操作系统的内存管理、进程管理;

那么这样看来的话,我们实际写的程序运行起来就是一个一个的进程;

我们调试代码,让程序挂起来然后在“任务管理器”中观察,如下:

因为要为每个进程分配内存,那么分配内存的时候,为了更好的管理,会将内存分为几个区域,如下:

  • :又叫堆栈,函数调用建立栈帧,空间向下增长;在一个函数之中定义的局部变量是存在其栈帧之中;所以说,函数结束其中的局部变量便销毁,准确来说是因为函数结束,该函数的栈帧销毁,故而这些局部变量也就销毁了;故而,栈主要在函数调用建立栈帧时会被用;栈不大,在Linux 下,一个栈只有8M;
  • 内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库;用户可使用系统接口创建共享内存,做进程间通信。(此处仅了解即可)
  • ,需要注意的是,堆的空间是向上增长的;堆中的空间是动态分配的空间,可以利用malloc、calloc、realloc、new 来申请;堆的空间很大;
  • 数据段,之前称之为静态区,是用来存放静态数据、全局数据;因为静态数据、全局数据要在整个程序的运行期间都要存在,于是乎就单独分了一块区域出来专门存放;在语言角度上喜欢称这块区域为静态区,而在操作系统的角度喜欢称这块区域为数据段;在有些地方还会将数据段再进一步地细分;
  • 代码段,从语言的角度喜欢称之为常量区,但从操作系统地角度喜欢称之为代码段;代码段中除了会存放常量还会存放一些编译好了的可执行命令(因为进程运行就是读取这个内存中存放在代码段的指令,然后再去执行这些指令);

显然,之所以定义不同的变量,其生命周期不同?是因为它们被存放在了不同的内存区域之中;

二、C/C++内存管理

1、C/C++内存分布

小练习:

题解如下:

需要注意的是sizeof(数组名) ,求的是该整个数组所占的空间的大小;而sizeof(指针),求的是这个指针变量所占的空间大小strlen 计算字符串的长度不会计算最后的 '\0';

关于 char char2[] = "abcd";程序在编译期间便会将常量字符串存放在常量区,即确定了该常量字符串的地址,char2 所在的函数当其函数栈帧开辟的时候,数组char2 也便有了其对应的空间,此时编译器就会将存放在常量区中的字符串"abcd" 拷贝放入该数组的空间中;所以 *char2 在栈中;

虽然上图中也有静态变量存放在函数main 中,但是它们的生命周期却不同;这是由于不同类型的数据存储在不同的内存区域每个内存区域存储的数据具有相似特性(例如:局部数据均是存放在栈这个区域的,更细分:对应所在函数被调用时所创建的栈帧之中)而不同的区域之中的数据其性质是不同的;全局、静态变量在整个程序期间均会使用,所以处于静态区的数据生命周期处于整个程序的运行期间;

2、C语言中动态内存管理方式

malloc/calloc/realloc/free

Q1: malloc/calloc/realloc 的区别?

  • malloc 只开辟空间;
  • calloc 在开辟空间的基础上会将空间按字节初始化为0
  • realloc 分为原地扩容、异地扩容;

realloc 的原地扩容与异地扩容如下:

注:不同的平台下,其实现的原理不同(在VS C语言库中实现malloc的方式与Linux 下实现malloc 的方式不同);

C++兼容C语言,既然C语言已经有了malloc 系列了,我们仍然可以在C++中使用,为什么C++推出了 new、delete 系列?

3、C++内存管理方式

C语言中的内存管理方式仍然可以在C++中使用,但是有些地方就显得无能为力,而且使用起来比较麻烦;因此C++就提出了自己的内存管理方式:通过new 和 delete 操作符去进行动态内存管理;

需要注意的是,C语言中的malloc/calloc/realloc/free 这一系列是一个函数调用;而C++中的new/delete 操作符

3.1、new/delete 操作内置类型

使用如下:

void Test()
{
	//动态申请一个int 类型的空间
	int* ptr1 = new int;

	//动态申请一个int 类型的空间并初始化为10
	int* ptr2 = new int(10);

	//动态申请5个int 类型的空间
	int* ptr3 = new int[5];

	delete ptr1;
	delete ptr2;
	delete[] ptr3;
}

调试如下:

倘若new 多个对象还想要初始化,可以使用 {} 来实现,如下:

这一点与C语言数组的初始化比较像,只要你给了初始化的值就按照你给的进行初始化,个数不够剩下的就会默认初始化为0;

需要注意的是:申请和释放单个元素的空间,使用new 和 delete 操作符,申请和释放连续的空间,使用new[] 和 delete[] , 需要匹配使用

C++ 中的new/delete 与 C语言中的malloc/calloc/realloc/free 的比较;

  • 1、malloc/calloc/realloc 的返回类型为 void* , 使用的时候需要进行强制类型转换,但是使用new 无需进行强转;
  • 2、使用malloc 需要计算实际所要开辟空间的字节数,而使用new 无需进行计算(需要开辟多个数据只需要在类型后面增加一个[] ,然后再其中填写所要开辟的空间个数);
  • 3、释放的时候,C语言调用free 即可; 在 C++ 中,申请和释放单个元素的空间,使用new 和 delete ;申请和释放连续的空间,则使用new[] 以及 delete[];需要进行配套使用
  • 4、malloc/calloc/realloc/free 是函数调用,而new/delete 是操作符
  • 5、C语言一系列的只有calloc 可以初始化,而C++中的new 在开辟空间的时候,初始化方式更加多元,倘若该对象为自定义类型还会自动调用构造函数;
  • 6、使用malloc/calloc/realloc 的时候需要检查其返回值以判断空间开辟是否正确;而new 如果开辟空间失败会抛出异常(通过一种抛异常的机制),无需人为进行检查;

Q:什么是抛异常机制?

  • 异常涉及继承、多态……异常是面向对象处理错误的一种方式,当出错时会抛出异常,而抛出异常之后需要跳到对应捕获异常的地方(可以见下文的简单的对异常的描述)

Q:C++中的 new 和 delete 与 C语言中的malloc/calloc/realloc/free 在功能上还有哪些区别?

  • 对于内置类型来说,在功能上没有太大的差别;但是对于自定义类型数据来说,差别就非常大,new、delete 会自动调用该自定义类型的构造、析构函数;

3.2、new 和 delete 操作自定义类型

代码如下:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0):" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	//开辟自定义类型空间
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);
	free(p1);
	delete p2;
	return 0;
}

运行结果如下:

使用malloc free:

使用new delete:

在申请自定义类型的空间的时候,new 会调用构造函数,delete 会调用析构函数,而 malloc、 free 不会去调用

所以,new/delete 与 malloc/free 的本质区别:new 对于自定义类型,它会自动调用其构造函数去初始化,delete 会调用析构函数,而malloc 只是单纯开辟空间,free 只是单纯释放空间;

对于new 来说,开辟内置类型空间可以初始化(想要初始化的话就需要手动写圆括号并给值),开辟自定义类型空间一定会初始化(因为会自动调用该自定义类的构造函数,而自定义类一定会有构造函数);

而对于内置类型,C语言中的malloc/calloc/realloc/free 与 C++中的new/delete 只有用法上的区别,功能上的区别不大;

注:new 多少个对象便会调用多少次构造函数去初始化;

代码如下:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0):" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;
	free(p1);
	delete p2;

	A* p3 = (A*)malloc(10 * sizeof(A));
	A* p4 = new A[10];
	free(p3);
	delete[] p4;

	return 0;
}

运行结果如下:

如果坚持用malloc/free开辟空间,但是又想要初始化,就只有利用循环手动给值,但是对于A类来说,我们可以手动给值吗?不能因为A类中的成员变量均是私有的 ,只有通过该类中的成员函数才可以访问私有的成员变量;并且也不可以显式调用其构造函数(就我们目前所学到的知识无法显式调用构造函数,但是实际上也还是有办法的,后面会学到placement-new);就我们目前而言,构造函数均是自动调用的(定义/实例化 对象的时候便会自动调用,即new 对象的时候会自动调用其构造函数);所以想要初始化用malloc/free 开辟空间并且还想要初始化的话,就得在该自定义类专门写一个可在外部调用其成员变量的初始化函数,然后再借助于循环进行初始化;

Q:倘若该自定义类没有默认构造函数的话,使用new/delete 会怎么样?

  • 会报错;

代码如下:


class A
{
public:
	A(int a )//非默认构造函数
		:_a(a)
	{
		cout << "A(int a = 0):" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;
	free(p1);
	delete p2;

	A* p3 = (A*)malloc(10 * sizeof(A));
	A* p4 = new A[10];
	free(p3);
	delete[] p4;

	return 0;
}

运行结果如下:

先来回忆一下什么是默认构造,默认构造有三个:无参、全缺省、没有显式实现编译器自动生成的;没有默认构造函数,而又想要编译器自动调用显然是不可行的,因为编译器不知道传什么参数,就会报错

Q: 既然该自定义类没有默认构造函数的话,又该怎么解决呢?

  • new 可以显式传参;先创建该类的对象(匿名对象或者有名对象),然后利用new 进行传参;此处有三种处理方式(有名、匿名、隐式类型转换)

A类的代码如下:

class A
{
public:
	A(int a )//非默认构造函数
		:_a(a)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

方式一:有名对象

方式二:匿名对象

方式三:利用隐式类型转换

因为构造函数的形参类型为int 类型,即支持用int 类型的数据去构造类型为A的数据,那么该处 int 类型的数据便要转换为A类型的数据,即将int 类型的数据拷构造一个A类型的临时对象,然后再将临时对象拷贝构造给new 开辟的空间;整个过程编译器会优化为直接的构造,所以此处为类型转换+编译器优化;

Q:如果该自定义类的构造函数的参数不仅只有一个又该如何处理?

  • 多参数的构造函数也支持隐式类型转换;

类A代码实现如下:

class A
{
public:
	//多参数的非默认构造函数
	A(int a1 , int a2)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1;
	int _a2;
};

运行结果如下:

需要注意的是,多参数的构造函数的使用不可能写成:,而应该写成:,因为用 ( ) ,会被当作是逗号表达式,而逗号表达式的返回值为其最右的表达式的结果从左至右依次计算各表达式的结果;

需要注意的是,使用new 开辟的空间初始化自定义类型数据(该自定义类型没有默认构造函数)不可以部分初始化,如下:

(A类的实现还是照上)

new 开辟内置类型数据空间的时候可以只初始化部分,未初始化的部分会默认初始化为0/nullptr/0.0等,因为 0 一般是内置类型构造函数的缺省值;而对于自定义类型的数据,如若给的初始化值的个数比实际new 开辟的对象的空间的个数要少,剩下没有给初始化值的部分就会去调用默认构造函数,但是如果该自定义类型没有默认构造函数就会报错;

解决:将A类的构造函数调整为默认构造函数,代码如下:

class A
{
public:
	//默认构造函数
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1;
	int _a2;
};

int main()
{
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1, 1);//传多个参数便好,这也是调用构造函数的一种方式
	free(p1);
	delete p2;

	A* p3 = (A*)malloc(10 * sizeof(A));

	//隐式类型转换 - 多参数的构造函数也支持隐式类型转换
	A* p4 = new A[10]{ {1,1},{2,2},{3,3} };

	free(p3);
	delete[] p4;

	return 0;
}

调试如下:

总结:

自定义类型的对象想要初始化,便一定会借助于其构造函数,倘若该构造函数为默认构造函数(不传参就可以可以使用给的构造函数)那么参数可传可以不传;但如果没有默认构造函数,就必须显式地传参,在new 的时候就必须初始化,不然会报错;

学习了C++之后就不再推荐使用malloc/calloc/realloc/free了,任何地方均推荐使用new/delete ,原因有以下几点:

  • 1、new/delete 它的语法比C语言的更加简洁
  • 2、new/delete 支持的功能更健全、更好用

对于内置类型C语言中的calloc 可以将开辟的空间按字节初始化为0,但是new 可以任意给值初始化,不显式给值便不初始化

而对于自定义类型C++中的new 一定会调用构造函数进行初始化;(若该构造函数为默认构造,那么可传参可不传参,如果为非默认构造函数就必须传参)

4、operator new 与 operator delete 函数

4.1 operator new 与 operator delete 函数(重点)

 new 和 delete 用户进行动态申请和释放操作符operator new 和 operator delete 系统提供的全局函数new 在底层调用operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间;

注:也就是说在C++库中还有一些特殊的函数:operator new 、operator delete ,并不是堆new 和 delete 的重载,此处比较特殊,之前 operator 和运算符放在一起表示对这个运算符的重载,这两个函数operator new 与 operator delete 就是两个全局函数,其用法与 malloc/free 的用法一模一样;

  • operator new : 该函数实际通过 malloc 来申请空间,当malloc 申请空间成功时直接返回;申请空间失败则尝试执行空间不足的应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常
  • operator delete : 最终是通过 free 来释放空间的;

源码实现如下:

/*
operator new
*/
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);
}

/*
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;
}

/*
free的实现
*/
#define   free(p)               _free_dbg(p, _NORMAL_BLOCK)

分析如下:

可以从上得出一个结论:operator new 与 operator delete 是 malloc 与 free 的封装

Q:operator new / operator delete 与 malloc /free 的区别是什么?

  • 二者没有太大的区别,只有一个小区别:使用operator new 无需检查其返回值,不过需要使用try……catch 去捕获异常,因为利用operator new 开辟空间失败之后并不是返回nullptr ,而是抛异常;

我们实际当中并不会常用operator new /operator delete , 因为 new /delete 就可以解决我们的需求;

new 始终都是要开辟空间的,其开的空间是在堆上,而去开辟堆上的空间仍需要调用系统库的接口——malloc(对于delete 同理,需要去调用系统库的接口——free);

运算符、操作符编译器编译以后便直接变为指令,new 底层是malloc ,并且调用构造函数(已经就有了向堆申请空间的机制就没有必要再重新再搞一个),但new 不会直接去调用malloc ,因为直接调用malloc 就需要判断其返回值,new 调用的是封装了malloc 的operator new;new 的核心操作: operator new 以及调用构造函数

系统库中造了operator new 与 operator delete 出来,我们可以使用,单并不只主要给我们去使用的,是给new 和 delete 使用的;而至于需要造 operator new 出来就是为了封装处理 malloc 、抛异常

5、new 和 delete 的实现原理

5.1 内置类型

如果申请的是内置类型的空间,new 和 malloc 、delete 和 free 基本类似;

不同的地方是:new/delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续的空间,而new 在申请空间失败会抛异常,malloc 会返回NULL;

5.2、自定义类型

new 的原理

  • 1、调用operator new 函数申请空间
  • 2、在申请的空间上执行构造函数,完成对象的构造

delete 的原理

  • 1、在空间上先执行析构函数,完成对象中资源的清理工作
  • 2、调用operator delete 函数释放对象中的空间

new T[] 的原理

  • 1、调用operator new[] 函数,而在operator new[] 中实际是调用operator new函数完成N个对象空间的申请
  • 2、在申请的空间上完成N次构造函数

delete[] 的原理:

  • 1、在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  • 2、调用operator delete[] 释放空间,实际在operator delete[] 中调用 operator delete 来完成N个空间的释放;

详细理解:

我们可以反汇编行观察:

代码:

class A
{
public:
	//默认构造函数
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1;
	int _a2;
};

int main()
{
	//new --> operator new + 构造函数
	A* p1 = new A(1, 1);

	//delete --> 析构函数 + operator delete 
	delete p1;

	return 0;
}

转到反汇编:

delete 是先调用析构函数然后再调用operator delete,这是因为需要先释放资源让后再释放整体空间;而new 开辟空间抛出异常是从operator new 中来的

Q:倘若new 多个对象呢?

代码如下:

class A
{
public:
	//默认构造函数
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1;
	int _a2;
};

int main()
{
	//new --> operator new + 构造函数
	A* p1 = new A(1, 1);
	A* p2 = new A[10];

	//delete --> 析构函数 + operator delete 
	delete p1;
	delete[] p2;

	return 0;
}

转到反汇编:

需要注意的是,在使用new 与 delete 的时候注意匹配使用;

new 和 delete 不匹配使用的时候有时候会报错,而有时候不会报错,这是为什么呢?

1、对于内置类型,malloc 和 delete 乱搭,没有报错,如下:

2、对于自定义类型, malloc 与 delete 乱搭,没有报错,如下:

类A的实现:

class A
{
public:
	//默认构造函数
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1;
	int _a2;
};

3、对于内置类型 new[] 和delete 乱搭:

Q: 也没有报错,并不会内存泄漏?为什么?

  • 对于内置类型而言没有构造函数这一说法,new 相当于调用 operator new申请了40 byte 的空间,而operator new 又会调用malloc ,故而相当于是使用malloc 申请了 40byte 大小的空间;而delete 会转换成调用 operator delete , operator delete 会调用 _free_dbg,而 _free_dbg 就是 free;所以内置类型 new[] 与 delete 的乱搭本质上就是使用malloc 开辟空间,delete 释放空间,故而没有报错;

4、对于自定义类型, new[] 和 delete 乱搭:

程序出现了错误;

new[] 会去调用operator new[] 和A类的构造函数, 而operator new[] 会去调用operator new;

调试观察:

Q:多开辟的4byte 的空间会拿来干什么?

  • 未来此处需要80byte 的空间来存放10个A类型的对象,但是实际上还会开辟 4byte 的空间来放在这80 byte 空间的前面,在 4byte 空间之中会存储new 的对象个数以确定调用多少次的析构函数

new[] 的时候转换成指令知道自己要new 多少个对象,但是delete 由于不知道new 了多少个对象,也就不知道自己要调用多少次析构函数;所以new[] 开辟空间时会在原基础上多开辟4byte 的空间,而返回的p4 是这4byte 之后的地址,即返回的是本应开辟的空间的起始地址;当delete[] 转换成指令之后会从实际应该开的空间的首地址(p4)往前减 4byte 的空间去获取该4byte 空间中存放的值,那么delete[] 便就会知道需要调用多少次析构函数;当operator delete[] 释放空间时时从整体开辟的空间的首地址开始释放的(释放84 byte 大小的空间,free 释放空间不能只释放部分也不能从中间释放);

显然对于自定义类型的数据去开辟空间,若用new ,无需多开辟 4 byte 的空间,因为只开辟一个对象的空间; 而 delete 会默认只有一个对象,new 和 delete 本身就是匹配的;而若用new[] 和 delete 匹配,报错的原因有两个,一是因为析构函数的次数没有调够;二是因为delete 在调用operator delete 释放空间时只会从p4 的位置开始释放(底层的free 是不允许只释放动态开辟部分的空间)

而对于内置类型的new[] ,由于内置类型的对象不存在需要调用析构函数,所以new[] 在开辟空间的时候不会额外再多开辟4byte 的空间;所以对于内置类型,new[] 与 delete 配对使用并不会报错;

new[] 与 delete 配对使用报错的原因:

1、析构函数未调用完,存在内存泄漏的风险

2、释放的位置不对

需要注意的是,上述说的内存泄漏编译器不会报错;

我们将A类的析构函数屏蔽掉:

代码如下:

class A
{
public:
	//默认构造函数
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A():" << this << endl;
	}

	//~A()
	//{
	//	cout << "~A():" << this << endl;
	//}
private:
	int _a1;
	int _a2;
};

int main()
{
	//内置类型 - new[] 和 delete 乱搭
	int* p3 = new int[10];
	delete p3;

	//自定义类型 - new[] 和 delete 乱搭
	A* p4 = new A[10];
	delete p4;

	return 0;
}

运行结果如下:

调试如下:

Q:为什么没有显式实现析构函数,就没有多开辟那4byte 的空间?

编译器优化。编译器首先去分析了一下A类之中并没有显式实现析构函数,并且编译器自己生成的析构函数也没有释放什么资源,即调用编译器自己生成的析构跟不调用的区别不大,于是编译器便优化不再调用。而delete[] 不调用对应的析构函数,那么new[] 便不会多开辟 4byte 的空间,所以最后delete 释放的位置是对的,所以没有报错;

释放位置是对的,但是没有调用析构函数,如果该类存在资源的话,不就会造成内存泄漏吗?

我们用没有析构的Stack 实验一下:

代码如下:

class Stack
{
public:
	Stack(int n = 4)
	{
		cout << "Stack()" << endl;
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("Stack malloc");
			exit(-1);
		}
		_top = 0;
		_capacity = 0;
	}
	//不实现析构
private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	//自定义类型 new[] 与 delete 乱搭
	Stack* p1 = new Stack[10];
	delete p1;
	return 0;
}

运行结果如下:

Satck 是存在资源的,但是编译器没有报错,这是为什么呢?

  • 编译器在一些特殊的情况下会检查到空间使用错误(类似于数组越界的抽查)然后才会报错;但是如果编译器没有检查到空间使用异常,也会正常结束程序

对于自定义类类型的对象,其new[] 和 delete 不匹配不一定会出问题;

  • 问题出现的原因在于程序遵循了默认机制。当自定义类实现析构函数时,使用 new[] 运算符会在内存空间前额外分配 4 字节用于记录数组元素个数。此时若直接使用 delete 释放内存,会导致释放位置错误
  • 不出问题,是因为没有实现析构函数,因编译器自己实现的析构未释放什么资源,所以编译器便优化不去调用析构函数,所以new[] 时就不会多开辟 4byte 的空间,使得  delete 释放的位置就是正确的,从而避免了问题。

所以,new/delete 、new[]/delete[] 的底层都挺复杂的,不要随便去匹配;

new[] 的底层就是malloc + 构造, delete的底层是free + 析构;并不会malloc 申请了10个对象的空间,而free 只释放其中一个;因为malloc 、free 的底层也会去记录指针指向的空间有多大;

严格来说,new[] 和 delete[] 并不会关联底层的申请和释放空间,关联的是构造函数与析构函数

6、定位new 表达式(placement-new)

知识点汇总:

定位new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

定位new 又叫做:placement-new

使用格式:

  • new(place_address) type 或者 new(place_address) type(initializer-list)

place_address 必须是一个指针initializer-list 类型的初始化列表

使用场景:

  • 定位new 表达式在实际中一般是配合内存池使用,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要利用 new 的定位表达式进行显式调用构造函数进行初始化

详细理解:

使用例子如下:

//定位new 的使用
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
public:
	int _a;
};

int main()
{
	//p1 现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为没有进行初始化未调用构造函数
	A* p1 = (A*)operator new(sizeof(A));

	//如果A类对象的构造函数有参数,此时就需要传参
    //定位new 的使用
	new(p1)A;
	return 0;
}

不能直接对p1 指向的这块空间进行初始化,因为类A中的成员变量是私有的,在类外无法直接访问;并且也不可以直接调用构造函数,因为构造函数是对象实例化编译器自动调用的,我们无法显式调用;有两种方法可以解决初始化问题:

  • 方法一:在该A类中提供一个公有的成员函数。eg.Init ,然后让 p1 去调用该成员函数Init 去完成初始化工作;此方法可行,但是此Init 函数实际上与其构造函数的功能相重叠,一个初始化功能,却要实现为两个函数,显得繁琐而不够简练;
  • 方法二,使用定位new(placement-new) ,来对已分配的空间调用构造函数去进行初始化;

而至于使用定位new 的格式: 

new(place_address) type 或者 new (place_address) type (initializer-list)

new(palce_address)type(initializer-list) 的使用如下:

//定位new 的使用
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
public:
	int _a;
};

int main()
{
	//p1 现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为没有进行初始化未调用构造函数
	A* p1 = (A*)operator new(sizeof(A));
	//new(p1)A;
	new(p1)A(10);//传值调用构造函数
	return 0;
}

需要注意的是,我们还可以显式调用析构函数不支持直接显式调用构造函数,想要显式调用构造函数就得使用定位new),如下:

我们平时一般是直接使用new 而不是使用 operatoor new + 定位new ;

Q:定位new(placement-new) 的真正用途体现在哪里?

  • 现实生活中确实不会要求我们不用new 而是用operator new 与 定位new;但是有些场景(eg.内存池)之中我们仍然会选择不会用new;

Q:什么是内存池?

内存池提供了一个技术:池化技术计算机中用得比较多)

常见的池化技术有:内存池、线程池、连接池……(将这些东西放在一个池子里面,当我们有需要的时候随时都可以用);

Q:为什么要把这些东西蓄起来呢?

内存、线程等均是属于系统,当我们想要去申请一块内存,创建一个线程、连接等,其本质都是属于系统的资源(均要向系统去申请);而当一个程序运行起来时候,会存在多个要资源的“人”;

操作系统可以类比为一个工厂:工厂中的工人相当于进程,而每个进程可能包含多个线程(即多个"工人"并行工作)。为了提升效率,这些线程可以在多个CPU上并行执行。

当进程中的多个线程都需要申请内存时,如果每次都直接向操作系统申请(频繁地进行内存分配和释放),会干扰操作系统的正常运行,导致性能下降。因此引入了"内存池"的概念:内存池中的内存并非凭空产生,而是来自堆区的动态分配。

使用内存池的优势在于:

  1. 无需频繁地向堆区申请内存(远程操作速度较慢),使用内存池申请空间速度更快
  2. 可直接使用内存池中的预分配空间,不用面临竞争
  3. 减少对操作系统运行的干扰

通过内存池机制,既提高了内存分配效率,又降低了对系统性能的影响

注:多线程还会涉及竞争的问题,多个线程同时向操作系统申请堆中的空间,而堆中的数据有可能会混乱而而产生线程安全的问题,那么此时就会涉及线程加锁的问题,而内存池可以解决竞争的问题;

所以内存池可以解决多线程竞争的问题,更适用于频繁地申请和释放内存;

malloc 和 new 是在堆上申请空间,即向操作系统申请去堆上开辟空间。倘若要频繁地申请、释放内存,直接找系统申请其效率会有损失并且还会面临竞争地问题。所以机会给自己建立一个“池子”,一次多申请点空间放在内存池中,而内存池中的内存开源于堆;

而线程池、进程池等的池化技术与内存池相似,其本质为:提前储蓄一些资源为“我”专用以提高效率;

内存池、库之中有一个,但是设计地比较简单,很多公司若要使用内存池会自己造一个轮子,或者用一些开源项目中的内存池;内存池只申请了内存,会提供类似于 alloc 的接口来帮助我们申请内存此时申请的内存便不会帮我们调用构造函数进行初始化;所以需要我们自己调用定位new(placement-new) 去完成初始化

我们在STL中也可以看到,在一些会高频申请内存的数据结构中,例如:链表,stl_list.h 部分源码如下:

template <class T, class Alloc = alloc>
class list {
protected:
  typedef void* void_pointer;
  typedef __list_node<T> list_node;
  typedef simple_alloc<list_node, Alloc> list_node_allocator;
public:      
  typedef T value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type& reference;
  typedef const value_type& const_reference;
  typedef list_node* link_type;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;

public:
  typedef __list_iterator<T, T&, T*>             iterator;
  typedef __list_iterator<T, const T&, const T*> const_iterator;

#ifdef __STL_CLASS_PARTIAL_SPECIALIZATION
  typedef reverse_iterator<const_iterator> const_reverse_iterator;
  typedef reverse_iterator<iterator> reverse_iterator;
#else /* __STL_CLASS_PARTIAL_SPECIALIZATION */
  typedef reverse_bidirectional_iterator<const_iterator, value_type,
  const_reference, difference_type>
  const_reverse_iterator;
  typedef reverse_bidirectional_iterator<iterator, value_type, reference,
  difference_type>
  reverse_iterator; 
#endif /* __STL_CLASS_PARTIAL_SPECIALIZATION */

protected:
  link_type get_node() { return list_node_allocator::allocate(); }
  void put_node(link_type p) { list_node_allocator::deallocate(p); }

  link_type create_node(const T& x) {
    link_type p = get_node();
    __STL_TRY {
      construct(&p->data, x);
    }
    __STL_UNWIND(put_node(p));
    return p;
  }
  void destroy_node(link_type p) {
    destroy(&p->data);
    put_node(p);
  }

protected:
  void empty_initialize() { 
    node = get_node();
    node->next = node;
    node->prev = node;
  }

  void fill_initialize(size_type n, const T& value) {
    empty_initialize();
    __STL_TRY {
      insert(begin(), n, value);
    }
    __STL_UNWIND(clear(); put_node(node));
}

分析如下:

从上图中可以知道,本质上create_node 和 destroy_node 是在模拟new(开辟空间+调用构造初始化) 和 delete(调用析构 + 释放空间) 的功能

Q:为什么要模拟而不直接使用 new  和 delete 呢?

使用new 时直接向操作系统申请堆上的空间,delete 会将空间还给操作系统——即new 和 delete 申请的是堆上的空间;而在list 的实现中不想频繁的申请空间堆上的空间来打扰操作系统,从而选择使用内存池(memory pool),所以需要模拟new 和 delete 的行为;而构造函数不可以显式调用,所以只能使用定位new (replacement-new) ,析构函数可以直接调用(封装在 destroy 中)

7、malloc/free  和 new/delete 的区别

共同点:都是从堆上申请空间,并且需要用户手动释放

不同点:

  • 1、malloc 和 free 是函数, new 和  delete 是操作符
  • 2、malloc 申请的空间不会初始化,new 申请的空间可以初始化(内置类型想初始化就传参,自定义类型一定会初始化)
  • 3、malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可,如果是多个对象,在[] 中指定对象的个数即可;
  • 4、malloc 的返回值为 void*, 在使用时必须强转,new 不需要,因为new 后面跟的是空间类型
  • 5、malloc 申请空间失败的时候会返回NULL,因此使用malloc 的时候需要判空来判断空间是否开辟成功;而new 不需要判断返回值,但是需要捕获异常
  • 6、申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间之后会自动调用构造函数完成对象的初始化工作,delete 在释放空间前会先调用析构函数完成空间中资源的清理释放;

前五点是malloc/free 和 new/delete 用法上的区别,最后一点是功能上的区别;

注:在C++的面试中特别爱问malloc/free 和 new/delete 的区别、指针与引用的区别……不要死记硬背重在理解~

三、异常

1、异常的概念

知识点汇总:

  • 异常的处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理;异常使得我们能够将问题的检测解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无需知道问题的处理模块的所有细节;
  • C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦;而异常时抛出一个对象,这个对象可以涵盖更全面的各种信息;

2、异常的抛出与捕获

  • 程序出现问题的时候,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个catch 的处理代码来处理异常
  • 被选中的处理代码时调用链中与该对象类型匹配且离抛出异常最近的那一个;根据抛出对象的类型和内容,程序的抛出异常部分告知处理部分到底发生了什么错误;
  • 当throw 执行的时候,throw 后面的语句就不会再被执行程序的执行从throw 位置跳转到与之匹配的catch 模块,catch 可能是同一函数中的一个局部的catch ,也可能是调用链中另一个函数中的catch ,控制权从throw 位置转移到了catch 位置;这里还有两个含义:1、沿着调用链的函数可能提早退出;2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁;
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出异常对象可能是一个局部对象,所以生成一个拷贝对象,这个拷贝的对象会在catch 句子后销毁;(这里类似于函数的传值返回)

详细理解:

也就是说,异常是由一个关键字 throw 抛出的;在C语言中通过设置错误码或者以返回值的方式来判断该程序是否有错,而在C++之中的异常,是抛出一个对象(可以抛出任意类型),倘若要抛出的类型是一个字符串,在字符串中便可以携带出错的信息;相较于C语言的错误码,C++抛出异常的好处在于:错误码本质上就是对错误的编号,需要在拿到错误码然后去查出了什么错误;遇到问题就返回一个数字(整形即错误码),在返回上比较简单,但是去查究竟出了什么错误就有消耗,使用如下:

double Divide(int a, int b)
{
	try
	{
		//当b == 0 时抛出异常
		if (b == 0)
		{
			string s("Divide by zero condition!");
			throw(s);
		}
		else
		{
			return ((double)a / (double)b);
		}
	}
	catch (int errid)
	{
		cout << errid << endl;
	}
	return 0;
}

Q:如何对异常进一步处理?

  • 捕获异常;

异常必须被捕获,如果异常未被捕获,那么便会报错;

代码如下:

double Divide(int a, int b)
{

	//当b == 0 时抛出异常
	if (b == 0)
	{
		string s("Divide by zero condition!");
		throw(s);
	}
	else
	{
		return ((double)a / (double)b);
	}
	return 0;
}


int main()
{
	//未捕获异常
	int len, time;
	cin >> len >> time;

	cout << Divide(len, time) << endl;
	return 0;
}

Q:异常可以在哪里进行捕获?

  • 可以在或抛出异常的当前位置进行捕获,也可以在外层进行捕获

只不过,更多地是在外层进行捕获,二捕获的时候会用到关键字: try …… catch ;抛出异常是什么类型,那么捕获时就会用该类型去捕获,此处还有一个执行流跳转的逻辑,代码如下:

{

	//当b == 0 时抛出异常
	if (b == 0)
	{
		string s("Divide by zero condition!");
		throw(s);
	}
	else
	{
		return ((double)a / (double)b);
	}
	return 0;
}

int main()
{
	try
	{
		int len, time;
		cin >> len >> time;

		cout << Divide(len, time) << endl;
	}
	catch (const string& s)
	{
		cout << s << endl;
	}

	return 0;
}

Q:何为执行流跳转逻辑?

调试一下~

无论套了多少层函数,只要抛出了异常便会跳转到捕获异常的地方去,如果有多个抛出就会看先后性,即先捕获谁那就先跳转谁

同理,倘若此处未抛出异常也就不会执行catch 语句;

假设有三个函数,func1() func2() func3(), 在 func2()中调用func1() , 在func3 中调用func2(), main() 中调用func3() , 并在func1() 中抛出一个异常,在main() 用catch 语句捕获,如下:

栈展开过程如下:

首先会检查throw 本身是否存在try 块内部,如果是在,则去检查匹配的catch 语句;如果有匹配的,就处理;没有则退出当前函数栈帧,继续在调用函数的栈中进行查找,不断重复上述过程,若到达main 函数的函数栈帧,依旧没有匹配的,则会报错,有则处理;

3、异常 与 new

代码如下:

void Func()
{
	int* p1 = new int[1024 * 1024];
	cout << p1 << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const std::exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果如下:

需要注意的是,new 抛出的异常是 execption 的派生类,exception 是库中的(execption 就是异常的意思);在execption 之中提供了what 这一函数,what 函数会返回库中的异常信息;如果想用new 开辟空间而又不想处理其错误,就可以使用try……catch 进行捕获execption 的异常;

我们对上面的代码进行整改,设置一个while循环,一直开辟空间直至开不了为止,看一下捕获的异常是怎么样的,代码如下:

void Func()
{
	int i = 0;
	while (1)
	{
		int* p1 = new int[1024 * 1024];
		cout <<  i << "-->" << p1 << endl;
		i++;
	}
}

int main()
{
	try
	{
		Func();
	}
	catch (const std::exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果如下:

注:在日常练习中用new 开辟空间无需写 try……catch 去捕获异常,因为堆的空间很大,一般很少new 对象失败;除非没有内存了或者你申请了一个巨大的空间,此时便会失败但在项目之中必须要用 try……catch 去捕获异常;

知识点回顾:

在32 位机器下,相当于有32条地址总线,地址总线通电便会产生电信号,分为正脉冲和负脉冲。地址信息会下达给内存,在内存之中便可找到该地址对应的数据,将数据通过地址总线传入CPU寄存器;32位机器会产生2^32 个地址,一个地址是一个内存单元的编号,而内存单元的大小为 1 byte, 1024 byte  = 1KB, 1024KB = 1MB, 1024MB  = 1GB;故而2^32 为 4GB,所以32位机器整个进程地址空间为4GB;相较而言,那么64位机器的进程地址空间便会更大;2^64byte = 2^34GB;而1TB = 1024GB , 1PB = 1024TB,1EB = 1024 PB;


总结

1、malloc/calloc/realloc 的区别?

  • malloc 只开辟空间;
  • calloc 在开辟空间的基础上会将空间按字节初始化为0
  • realloc 分为原地扩容、异地扩容;

2、C语言中的malloc/calloc/realloc/free 这一系列是一个函数调用;而C++中的new/delete 操作符

3、new/delete 与 malloc/free 的本质区别:new 对于自定义类型,它会自动调用其构造函数去初始化,delete 会调用析构函数,而malloc 只是单纯开辟空间,free 只是单纯释放空间;

4、 多参数的构造函数的使用不可能写成:,而应该写成:,因为用 ( ) ,会被当作是逗号表达式,而逗号表达式的返回值为其最右的表达式的结果从左至右依次计算各表达式的结果;

5、 new 和 delete 用户进行动态申请和释放操作符operator new 和 operator delete 系统提供的全局函数new 在底层调用operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间;

6、定位new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

定位new 又叫做:placement-new

使用格式:

  • new(place_address) type 或者 new(place_address) type(initializer-list)

place_address 必须是一个指针initializer-list 类型的初始化列表

7、使用内存池的优势在于:

  1. 无需频繁地向堆区申请内存(远程操作速度较慢),使用内存池申请空间速度更快
  2. 可直接使用内存池中的预分配空间,不用面临竞争
  3. 减少对操作系统运行的干扰
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值