一篇文章了解c++中的new和delete

目录
  • new expression
  • delete expression
  • new[]和new()
  • new[]和delete[]
  • new的内存分布
  • placement new
  • new失败处理
    • 捕捉异常
    • 禁用new的异常
    • new-handler
  • 重载
    • 重载全局的::operator new
    • 重载局部的Foo::operator new
    • 重载placement new
  • 总结

new expression

new一个类型,会创建一个该类型的内存,然后调用构造函数,最后返回该内存的指针

注意:该操作是原子性的。

在vc6中的实现如下

void *operator new(size_t size, const std::nothrow_t &) _THROW0()
{
    void *p
    while((p = malloc(size)) == 0)
    {
        // 如果调用malloc失败后会调用_callnewh
        // _callnewh含义是call new handler,简单说就是用户设定一个回调函数
        // 使用_set_new_handler来设置,通常是用户自己控制释放一些不用的内存
        _TRY_BEGIT
            if(_callnewh(size) == 0) break;
        _CATCH(std::bad_alloc) return (0);
        _CATCH_END
    }
    return (p);
}

delete expression

delete 一个指针,先调用析构函数,然后释放内存

在vc6中的实现如下

void *operator delete(void *p) _THROW0()
{
    free(p);
}

new[]和new()

new[]是分配指针数组,new()是分配时直接初始化,这两个很容易搞混,关键是编译都能过,一定要注意。比如:

int *p = new[3]; // 是分配三个int*指针所组成的指针数组
int *q = new(3); // 是分配一个int堆内存,并初始化为3

new[]和delete[]

Complex *pca = new Complex[3];

调用三次Complex的构造函数,分配三个Complex对象

delete[] pca;

释放内存。

如果这里的delete[]只写写成delete会怎么样?好多人一定会说:会内存泄露。

其实正确的答案是不确定,具体需要看Complex类的内部有没有堆内存

new[]后内存是怎么样的呢?看下图

关键是看图中的cookie部分,存放了一些内存相关的数据,其中最关键的是在cookie中存放了分配内存的大小

再来看一下下面的代码

string *psa = new string[3];
delete psa;

执行完该代码后内存分配如下

由于string类的内部使用动态堆内存来保存字符串,new[]分配的内存的cookie只记录了string类的信息,而类内部的动态堆内存信息由每个实例自行管理,不在new[]的cookie中。

前面说过,delete释放内存的过程是先调用析构函数,再释放内存。在本例中,如果使用delete[]来释放内存,会依次调用每个实例的析构函数,每个析构函数会自行释放自己内部的堆内存,然后在释放new的内存块。但是如果使用delete来释放内存,只会是第一个实例调用一次析构函数,另外两个实例不会调用,然后根据cookie中记录的内存大小释放有new分配的内存,另两个实例中的堆内存就泄露了。

也就是说,对于上图string的例子,如果使用delete直接释放内存,泄露的是str2和str3箭头右边的白色区域所示的内存,而pas箭头右边的绿色区域是能够正确释放的(具体是调用的str1还是str3取决于编译器的具体实现,理解意思即可)。

但是,这不意味着你可以在类内部没有堆内存的情况下就可以毫无顾忌的使用delete来释放new[],这是编码规范的问题,使用delete不一定有错,但使用delete[]则是一定没错。

new的内存分布

下图是vc6中new的内存布局

我们得到的是图中0x00441c30这一部分的指针,但实际上内存管理的是图中所有的一大块内存,其中橘黄色部分只有在debug模式下才有。由于内存管理需要是16的倍数,如果不够16的倍数,则添加一些数据凑到16的倍数,图中蓝色的pad部分就是添加的无用数据。图中61h部分就是cookie,上下部分分别为上cookie和下cookie。由于本例使用的是int类型举例,而int没有析构不析构的,所以可以直接使用delete就能完整释放整块内存。这里这么写是为了让读者加深理解,实际编码的时候要加上[],这里对比一下下图

