首页 > 编程语言 >C++内置 new /delete 运算符浅析

C++内置 new /delete 运算符浅析

时间:2024-03-21 10:02:49浏览次数:42  
标签:__ malloc void C++ 运算符 new Size 浅析 delete

全文目录

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() 做的事情)

几个注意点

  1. new和delete 要配套使用,对于单个对象就用new/delete形式,对于对象数组就需要使用new[]/delete[]形式
  2. 多次delete一个非空指针会导致问题,但是多次delete一个空指针是没有问题的,因为delete会进行检查,如果指针为空则直接返回
  3. new、delete、malloc()、free() 不能交叉着使用, 即new/free(), malloc()/delete等

写在最后

因本人水平有限,错误之处在所难免,欢迎评论区交流指正。

参考:
MicrosoftC++文档
《高质量程序指南C++第三版.林锐》

标签:__,malloc,void,C++,运算符,new,Size,浅析,delete
From: https://blog.csdn.net/weixin_41403580/article/details/136653025

相关文章

  • C++模板实现之谜:为何只能在头文件中?解密原因与高级分离技术
     概述:C++中模板必须在头文件中实现,因为编译器需要可见的实现以生成模板具体实例的代码。通过头文件,确保模板在每个编译单元中都能被正确展开,提高可维护性。在C++中,模板只能在头文件中实现的主要原因是编译器在使用模板时需要生成对应的代码,而这部分代码必须在编译时可见。以......
  • python中出现Microsoft Visual C++ 14.0 or greater is required
    我尝试下载了Microsoftvisualc++14.0,但是依然不管用,而且它是真的很大…… 直接安装相应依赖也不管用(可能其他人管用?)——condainstalllibpythonm2w64-toolchain-cmsys2链接:https://blog.csdn.net/qzzzxiaosheng/article/details/125119006 然后我有找到一个,看着描......
  • C++ 函数模板
    C++函数模板函数模板在C++中,函数模板是一种允许函数以一种类型无关的方式来操作的工具。它们使得函数能够处理不同类型的数据而不需要为每种类型编写重复的代码。函数模板的核心思想是“参数化类型”,这意味着在定义函数时,可以使用一个或多个通用类型参数,而在函数被调用时......
  • C++ 编程入门指南:深入了解 C++ 语言及其应用领域
    C++简介什么是C++?C++是一种跨平台的编程语言,可用于创建高性能应用程序。C++是由BjarneStroustrup开发的,作为C语言的扩展。C++为程序员提供了对系统资源和内存的高级控制。该语言在2011年、2014年、2017年和2020年进行了4次重大更新,分别为C++11、C++14、C+......
  • C++ 多重继承下的内存布局
    1.多重继承多重继承示例代码如下:classBase1{public:voidf0(){}virtualvoidf1(){}inta;};classBase2{public:virtualvoidf2(){}intb;};classDerived:publicBase1,publicBase2{public:voidd(){}voidf2(){}......
  • C++ <atomic>汇编语言实现原理
    C++<atomic>汇编语言实现原理问题我们先看一下这段代码:/**badcnt.c-Animproperlysynchronizedcounterprogram*//*$beginbadcnt*//*WARNING:Thiscodeisbuggy!*/#include"csapp.h"void*thread(void*vargp);/*Threadroutineprototype*//*......
  • C++ 模板入门详解
    目录0.模板引入1.函数模板 1.函数重载的缺点 2.函数模板的概念和格式2. 函数模板的实例化 2.1 隐式实例化:让编译器根据实参推演模板参数的实际类型 2.2 显式实例化:在函数名后的<>中指定模板参数的实际类型2.3函数模板参数的匹配规则 3.类模板 3.1类......
  • C++ 静态变量的初始化线程安全问题
    1.静态变量的初始化线程安全问题C++的局部static变量,是预先在静态存储区分配了内存,然后在第一次执行到这里的时候进行初始化。C++11规定了局部static变量的线程安全,实现上应该是类似std::call_once的实现,我估计基本上就是基于cas的spin-lock,这里当然可以根据编译器不同有不同......
  • 复试C++15真题_程序设计2_递归_输入字符串倒序转整形
    编写一个递归函数,功能为:输入一个字符串,输出一个整数值。例如输入 "1a2xcz34,5a!6" , 输出654321。一开始想不明白怎么写递归,于是我写了迭代的函数。意识到,递归的过程就是实现了迭代的循环,而循环内的操作本质没有太大差别。于是就写出来了:#include<iostream>usingnam......
  • C++ 引用底层解析
    1.引用的底层原理解析引用被称为变量的别名,它不能脱离被引用对象独立存在,这是在高级语言层面的概念和理解,并未揭示引用的实现方式。常见错误说法是“引用“自身不是一个变量,甚至编译器可以不为引用分配空间。引用地址空间存放的是被引用对象的地址。实际上,引用本身是一个变量,......