目录
[三、重载operator new和operator delete操作符](#三、重载operator new和operator delete操作符)
[1.重载类中的operator new和operator delete操作符](#1.重载类中的operator new和operator delete操作符)
[2.重载全局operator new和operator delete操作符](#2.重载全局operator new和operator delete操作符)
一、new和delete
一般来讲,写C++程序,多数情况下还是提倡使用new和delete,不提倡使用malloc和free(这是C编程风格中才使用的)。
1.new类对象时,括号问题
(1)如果是一个空类,加不加括号没什么区别。
(2)类中如果有成员变量,带括号这种初始化对象的方式会把一些和成员变量有关的内存内容设置为0(内存中显示的内容是0)。
(3)如果类中有构造函数,main中的这两行代码执行的结果又变得相同了,如果构造函数中没有给age初始化,那么最终age的值没有被初始化为0,而是一个随机的值。
cpp
class TestMemory {
public:
int age;
};
void main()
{
TestMemory *p = new TestMemory();
TestMemory *p1 = new TestMemory;
std::cout << p->age << std::endl; //0
std::cout << p1->age << std::endl; //-842150451
}
2.new做了什么事
new关键字主要做了两件事:①一个是调用operator new;②一个是调用类的构造函数。(调试中可以使用F11键(或选择"调试"→"逐语句"命令)跳转进operator new,发现operator new调用了malloc)new关键字的调用关系如下表示:
cpp
TestMemory *p = new TestMemory();
operator new
malloc
TestMemory::TestMemory();
3.delete做了什么事
delete关键字释放内存时的大概调用关系(注意调用顺序)如下:
cpp
delete p;
TestMemory::~TestMemory();//如果有析构函数,先调用析构函数
operator delete();
free();
4.new与malloc的区别
(1)new是关键字/操作符,而malloc是函数。
(2)new一个对象的时候,不但分配内存,而且还会调用类的构造函数(当然如果类没有构造函数,系统也没有给类生成构造函数,那没法调用构造函数了)。
(3)另外刚才也看到了,在某些情况下,"A *pa=newA();"可以把对象的某些成员变量(如m_i)设置为0,这是new的能力之一,malloc没这个能力。
(4)new最终是通过调用malloc来分配内存的。
5.delete与free的区别
delete不但释放内存,而且在释放内存之前会调用类的析构函数(当然必须要类的析构函数存在)。
二、分配及释放内存
分配内存这件事,假设分配出去的是10字节,但这绝不意味着只是简单分配出去10字节(而是比10字节多很多),而是在这10字节周围的内存中记录了很多其他内容,如记录分配出去的字节数等。(分配内存时为了记录和管理分配出去的内存,额外多分配了不少内存,造成了浪费,尤其是对于频繁分配小块的内存,浪费就显得更加严重)。
释放一块内存,影响的范围很广,虽然分配内存的时候分配出去的是10字节,但释放内存的时候影响的远远不止是10字节的内存单元,而是一大片。(free一个内存块并不是一件很简单的事,free内部有很多的处理,包括合并临近空闲内存块、登记空闲块的大小、设置空闲块首位的一些标记以方便下次分配等一系列工作。)
cpp
char* point = new char[10];
总之,编译器要有效地管理内存的分配和回收,肯定在分配一块内存之外额外要多分配出许多空间保存更多的信息。编译器最终是把它分出去的这一大块内存中间某个位置的指针返回给point,作为程序员能够使用的内存的起始地址。也就是说,程序员拿到的point的地址实际上是malloc所分配出去的地址中中间的某个地址。
三、重载operator new和operator delete操作符
1.重载类中的operator new和operator delete操作符
如果不想用自己写的operator new和operator delete成员函数了,怎样做到呢?当然不需要把类中的operator new和operator delete注释掉,只需要在使用new和delete关键字时在其之前增加"::"(两个冒号)即可。两个冒号叫作"作用域运算符",在new和delete关键字之前增加"::"的写法,表示调用全局的new和delete关键字。此时,就不会调用类中的operator new和operator delete了。
cpp
class TestOverrideOperator {
public:
static void* operator new(size_t size) //重载时第一个参数必须时size_t类型
{
std::cout << "调用重载operator new" << std::endl;
TestOverrideOperator* mem = (TestOverrideOperator*)std::malloc(size);
return mem;
}
static void operator delete(void* p) // 重载时第一个参数必须时void*类型
{
std::cout << "调用重载operator delete" << std::endl;
free(p);
}
static void* operator new[](size_t size) //重载时第一个参数必须时size_t类型
{
std::cout << "调用重载operator new[]" << std::endl;
TestOverrideOperator* mem = (TestOverrideOperator*)std::malloc(size);
return mem;
}
static void operator delete[](void* p) // 重载时第一个参数必须时void*类型
{
std::cout << "调用重载operator delete[]" << std::endl;
free(p);
}
TestOverrideOperator()
{
std::cout << "调用构造函数" << std::endl;
}
~TestOverrideOperator()
{
std::cout << "调用析构函数" << std::endl;
}
};
void TestNewAndDelete()
{
std::cout << "-------调用重载operator new和重载operator delete------" << std::endl;
TestOverrideOperator *test = new TestOverrideOperator();
delete test;
std::cout << "-------调用全局operator new和全局operator delete------" << std::endl;
TestOverrideOperator *test1 = ::new TestOverrideOperator();
::delete test1;
std::cout << "-------数组测试:调用重载operator new[]和重载operator delete[] ------" << std::endl;
TestOverrideOperator* testArray = new TestOverrideOperator[3]();
delete[] testArray;
std::cout << "------- 数组测试:调用全局operator new[]和全局operator delete[] ------" << std::endl;
TestOverrideOperator* testArray1 = ::new TestOverrideOperator[3]();
::delete[] testArray1;
}
因为new和delete本身称为关键字或者操作符,所以类中的operator new和operator delete叫作重载operator new和operator delete操作符,但是这里将重载后的operator new和operator delete称为成员函数也没问题。
(1)为数组分配内存
从上面的代码可以看出:operator new[]和operator delete[]只会被调用1次,但是类的构造函数和析构函数会被分别调用3次,这一点千万别搞错,不要误以为3个元素大小的数组new的时候就会分配3次内存,而delete也会执行3次。
将断点设置在operator new[]函数体内,调试起来,观察形参size的值,发现是7。为什么会是7呢?因为这里创建的是3个对象的数组,每个对象占1字节,3个对象正好占用3字节(如下图,地址为0x016EEB44)。另外4字节是做什么用的呢?其实是记录数组大小的,数组大小为3,所以,这4字节(一个int或者unsigned int类型数据的大小)里面记录的内容就是3(如下图,地址为0x016EEB40),可以想象,释放数组内存的时候必然会用到这个数字(3),通过这个数字才知道new和delete时数组的大小是多少,从而知道调用多少次类的构造函数和析构函数。
2.重载全局operator new和operator delete操作符
也可以重载全局的operator new、operator delete以及operator new[]、operator delete[],当然,在重载这些全局函数的时候,一定要放在全局空间里,不要放在自定义的命名空间里。
虽然可以重载全局的operator new、operator delete、operator new[]、operator delete[],但很少有人这样做,因为这种重载影响面太广。读者知道有这样一回事就行了。一般都是重载某个类中的operator new、operator delete,这样影响面比较小(只限制在某个类内),也更实用。
类中的重载会覆盖掉全局的重载。
四、内存池
使用malloc这种分配方式来分配内存会产生比较大的内存浪费,尤其是频繁分配小块内存时,浪费更加明显。所以一个叫作"内存池"的词汇就应运而生。
1.内存池要解决的主要问题是什么?
- 减少malloc调用次数,这意味着减少对内存的浪费。
- 减少对malloc的调用次数后,能不能提高程序的一些运行效率或者说是运行速度呢?从某种程度上来说,能,但是效率提升并不太多,因为malloc的执行速度其实是极快的。
减少内存浪费是根本,提高程序运行效率是顺带(不是最主要的)的。
2.内存池的实现原理是什么呢?
就是用malloc申请一大块内存,分配内存的时候,就从这一大块内存中一点点分配给程序员,当一大块内存差不多用完的时候,再申请一大块内存,然后再一点一点地分配给程序员使用。
五、定位new
除了传统new之外,还有一种new叫作"定位new",翻译成英文就是placement new,因为它的用法比较独特,所以并没有对应的placement delete的说法。
定位new的功能是:在已经分配的原始内存中初始化一个对象。
- 已经分配,意味着定位new并不分配内存,也就是使用定位new之前内存必须先分配好。
- 初始化一个对象,也就是初始化这个对象的内存,可以理解成其实就是调用对象的构造函数。
总而言之,定位new就是能够在一个预先分配好的内存地址中构造一个对象。 定位new的格式如下:
cpp
new(分配好的内存首地址) 类类型(参数)
cpp
void TestNew()
{
student *s = new student();
int *p= new(s) int(10); //new(分配好的内存首地址) 类类型(参数)
std::cout << *p << std::endl;
}