这张图使用的类型是一个类,用new[]分配内存的时候,返回的指针和调试信息中间多出来一块内存用来记录实例的个数,就是图中的3。这中情况,如果使用delete[]来释放内存,会正确索引到实例的首地址进行释放操作,如果使用delete来释放内存,索引到的内存是记录实例个数的整型数据位置,如果从这里开始按找该类的内存结构进行析构,肯定是会出问题的,整个内存结构都乱了。

这里有个地方需要注意,这里的delete和delete[]部分看起来和new[]和delete[]小结中介绍的有些矛盾,老师是怎么讲的,由于是看的盗版网课,也没办法请教老师,具体是怎么情况我也不太清楚。猜测是因为不同编译器具体实现时,3的位置不同,有的在前面,有的在后面,关键是看具体实现,在前面的情况就是矛盾的,在后面就没事,关键是领会精神。

placement new

placement new 允许我们将对象构建于一个已经分配的内存当中

没有所谓的placement delete,因为placement根本就没有分配内存,它只是使用了一个已经分配好的内存,所以不需要配套的释放操作,具体用法如下

#include <new>

// 分配内存
char *buf = new char[sizeof(Complex) * 3];

// 在分配好的内存上构造Complex
Complex *pc = new(buf)Complex(1, 2);

// 注意这里要释放的指针
// 感觉如果直接释放pc应该也没错
// 手上没环境不能测试,以后有时间测一下
delete[] buf;

new失败处理

在纯C中使用malloc来分配内存,需要判断一下返回的指针,如果返回一个空指针,则代表内存分配失败。

到了c艹中,使用new来分配内存,则无法通过判断空指针的方法判断是否失败。因为在c艹中,如果new失败会抛出异常,代码是走不到判断空指针的语句的。new失败正确处理方法有以下几种

捕捉异常

try
{
    int* p = new int[SIZE];
    // 其它代码
} catch ( const bad_alloc& e )
{
    return -1;
}

据说古老的c++编译器new失败不会抛异常,而是和malloc一样返回空指针,因为那时候c++还没有异常机制,坊间流传,也懒得考证,了解以下即可。顺便吐槽一下,说c艹的异常是屎,这是对屎的侮辱,屎还能当肥料种地呢,c艹的异常除了捣乱没任何鸟用。

禁用new的异常

 int* p = new (std::nothrow) int; // 这样如果 new 失败了,就不会抛出异常,而是返回空指针

new-handler

文章开始介绍new源码的时候提到过,new实现的时候会调用new-handler的回调函数,在new之前设置好回调函数即可。由于此方法太过麻烦,懒得研究,具体用法读者自行查找相关资料。

重载

重载的时候,一般不重载全局的::operator new,因为全局的影响太大,一般只重载类自身的Foo::operator new。

重载一般在内存池中用的比较多,可以减少cookie

重载全局的::operator new

void *myAlloc(size_t size)
{ return malloc(size); }

void myFree(void *p)
{ free(p); }

// 下面代码实现部分不重要,关键看接口的重载
// 它们不可以被声明在一个namespace内
inline void *operator new(size_t size)
{ cout << "global new()\n"; return myAlloc(size); }

inline void *operator new[](size_t size)
{ cout << "global new[]()\n"; return myAlloc(size); }

inline void operator delete(void *p)
{ cout << "global delete()\n"; return myFree(p); }

inline void operator delete[](void *p)
{ cout << "global delete[]()\n"; return myFree(p); }

重载局部的Foo::operator new

class Foo
{
public:
    void *operator new(size_t);
    void operator delete(void*);
};

需要注意的是,重载局部的new和delete必须是static的,因为new调用时是内存对象创建过程当中,此时还没有一个完整的内存对象,无法通过对象来调用一般的函数。由于必须是static的,不管写不写static,编译器都会当成是static处理。

数组版本也是一样的,只是都加了一个[],这里就不再写一次了

重载placement new

