我们在C语言当中想要使用堆区的空间的时候就需要使用malloc函数进行手动的申请,但是我们在申请的时候需要手动进行计算,经过计算之后还需要进行判空操作,并且还不能进行任意值的初始化。这一切看起来在学习完C++当中的动态开辟之前显得很正常,但是在学习完C++当中的动态内存开辟之后我们就会感绝倒C语言的麻烦之处,接下来我们就来认识一下C++当中的动态内存开辟操作符,并将它和C语言当中的malloc函数进行对比吧。
一:认识new和delete操作符
在C++当中想要动态内存开辟的时候我们使用的是new和delete操作符。我们通过一段代码来学习他的使用方法:
#include<iostream>
int main()
{
//使用new操作符进行动态内存开辟,使用delete进行内存空间的释放
//开辟一个字符的空间并使用
char* pyes = new char;
*pyes = 'y';
std::cout << *pyes << std::endl;
//开辟一个连续的空间
int* pret = new int[3];
int i = 0;
for (i = 0; i < 3; i++)
{
pret[i] = i + 1;
std::cout << pret[i] <<" ";
}
//使用delete释放单个元素开辟的空间
delete pyes;
//使用delete释放动态开辟数组的空间的时候需要使用以下格式进行空间的释放
delete[] pret;
return 0;
}
通过上面的代码我们肯定可以发现C++当中很多不同的用法。想要动态内存开辟就需要使用new操作符,new操作符后面跟的是我们想要开辟的空间的类型,不像C语言当中的malloc函数返回值是void所以在使用的时候还需要对目标的空间进行强转,new开辟出来的空间直接使用即可。
如果想要开辟一串连续的空间的话,我们只需要在new的类型的后面加上一个方括号 [ ] 里面填入我们想要的元素的个数即可。不再需要我们自己亲手计算。在申请完空间之后我们不再需要对其进行判空操作,直接使用即可。因为我们的new操作符在申请失败的时候会抛出一个异常我们以后可以通过捕获进行获得异常的报错。(捕获等后面我们再详细讲解)这也就避免了我们重复判断的麻烦。使用的方式和正常的相同。
在使用完毕的时候我们使用delete将我们开辟的空间释放即可。对于单个空间我们只需要使用delete加上指针即可,对于连续的空间我们使用delete [ ] 指针进行释放。如上程序运行的结果如下:
但是我们在上面说到过new和malloc的区别之一就是可以对申请的空间进行任意的赋值,这和我们的malloc甚至是calloc函数都有很大的区别。同样的,我们通过一段代码进行详细的了解:
#include<iostream>
int main()
{
//尝试使用new开辟空间并初始化
//开辟单个字符的空间并初始化
int* pret = new int(3);
std::cout << *pret << std::endl;
delete pret;
//开辟一个数组的空间并对其进行初始化
int* ptmp = new int[2]{3,4};
std::cout << ptmp[0] << " " << ptmp[1] << std::endl;
delete[] ptmp;
//开辟一个数组的空间,使用默认的初始化方式进行试验
int* pres = new int[3]();
std::cout << pres[0] << " " << pres[1] << " " << pres[2] << std::endl;
delete[] pres;
return 0;
}
当我们想要对单个字符进行初始化的时候我们只需要在类型的名称后面加上一个小括号,里面输入我们想要初始化的值即可。但是对于一个数组来说我们所需要进行的操作就发生了些许的改变:我们需要分为很多种情况:1.具有构造函数的情况 2.没有构造函数的情况 我们来一一对其进行解释。
1.如果没有构造函数就会调用系统的默认初始化的方式,后面可以加上括号但是什么值都不能有,系统会将数组的值默认初始化为0。如果想要对没有构造函数的数组进行初始化我们还可以通过大括号 { } 的当时一一对数组的元素进行赋值,但是并不能统一进行赋值。如上代码运行的结果:
2.对具有默认构造的对象进行初始化,我们对于具有默认构造的函数进行初始化的情况,大多数都是针对于类来说的,我们通常会在类当中写一个构造函数,之后我们在使用new操作符开辟空间的时候就会自动调用类的构造函数进行对象的初始化。同样的,我们通过一段代码来学习具有构造函数的对象如何进行初始化:
#include<iostream>
//写一个类,在使用new开辟空间的时候验证默认会自动调用默认初始化
class Date
{
private:
int _year;
int _month;
int _day;
public:
//写一个构造函数,用于验证初始化
Date(int year = 2022, int month = 12, int day = 12)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
std::cout << _year << "年" << _month << "月" << _day <<"日"<< std::endl;
}
};
int main()
{
//使用new开辟一个类大小的空间,并验证其初始化的内容
Date* pret = new Date;
(*pret).print();
//开辟多个类的空间,并验证其初始化的内容
Date* ptmp = new Date[3];
int i = 0;
for (i = 0; i < 3; i++)
{
ptmp[i].print();
}
delete pret;
delete[] ptmp;
return 0;
}
经过上面的代码我们可以看到,在使用new进行默认初始化的时候,当存在默认构造函数的时候就会自动调用默认构造函数进行初始化,如果像针对某个对象进行特定初始化的时候我们同样可以使用 { } 进行指定的初始化。 程序运行的结果如下:
二:new和delete操作符的原理
在认识了new和delete函数的使用之后相信大家对于这两个操作符可以实现该功能的原因,所以我们就在此进行进一步的原理讲解。
就像我们上面所看到的现象所展示的那样,假如我们在申请堆区的指定的空间的时候,对于有默认构造的对象会调用指定的默认构造,对于没有默认构造的对象想要初始化要么将其初始化为0,要么就需要对其进行指定的构造。所以我们也可以联想到我们的new操作符是不是调用了我们类当中的构造函数?我们对其进行验证:
我们可以通过上面的运行结果得到答案,可以看出我们的程序是一定调用了构造函数了的。
与之相对应的我们的delete操作符在发挥作用的时候也可以验证一下是否调用了析构函数:
同样我们可以知道的是,在使用delete操作符的时候调用了我们类当中所定义的析构函数。
实质上C++当中的操作符在定义的时候其实没有那么高级,也并不是说有了new就可以完全舍弃malloc函数。在底层的封装之上其实new操作符就是使用我们的malloc函数再加上我们的构造函数所得到的。本质上我们的new操作符在发挥作用的时候会先调用malloc函数进行空间的申请,假如申请成功之后再调用我们类的构造函数,这样就形成的new操作符。delete操作符的原理其实是一样的。我们在空间释放的时候会先调用析构函数,在将我们的数据清理了之后再调用free函数将我们申请的空间释放掉。
三:定向空间申请
这其实并不是new操作符的使用方法当中最巧妙的一个,最巧妙的是:当我们需要多次向堆区申请空间的时候,每一次的申请都会造成一定程度上的浪费。所以我们可以现申请好一整块的空间,之后使用我们申请好的空间即可。这样就可以免去我们连续调用malloc函数所带来的缺陷。我们同样通过一段代码进行学习这种使用的方法:
#include<iostream>
//写一个类,在使用new开辟空间的时候验证默认会自动调用默认初始化
class Date
{
private:
int _year;
int _month;
int _day;
public:
//写一个构造函数,用于验证初始化
Date(int year = 2022, int month = 12, int day = 12)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl;
}
};
int main()
{
//使用new开辟一个类大小的空间,并验证其初始化的内容
Date* pret = new Date[3];
pret[0] = {2023,1,1};
pret[0].print();
return 0;
}
其使用的方法为:new (place_address) type或者new (place_address) type(initializer-list)
其中place_address必须是一个指针,initializer-list是类型的初始化列表。也就是说我们new后面括号里的是我们提前申请好的地址的指针,括号后面的 type 表示我们使用的地址的类型。我们还可以在后面的 { } 里面进行初始化,但是我们需要注意的是:我们进行的初始化只能是单个对象的初始化,否则就有可能产生特殊意想不到的结果。