18.1 C++内存高级话题-new、delete的进一步认识
18.2 C++内存高级话题-new内存分配细节探秘与重载类内operator new、delete
18.3 C++内存高级话题-内存池概念、代码实现和详细分析
18.4 C++内存高级话题-嵌入式指针概念及范例、内存池改进版
18.5 C++内存高级话题-重载全局new/delete、定位new及重载
1.new、delete的进一步认识
1.1 总述与回顾
如果不涉及高级用法,只是从简单应用的层面来讲,学会new、delete的基本使用已经够应付日常的开发工作了。也就是说,常规情况下,对内存的使用、管理上,可能不需要做太多。
但是,本章作为内存高级话题,笔者希望在原有基础之上讲解一些新内容,让读者对内存有一个更深入的了解,因为这些高级内存知识的储备,对理解其他的C++话题特别有帮助,如理解模板中的内存分配等。同时,学习到这个阶段的时候,后续可能要开始实战了,实战中不可避免地会看到很多内存有关的高级用法,接触到一些项目中常用的概念,如内存池等。这也是笔者讲解本章的主要目的。
1.2 从new说起
(1)new类对象时加与不加括号的差别
class A
{
public:
}
int main()
{
A* pa = new A();
A* pa2 = new A;
}
可能不少人会疑惑这两行代码的区别,分几个方面来说:
(1)如果是一个空类,这两行代码没什么区别。当然现实中也没有程序员会写一个空类。
(2)类A中如果有成员变量,先给类A增加一个public修饰的成员变量:
public:
int m_i;
通过设置断点观察main主函数中的这两行代码执行后的结果可以得到如下结论,注意看注释:
A* pa = new A(); //带括号的写法,m_i成员变量被初始化为0
A* pa2 = new A; //这种写法,m_i成员变量中是随机值
这说明带括号这种初始化对象的方式会把一些和成员变量有关的内存设置为0(内存中显示的内容是0)。
(3)如果类A中有构造函数,增加如下用public修饰的构造函数:
public:
A()
{
}
通过设置断点观察main主函数中的这两行代码执行后的结果会发现,main中的这两行代码执行的结果又变得相同了,“A *pa=newA();”这种写法,成员变量m_i内存也不设置为0了,想必是成员变量(如m_i)的初始化工作要转交给类A的构造函数做了(而不再是系统内部做),而这个构造函数的函数体为空(什么也没做),所以,m_i的值没有被初始化为0,而是一个随机的值。
(4)感觉不同。
可以看出来,“newA();”的写法类似于函数调用,感觉像调用了一个无参的构造函数——类名,后面跟一对小括号。
而单纯的“newA;”这种写法,当然不像函数调用,但实际上它也是调用类A的默认构造函数的。
其实“newA();”和“newA;”这两种写法没有什么太大的区别。
那么,如下这三行代码有什么差别吗?
int* p1 = new int;
int* p2 = new int();
int* p3 = new int(100);
上面这三行代码new的是简单类型(int类型),所以这三行代码的差别主要还是在初值上。
第一行代码执行后,p1的初值为随机值。
第二行代码执行后,p2的初值为0。
第三行代码执行后,p3的初值为100。
(2)new做了什么事
A *pa = new A();
上面这行代码经常见,但其中的new是什么?new可以称为关键字,也可以称为操作符。在Visual Studio 中,光标定位到new关键字上,按一下F12键,会跳转到一个系统文件的某位置。在该位置处可以发现operator new字样如下(不同版本的内容看上去可能不同):
_Ret_notnull_ _Post_writable_byte_size_(_Size)
_VCRT_ALLOCATOR void* __CRTDECL operator new(
size_t _Size
);
可以在“A *pa=new A();”代码行处设置一个断点,并按F5键(或选择“调试”→“开始调试”命令)进行调试,当断点停留在该代码行时,通过选择“调试”→“窗口”→“反汇编”命令打开反汇编窗口,这样就可以看到这行代码对应的汇编语言代码是什么,如图所示。
不难发现,new关键字主要做了两件事:①一个是调用operator new;②一个是调用类A的构造函数。
调试中可以使用F11键(或选择“调试”→“逐语句”命令)跳转进operator new,发现operator new调用了malloc,如图所示。
有些读者可能会问,operator new是什么?operator new其实是一个函数。既然是一个函数,就是可以被调用的。在main主函数中增加如下一行代码:
operator new(12);
可以成功编译。如果计算机上安装了Visual Studio ,在其安装目录下的某个子目录中会存在一个叫作new_scalar.cpp的文件,operator new的实现源码就在该文件中。源码类似如下:
void* __CRTDECL operator new(size_t const size)
{
for (;;)
{
if (void* const block = malloc(size))
{
return block;
}
if (_callnewh(size) == 0)
{
if (size == SIZE_MAX)
{
__scrt_throw_std_bad_array_new_length();
}
else
{
__scrt_throw_std_bad_alloc();
}
}
// The new handler was successful; try to allocate again...
}
}
根据上面这些线索,可以写出new关键字分配内存时的大概调用关系。new关键字的调用关系如下表示:
A *pa = new A(); //操作符
operator new(); //函数
malloc(); //C风格函数分配内存
A::A(); //有构造函数就调用构造函数
有分配内存,必然有释放内存。在main主函数中,增加释放内存代码:
delete pa;
同时,给类A增加一个析构函数:
public:
~A()
{}
设置断点到delete pa;行并进行跟踪调试。辅助反汇编窗口中显示的汇编代码和F11快捷键(跟踪进函数调用内部),最终可以写出delete关键字释放内存时的大概调用关系(注意调用顺序)如下:
delete pa;
A::~A(); // 如果有析构函数,则先调用析构函数
operator delete(); //函数
free(); //C风格函数释放内存
上面的调用关系中,operator delete也是一个函数。如果计算机上安装了Visual Studio ,在其安装目录下的某个子目录中会存在一个叫作delete_scalar.cpp的文件,operator delete的实现源码就在该文件中。源码类似如下:
void __CRTDECL operator delete(void* const block) noexcept
{
#ifdef _DEBUG
_free_dbg(block, _UNKNOWN_BLOCK);
#else
free(block);
#endif
}
注意,如果将来面试C++开发的岗位,被面试官问到new与malloc的区别,必须要能够回答上:
(1)new是关键字/操作符,而malloc是函数。
(2)new一个对象的时候,不但分配内存,而且还会调用类的构造函数(当然如果类没有构造函数,系统也没有给类生成构造函数,那没法调用构造函数了)。
(3)另外刚才也看到了,在某些情况下,“A *pa=new A();”可以把对象的某些成员变量(如m_i)设置为0,这是new的能力之一,malloc没这个能力。
同理,delete与free的区别也就比较明显:delete不但释放内存,而且在释放内存之前会调用类的析构函数(当然必须要类的析构函数存在)。
(3)malloc做了什么事
上面已经看到,new最终是通过调用malloc来分配内存的,这一点务必要有清晰的认知。
那么malloc是怎样分配内存的呢?这很复杂(它内部有各种链表,链来链去,又要记录分配,当释放内存的时候如果相邻的位置有空闲的内存块,又要把空闲内存块与释放的内存块进行合并等),可能不同的操作系统有不同的做法。malloc可能还需要调用与操作系统有关的更底层的函数来实现内存的分配和管理,malloc是跨平台的、通用的函数,但是malloc再往深入探究,代码就不通用了,这也不是本书要研究的范畴,大家只需要知道最终是通过malloc来分配内存就可以了。
(4)总结
如果对operator new、operator delete这种函数以及malloc、free这种C风格函数的调用有兴趣,可以自己写测试代码来研究以满足猎奇心理。其中,operator new、operator delete极少会在实际项目中用到,而malloc、free也只会在C风格的代码中才会用到。这里就不写测试代码了。
一般来讲,写C++程序,多数情况下还是提倡使用new和delete,不提倡使用malloc和free(这是C编程风格中才使用的)。