placement new的括号中不一定非要放指针,我们可以自己来定义放任意的东西。放指针的版本是标准库中先写好给我们用的,我们也可以通过重载placement new来自定义所放的数据,比如Foo *pf = new(300, 'c')Foo;。可以重载为多种参数形式,但多个重载的参数列形式不能重复,必须满足普通函数重载的条件。其中第一个参数必须是size_t,用来传递类的大小,该参数类似于成员函数的this指针,在调用时自动传递,不需要显示传递。比如在Foo *pf = new(300, 'c')Foo;中,其声明形式为void *operator new(size_t, int, char);。如果内存不是外部申请好的,需要在placement new函数内部去申请内存。

重载new的时候应该对应重载一个相同形式的delete。但重载placement delete时需要注意,只有在placement new中产生异常,才会调用其对应的placement delete函数。c++这么设计的原因是,在调用placement new函数后,如果内存是由在placement new内申请的,在调用构造函数时如果发生了异常,可以在对应的在placement delete函数中将在placement new中申请的内存释放掉。

如果没有对应形式的delete,编译器也不会报错,编译器会认为你放弃处理该形式的new中产生的异常(个别编译器会给个警告)

class Foo
{
public:

    // 重载一个一般形式的operator new
    void *operator new(size_t);

    // 标准库中placement new的重载形式
    void *operator new(size_t, void *);

    // Foo *pf = new(300, 'c')Foo;调用形式的重载方式
    void *operator new(size_t, int, char);

    // 随便写的一种重载形式
    void *operator new(size_t, size_t, char *, int);

    // 以下是对应的delete
    void *operator delete(void *, size_t);
    void *operator delete(void *, void *);
    void *operator delete(void *, int, char);
    void *operator delete(void *, size_t, char *, int);
};

std::string中就是一个很好的placement new重载,有兴趣的朋友可以去看string的源码

c++ new与malloc的10点差别表格:

特征 new/delete malloc/free
分配内存的位置 自由存储区
内存分配失败返回值 完整类型指针 void*
内存分配失败返回值 默认抛出异常 返回NULL
分配内存的大小 由编译器根据类型计算得出 必须显式指定字节数
处理数组 有处理数组的new版本new[] 需要用户计算数组的大小后进行内存分配
已分配内存的扩充 无法直观地处理 使用realloc简单完成
是否相互调用 可以,看具体的operator new/delete实现 不可调用new
分配内存时内存不足 客户能够指定处理函数或重新制定分配器 无法通过用户代码进行处理
函数重载 允许 不允许
构造函数与析构函数 调用 不调用

malloc给你的就好像一块原始的土地,你要种什么需要自己在土地上来播种

而new帮你划好了田地的分块(数组),帮你播了种(构造函数),还提供其他的设施给你使用:

当然,malloc并不是说比不上new,它们各自有适用的地方。在C++这种偏重OOP的语言,使用new/delete自然是更合适的。

总结

