全文目录
malloc() /free()
提到 new/delete 运算符 就不得不说 malloc() /free() 函数,C++是在C的基础上扩展的,所以我们先了解malloc() /free() 函数这一对函数的作用有助于更多的了解 new/delete 运算符。
原型解析
malloc() /free() 函数是C运行时库(CRT)的内置函数,其函数原型分别为:
// 摘选自内置 corecrt_malloc.h 头文件
_Check_return_ _Ret_maybenull_ _Post_writable_byte_size_(_Size)
_ACRTIMP _CRTALLOCATOR _CRT_JIT_INTRINSIC _CRTRESTRICT _CRT_HYBRIDPATCHABLE
void* __cdecl malloc(
_In_ _CRT_GUARDOVERFLOW size_t _Size
);
// 摘选自内置 corecrt_malloc.h 头文件
_ACRTIMP _CRT_HYBRIDPATCHABLE
void __cdecl free(
_Pre_maybenull_ _Post_invalid_ void* _Block
);
其中 Check_return Ret_maybenull Post_writable_byte_size(_Size)
_ACRTIMP _CRTALLOCATOR _CRT_JIT_INTRINSIC _CRTRESTRICT _CRT_HYBRIDPATCHABLE 这一部分的内容大致意思就是指明 malloc函数必须有返回,且有内存对齐要求, 按字节分配内存空间。
这也是 malloc() 函数的构成细节:
(1) malloc() 函数会指向已分配空间的void指针,如果可用内存不足,则返回null ;
(2) malloc() 函数至少分配 Size 大小的内存空间,可能因为内存对齐会导致分配比Size大的内存空间;
(3) 如果Size为0,malloc() 函数也会在堆内存中分配零长度的项并返回指向改项的指针,即使请求的内存较小也会检查malloc() 函数的返回值;
__cdecl 是C/C++ 默认的调用规范,表明函数参数是从右往左依次压入栈。还有__stdcall、_thiscall、__fastcall,读者可自行了解函数原型中调用规范的作用。
free() 函数用来释放由 malloc() 函数分配的内存块。
简化版本
一般讨论时我们只需关注两者的简化版本,即:
void* __cdecl malloc(
size_t _Size
);
void __cdecl free(
void* _Block
);
用法举例
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>
int main()
{
char* pChar = (char*)malloc(1024);
if (nullptr == pChar)
{
std::cout << "分配失败!\n";
}
std::cout << "分配成功!\n";
free(pChar);
pChar = nullptr;
std::cout << "释放成功!\n";
}
/*
* (以下是输出部分)
* 分配成功!
* 释放成功!
*/
new/delete
通过上面知道了 malloc()/ free() 函数其实是用来 分配/释放内存块的,仅此而已。既然已经有了分配/释放内存空间的函数了,那C++ 为什么还需要重载new和delete呢?那是因为对于非内部数据类型(自定义类就是其中之一)无法满足动态对象的创建和释放,因为动态对象的创建/释放需要自动调用构造函数/析构函数来完成, 不能把这个任务强加给已有malloc()和free(),这就是为什么要重载new/delete来动态构建对象的原因。
静态/动态类型
那么问题来了,什么叫动态创建对象?还有其他方式创建对象么?聪明的你应该想到了, 对象可以动态创建,也可以静态创建。静态创建的对象具备静态类型,动态创建的对象可以是静态类型也可以是动态类型。
二者区别如下:
静态类型: 声明时采用的类型,在编译时就已经确定
动态类型: 对象的实际类型,在运行时才能确定
下面是对象的创建的简单示例:
#include <iostream>
class CExample {
public:
CExample()
{
std::cout << "构造函数\n";
}
~CExample()
{
std::cout << "析构函数\n";
}
};
int main()
{
CExample pExample1;
CExample* pExample2 = new CExample(); // 这里其实不用判空, 需要的是捕获new失败的异常
std::cout << "指针需置空!\n";
delete pExample2;
pExample2 = nullptr;
}
/*
* (以下是输出部分)
* 构造函数
* 构造函数
* 指针需置空!
* 析构函数
* 析构函数
*/
对象的静态创建是直接在栈上进行的(栈上由编译器管理和回收),动态创建是在堆上进行的(需要手动申请和释放)。
new / delete运算符原型
现在回归主线,老样子,上原型:
// 以下内容摘选自内置 vcruntime_new.h 头文件
#ifndef __NOTHROW_T_DEFINED
#define __NOTHROW_T_DEFINED
namespace std
{
struct nothrow_t {
explicit nothrow_t() = default;
};
#ifdef _CRT_ENABLE_SELECTANY_NOTHROW
extern __declspec(selectany) nothrow_t const nothrow;
#else
extern nothrow_t const nothrow;
#endif
}
#endif
_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new(
size_t _Size
);
_NODISCARD _Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new(
size_t _Size,
::std::nothrow_t const&
) noexcept;
_NODISCARD _Ret_notnull_ _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new[](
size_t _Size
);
_NODISCARD _Ret_maybenull_ _Success_(return != NULL) _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new[](
size_t _Size,
::std::nothrow_t const&
) noexcept;
void __CRTDECL operator delete(
void* _Block
) noexcept;
void __CRTDECL operator delete(
void* _Block,
::std::nothrow_t const&
) noexcept;
void __CRTDECL operator delete[](
void* _Block
) noexcept;
void __CRTDECL operator delete[](
void* _Block,
::std::nothrow_t const&
) noexcept;
void __CRTDECL operator delete(
void* _Block,
size_t _Size
) noexcept;
void __CRTDECL operator delete[](
void* _Block,
size_t _Size
) noexcept;
整理成简化版本(便于理解):
void* __cdecl operator new(
size_t _Size
);
void*__cdecl operator new(
size_t _Size,
::std::nothrow_t const&
) noexcept;
void* __cdecl operator new[](
size_t _Size
);
void* __cdecl operator new[](
size_t _Size,
::std::nothrow_t const&
) noexcept;
void __cdecl operator delete(
void* _Block
) noexcept;
void __cdecl operator delete(
void* _Block,
::std::nothrow_t const&
) noexcept;
void __cdecl operator delete[](
void* _Block
) noexcept;
void __cdecl operator delete[](
void* _Block,
::std::nothrow_t const&
) noexcept;
void __cdecl operator delete(
void* _Block,
size_t _Size
) noexcept;
void __cdecl operator delete[](
void* _Block,
size_t _Size
) noexcept;
由原型我们可以得知有针对于对象数组的构建和释放的重载,即new[] / delete[]。
所有new运算符重载返回值都是void指针类型,且new 也是按字节来分配内存,同样也存在内存对齐,这一点跟malloc() 函数如出一辙。 不论自定义的还是这种已经预定义的全局new的第一个参数都是自变量size_t类型,这个参数无需程序员手动传入。同样得由原型可知,不论自定义的还是这种已经预定义的全局delete第一个参数都是自变量void指针类型,这个指针就是用来指向解除分配的对象。
如果请求的存储空间为零字节,operator new 将返回指向不同对象的指针。也就是说,重复调用 operator new 会返回不同的指针。这也是为什么每new一次就会产生一个新的对象的原因。 执行new运算符未成功则会返回零或者异常,取决于选择什么版本。如果传递了第二个参数 ::std::nothrow_t const&, 则并不会产生异常, 因为有后置noexcept 的存在,决定了不会抛出异常。
常用但没有注意区分的例子
举个例子,我们平常使用的 A a = new A(), 使用new这个失败会抛出异常, 而我们常常受到老旧思想的缘故,还会立即写出是否判断为空的代码, if(nullptr == a) …,这其实是没必要的,因为如果一旦new失败了,delete就会执行,delete释放new失败的内存空间并没有什么问题. 但如果是A a = new(std::nothrow) A(); 就需要判空操作了,因为这个形式的new并不会抛出异常。如下所示:
#include <iostream>
class CExample {
public:
CExample()
{
std::cout << "构造函数\n";
}
~CExample()
{
std::cout << "析构函数\n";
}
};
int main()
{
CExample* pExample1 = new(std::nothrow) CExample();
if (nullptr != pExample1)
{
std::cout << "这里需要判读指针是否为空,因为未抛出异常!\n";
}
delete pExample1;
pExample1 = nullptr;
// 异常捕获
try
{
CExample* pExample2 = new CExample(); // 这里其实不用判空, 需要的是捕获new失败的异常
delete pExample2;
pExample2 = nullptr;
}
catch (std::bad_alloc& except)
{
std::cout << except.what();
}
}
/*
* (以下是输出部分)
* 构造函数
* 这里需要判读指针是否为空,因为未抛出异常!
* 构造函数
* 析构函数
* 析构函数
*/
使用new分配对象的生存期
很多时候我们因为偷懒或者其他什么原因导致在函数创建对象却又没有合理的释放内存空间,导致潜在的内存泄漏而不自知。使用new分配的对象在退出定义他们的范围时并不会被销毁,因为是在堆上被分配内存的需手动释放内存块,比如说:
int main()
{
char* pAnArray = new char[20];
for (int i = 0; i < 20; ++i)
{
if (i == 0)
{
char* pAnotherArray = new char[20]; // 这块造成了内存泄漏, 申请了内存但并未释放,此处可采用智能指针来处理
}
}
delete[] AnArray;
}
这里虽然程序不会崩溃(内存泄漏 ≠ 崩溃),但是对我们写成高质量代码无益,久而久之就会习以为常,可能你今天加的班就是之前的某段时间偷懒写的代码而导致的,出来混终究还是要还的!!!
那new/delete 都做什么事呢
前面说了new 干了跟malloc() 一样的事情,简单来说就是分配了内存空间。除此之外new还干了一件事就是调用构造函数来初始化对象,并返回指向对象的指针。归纳来说,new 运算符可以分为两步走:
第一步: 分配未初始化的内存空间,若失败则会抛出异常(malloc() 做的事情)
第二步: 使用对象的构造函数初始化对象的数据成员,并返回指向对象的指针,若失败则自动调用delete释放内存
同理,delete运算符也同样分为两步走:
第一步:调用对象的析构函数(需要对象指针不能为空才会调用)
第二步:释放new分配的内存空间(free() 做的事情)
几个注意点
- new和delete 要配套使用,对于单个对象就用new/delete形式,对于对象数组就需要使用new[]/delete[]形式
- 多次delete一个非空指针会导致问题,但是多次delete一个空指针是没有问题的,因为delete会进行检查,如果指针为空则直接返回
- new、delete、malloc()、free() 不能交叉着使用, 即new/free(), malloc()/delete等
写在最后
因本人水平有限,错误之处在所难免,欢迎评论区交流指正。
参考:
MicrosoftC++文档
《高质量程序指南C++第三版.林锐》