C++第十一节课 new和delete
一、new和delete操作自定义类型
new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数(new会自动调用构造函数;delete会调用析构函数)
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是
// new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
return 0;
}
通过调试可以发现new可以将值初始化为1;
如果是多个对象:
A* p5 = (A*)malloc(sizeof(A)*10);
A* p6 = new A[10];
free(p5);
delete[] p6;
每个元素都会调用一次构造函数和析构函数!
此时数组会调用默认构造函数将每个元素初始化为0;
如果没有默认构造函数,那么此时需要我们自己向构造函数传递数值;
分析下面的代码:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class A
{
public:
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
上面类中只有系统提供的默认的构造函数,new初始化对象的时候调用系统提供的默认构造函数,但是这个默认构造是跟malloc一样,将数组中的元素初始化为随机值;
如果构造函数不是默认构造函数:
class A
{
public:
A(int a)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[4]{1,2,3,4};
A* p7 = new A[4]{ A(1),A(2),A(3),A(4) };
free(p5);
delete[] p6;
return 0;
}
这里相当于隐式类型转换:1,2,3,4类型为A,1会调用构造函数(作为参数传递)变为A类型;
等价于下面A* p7!两者是等价的!(A(1),A(2),A(3),A(4)是匿名对象);
- 如果A有默认构造,那么可以采用注释的方式,前三个根据提供的值进行初始化,最后一个根据默认构造进行初始化;
- 如果A没有默认构造,那么必须提供准确的值进行初始化,每个元素都需要提供;
如果new对象再free,malloc再delete出产生什么结果?
对于内置类型,一般不会出现大问题;
但是对于自定义类型:
直接会引发程序崩溃!
原则:一定包匹配使用,否则可能会出现大问题!(结果是不确定的!)
二、operator new与operator delete函数
operator new与operator delete不是一个运算符重载,而是一个全局函数!(库里面的)
free是一个宏函数,底层调用_free_dbg;
malloc如果失败,会返回空,但是面向对象语言处理失败,不喜欢用返回值,更建议用抛异常;
直到返回空然后程序结束;
32位的进程空间总共的寄存器大小为4G(会有4G的虚拟内存 / 堆的总大小不会超过2G);
使用new申请过于大的空间会直接报异常:
可以使用下面的形式捕获异常,catch会捕捉失败的地方(具体语法后面讲):
报异常后会将之前开辟的内存直接释放;
因此,虽然new的功能是:开空间 + 构造函数,开空间部分如果直接调用malloc,那么开辟失败会返回空指针,不会报异常,C++希望的是报异常;
因此引入:operator new,实际上是对malloc的封装,如果失败了会报异常!
因此,实际上new开空间的功能是调用operator new,而operator new实际上是调用malloc!
delete释放空间的功能实际上是调用operator delete函数,而operator delete函数底层是通过封装free函数来实现的!
通过观察可以发现:new实际上就是调用operator new和构造函数!(先开空间再调用构造函数)
同理:delete实际上就是调用operator delete和析构函数(先调用析构函数清理资源,再释放空间;)
三、定位new表达式(placement-new)
分析下面场景:如果我们需要申请一个堆上的栈对象!
调用new的时候,首先,创建的指针变量位于栈区,然后调用operator new在堆上创建对应的成员变量空间!然后会调用构造函数在堆区创建数组空间(堆上的_array指向arr)!
同理:这个过程中,会先调用析构函数清理stack对象指向的资源arr(析构函数释放由于构造函数开辟的资源),operator delete调用free将开辟的成员变量释放!
科普:定位new的用途
如果需要频繁的申请和释放内存(直接在堆上找到合适的空间是一个比较麻烦的事),那么我们可以构造内存池,每次从这个池子中去申请(直接在内存池中申请会比直接在堆上申请快一点);
new是直接在堆上找到合适的内存进行初始化,而我们在内存池中找到的空间没办法进行初始化!
这时候我们可以采用定位new进行初始化!
STL中的链表源码实际上就用到了定位new!
- 这里的construct就是调用定位new;
- destory就是显示调用析构函数;
- 并且代码量少的函数直接设置为内联;
总结:
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
- new (place_address) type或者new (place_address) type(initializer-list)
- place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
四、内存泄漏
cout打印int*是按照指针类型进行打印的,但是cout打印char*会将其识别为一个字符串!
对于上面的代码,申请1G的内存,打印的p1会是乱码;
cout将char*识别为一个字符串,打印字符串遇到 \0 才停止,但是上面申请的内存没有进行初始化,因此会一直找 \0 ,且没有初始化的空间为随机值。
因此,我们将其初始化就不会遇到上面的错误!
如果我们想要按照地址打印char*类型怎么办?
方法一:使用printf进行打印(%p);
方法二:使用cout将其转化为(void*)进行打印!
进程结束的时候,操作系统会自动的将进程给回收了;
因此,平时我们运行的时候,就算不手动释放,操作系统会帮我们自动释放;
总结:
- 普通程序,内存泄漏影响不大,进程正常结束会释放资源;
- 长期运行的程序(服务器),内存泄漏危害很大,例如 --- 游戏服务,电商服务......
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
五、模板引入
模板分为:函数模板 + 类模板;
引入关键字:template(模板)typename可以缩写为T,其中T被称为模板参数;
函数模板
模板参数定义的是类型;
template<typename T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
rifht = tmp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
Swap(a, b);
Swap(c, d);
return 0;
}
问题:对于上面的代码,两次调用的Swap是否是同一个函数?
答案:不是同一个函数!
根据汇编代码可以分析:调用的不是同一个函数,调用根据模板生成的具体的函数(这个过程也叫做模板的实例化);
编译器根据函数模板生成对应具体的函数!
注意点:C++内置自己提供了swap函数,不需要我们自己实现!
底部也是根据模板实现的!
这里的swap可以交换内置类型和自定义类型!