到此这篇关于c++中new和delete的文章就介绍到这了,更多相关c++中new和delete内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅析c++中new和delete的用法

    new和delete运算符用于动态分配和撤销内存的运算符 new用法: 1.开辟单变量地址空间1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5 2. 开辟数组空间一维: int *a = new int[100];开辟一个大小为100的整型数组空间二维: int **a = new int[5][6]三维

  • 深入浅析C++的new和delete

    new和delete的内部实现 C++中如果要在堆内存中创建和销毁对象需要借助关键字new和delete来完成.比如下面的代码 class CA { public: CA()m_a(0){} CA(int a):m_a(a){} virtual void foo(){ cout<<m_a<<endl;} int m_a; }; void main() { CA *p1 = new CA; CA *p2 = new CA(10); CA *p3 = new CA[20]; delet

  • 详解C++中new运算符和delete运算符的使用

    C++ 支持使用 new 和 delete 运算符动态分配和释放对象.这些运算符为来自称为"自由存储"的池中的对象分配内存. new 运算符调用特殊函数 operator new,delete 运算符调用特殊函数 operator delete. 在 Visual C++ .NET 2002 中,标准 C++ 库中的 new 功能将支持 C++ 标准中指定的行为,如果内存分配失败,则会引发 std::bad_alloc 异常. 如果内存分配失败,C 运行库的 new 函数也将引发 st

  • C++表达式new与delete知识详解

    在C++中,new表达式用于动态创建对象,即在堆(自由存储区)空间上为对象分配内存,而程序员也要小心的使用这些申请来的内存空间,当不再使用时应该调用delete表达式来释放该存储空间并且将指针置零. 本文学习了如何动态创建对象,动态创建的对象与一般对象的区别,动态创建的对象的初始化以及释放动态分配的内存等知识点. C++中分配的内存大致有三类:静态存储区,栈内存和堆内存 其中,静态存储区是在程序编译阶段就已经分配好的,用于全局变量,static变量等:堆栈是比较常用的对象存储方式. new和de

  • C++ new/delete相关知识点详细解析

    每个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区(free store)或堆(heap).C语言用一堆标准库函数malloc和free在自由存储区中分配存储空间,而C++则用new和delete表达式实现相同的功能. 一.new和delete创建和释放动态数组:数组类型的变量有三个重要的限制:数组长度固定,在编译时必须知道其长度,数组只在定义它的语句内存在.动态数组:长度固定,编译时不必知道其长度,通常是运行时确定:一直存在,直到程序显示释放它.

  • C++基础入门教程(五):new和delete

    对于以前没有接触过C++,然后初次接触Cocos2d-x的朋友来说,可能对于内存管理方面会比较生疏. 也经常会因为内存问题导致各种小Bug,我也曾经写过一篇retain和release倒底怎么玩?,用来驾驭Cocos2d-x的对象引用和释放也算是足够了. 但,难道大家就不想知道retain和release背后的秘密吗?(小若:不想.)   没错,今天木头来带大家走进科学,走进世界,一起来探讨C++的new和delete.(小若:没兴趣.)   好,既然大家都等不及了,那就开始吧~ 1.动态分配内

  • C++中new和delete的介绍

    介绍 1.malloc,free和new,delete区别. a.malloc,free是C/C++的标准库函数.new,delete是c++的操作符. b.malloc申请的是内存,严格意义不是"对象",new申请的可以理解为"对象",new 时会调用构造函数,返回指向该对象的指针. c.对于class类型,必须用new/delete来创建和销毁,自动调用构造和析构函数,malloc/free无法胜任. 2.使用new遵循原则: a.用new申请的内存,必须用de

  • C++中new和delete的使用方法详解

    C++中new和delete的使用方法详解 new和delete运算符用于动态分配和撤销内存的运算符 new用法:           1.     开辟单变量地址空间 1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5           2.     开辟数组空间 一维: int *a = new in

  • c++中new和delete操作符用法

    "new"是C++的一个关键字,同时也是操作符.当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间.调用构造函数.返回正确的指针.当然,如果我们创建的是简单类型的变量,第二步就会被省略. new用法: 1. 开辟单变量地址空间 1)new int; 开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数

  • 一篇文章了解c++中的new和delete

    目录 new expression delete expression new[]和new() new[]和delete[] new的内存分布 placement new new失败处理 捕捉异常 禁用new的异常 new-handler 重载 重载全局的::operator new 重载局部的Foo::operator new 重载placement new 总结 new expression new一个类型,会创建一个该类型的内存,然后调用构造函数,最后返回该内存的指针 注意:该操作是原子性

  • 一篇文章了解Python中常见的序列化操作

    0x00 marshal marshal使用的是与Python语言相关但与机器无关的二进制来读写Python对象的.这种二进制的格式也跟Python语言的版本相关,marshal序列化的格式对不同的版本的Python是不兼容的. marshal一般用于Python内部对象的序列化. 一般地包括: 基本类型 booleans, integers,floating point numbers,complex numbers 序列集合类型 strings, bytes, bytearray, tupl

  • 一篇文章说通C#中的异步迭代器

    今天来写写C#中的异步迭代器 - 机制.概念和一些好用的特性 迭代器的概念 迭代器的概念在C#中出现的比较早,很多人可能已经比较熟悉了. 通常迭代器会用在一些特定的场景中. 举个例子:有一个foreach循环: foreach (var item in Sources) { Console.WriteLine(item); } 这个循环实现了一个简单的功能:把Sources中的每一项在控制台中打印出来. 有时候,Sources可能会是一组完全缓存的数据,例如:List<string>: IEn

  • 如何通过一篇文章了解Python中的生成器

    目录 前言 生成器也是迭代器 生成器推导式 无限生成器 生成器实际用法 1. 读取文件行 2.读取文件内容 高级生成器用法 总结 前言 生成器很容易实现,但却不容易理解.生成器也可用于创建迭代器,但生成器可以用于一次返回一个可迭代的集合中一个元素.现在来看一个例子: def yrange(n): i = 0 while i < n: yield i i += 1 每次执行 yield 语句时,函数都会生成一个新值. “生成器”这个词被混淆地用来表示生成的函数和它生成的内容. 当调用生成器函数时,

  • java识别一篇文章中某单词出现个数的方法

    本文实例讲述了java识别一篇文章中某单词出现个数的方法.分享给大家供大家参考.具体如下: 1. java代码: import java.io.DataInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.StringTokenizer; import java.util.regex.Matche

  • 一篇文章轻松搞懂Java中的自旋锁

    前言 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我们开发提供了便利. 在之前的文章<一文彻底搞懂面试中常问的各种"锁" >中介绍了Java中的各种"锁",可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙去脉,那么这篇文章就先来会一会"自旋锁". 正文 出现原因 在我们的

  • 一篇文章带你搞定SpringBoot中的热部署devtools方法

    一.前期配置 创建项目时,需要加入 DevTools 依赖 二.测试使用 (1)建立 HelloController @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello devtools"; } } 对其进行修改:然后不用重新运行,重新构建即可:只加载变化的类 三.热部署的原理 Spring Boot 中热部

  • 一篇文章带你搞定Ubuntu中打开Pycharm总是卡顿崩溃

    由于 Ubuntu 中的汉字输入实在是太不友好了,所以装了个 搜狗输入法,好不容易把 搜狗输入法装好,本以为可以开开心心的搞代码了,然而... pycharm 一打开,就崩溃,关不掉,进程杀死还是不行,只能关机重启. 本以为 pycharm 出现了问题,又重装了两遍,还是不行. 最终发现竟然是搜狗输入法以及 fcitx 输入法的锅 唉,只能老老实实的把 fctix 和搜狗输入法卸载了: (1)Ubuntu 软件里卸载 fctix,然后将键盘输入法系统改成 IBus (2)卸载搜狗输入法 先查找软

  • 一篇文章带你了解Java中ThreadPool线程池

    目录 ThreadPool 线程池的优势 线程池的特点 1 线程池的方法 (1) newFixedThreadPool (2) newSingleThreadExecutor (3) newScheduledThreadPool (4) newCachedThreadPool 2 线程池底层原理 3 线程池策略及分析 拒绝策略 如何设置maximumPoolSize大小 ThreadPool 线程池的优势 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些

  • 一篇文章学会GO语言中的变量

    目录 1.标识符 2.关键字 3.变量 3.1 Go语言中变量的声明 3.2 批量声明 3.3 变量的初始化 3.4 短变量声明 3.5匿名变量 4.常量 5.iota 总结 1.标识符 在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名,常量名,函数 .bc,_123,a1232 2.关键字 关键字是指编程语言中预先定义好的具有特殊含义的标识符,关键字和保留字都不建议用作变量名 Go语言中有25个关键字 break        default      func        

随机推荐