C++系列总结——new和delete

  • Post author:
  • Post category:其他


前言



new



malloc()

有什么区别”,这是一个很常见的C++面试题。我的回答是”

new

等于

malloc()

后再选择性执行构造函数”。执行流程上是这样的,但是这样的回答是有纰漏的,比如没有考虑异常。下面就仔细聊一聊

new

,了解了

new

就了解了

delete

选择性的含义是有构造函数就会执行,没有构造函数则不会执行。

new是什么


new

是C++的一个关键字,这个关键字有两种用法

通常我们都是使用

new

表达式,当然我们也可以直接使用

new

运算符。

int* a = new int;  // new表达式
int* b = (int*)operator new( sizeof(int) );  // new运算符

new表达式

调试一下下面这段代码,看看编译器是如何对待

new

表达式的。

class A
{
public:
    A(){}
    ~A(){}
    int a;
};
int main()
{
    A* a = new A;
    return 0;
}

截取一段汇编代码

=> 0x000000000040061f <+9>:     mov    $0x4,%edi    # 0x4就是A的大小,作为参数值传递
   0x0000000000400624 <+14>:    callq  0x400510 <_Znwm@plt>    # 单步调试,就会进入operator new( unsigned long ) 函数
   0x0000000000400629 <+19>:    mov    %rax,%rbx    # rax里存储了返回的内存地址
   0x000000000040062c <+22>:    mov    %rbx,%rdi    # 将分配的内存地址作为参数
   0x000000000040062f <+25>:    callq  0x400644 <A::A()>    # 如果没有构造函数的话,这里就不会调用了

从上,我们发现当使用

new

表达式时,编译器做的工作就是计算出类型的大小,然后将该大小作为参数调用

operator new()

,最后调用构造函数在

operator new()

返回的地址上做初始化。因为分配的大小是编译器自己计算出来的,所以即使

operator new()

的返回值是void*,编译器也不会告警。

new运算符

常用的

new

运算符原型有如下几种

1. void* operator new( std::size_t count ) throw( std::bad_alloc );    // 对应着 new T
2. void* operator new( std::size_t count, const std::nothrow_t& tag) throw(); // 对应着 new(std::nothrow) T
3. void* operator new( std::size_t count, void* ptr ) throw(); // 对应着 new(ptr) T,直接在ptr所指的内存上执行T的构造函数进行初始化。

1和2的区别仅在于1会抛出异常,而2不会。

一定不要写出类似下面的代码,当分配不出内存时,其实际结果是不符合预期的。

A* a = new A;
if( NULL == a ){
    return false;
}

除去异常这块,我们能看到

new

运算符和

malloc()

几乎是一模一样的:指定分配的内存大小,返回void*类型。如果去看内部实现的话,我们更会发现

operator new()

其实就是调用了

malloc()

,毕竟没有必要重复造轮子嘛。

new[]


new[]

表达式和

new

表达式本质上没有什么差别,都是分配内存并初始化,只不过

new[]

表达式是一次性对分配多个对象的内存并初始化。

常用的

new[]

表达式,对应以下几种

new[]

运算符

1. void* operator new[]( std::size_t count ) throw( std::bad_alloc );    // 对应着 new T[N]
2. void* operator new[]( std::size_t count, const std::nothrow_t& tag) throw(); // 对应着 new(std::nothrow) T[N]
3. void* operator new[]( std::size_t count, void* ptr ) throw(); // 对应着 new(ptr) T[N],实际是直接返回ptr

调试代码可以发现

new[]

运算符实际就是调用了对应的

new

运算符。

为了讲解

new[]

表达式的一些实现,需要从

delete[]

开始说起。当我们写

delete[] a;

时,并没有像

new[]

那样指定元素个数,那么编译器如何知道需要执行几次析构函数呢?答案很简单,就是

new[]

的时候,编译器会多分配点空间用来保存元素个数。

为什么

delete[]

不指定元素个数?我想还是为了减少程序员的工作量。

让我们来看看代码

class A
{
public:
    A(){}
    ~A(){}
private:
    int a;
};
int main()
{
    A* a = new A[1];
}

从下面的汇编可以看出实际分配的大小是12个字节,a地址的前8个字节保存了元素个数

=> 0x0000000000400623 <+13>:    mov    $0xc,%edi    # count是12
   0x0000000000400628 <+18>:    callq  0x400500 <_Znam@plt>    # 调用operator new[]()
   0x000000000040062d <+23>:    mov    %rax,%rbx    #  将operator new[]()的返回值,放入rbx
   0x0000000000400630 <+26>:    movq   $0x1,(%rbx)    # 记录元素个数是1
   0x0000000000400637 <+33>:    lea    0x8(%rbx),%rax    # 往后移动8字节,开始调用构造函数


delete[]

会根据记录的元素个数执行完指定个数的析构函数,然后往前偏移8字节,调用

free()

释放内存。

我们知道

编译器是不会做多余的事

,如果没有析构函数需要执行的话,还需要多分配空间来保存元素个数么?答案是不需要!

class A
{
public:
    A(){}
private:
    int a;
};
int main()
{
    A* a = new A[1];
}

可以看到分配的大小就是4字节。

   0x0000000000400623 <+13>:    mov    $0x4,%edi                                                                     
   0x0000000000400628 <+18>:    callq  0x400500 <_Znam@plt>   # 调用operator new[]()


因此如果没有析构处理的必要的话,就不要写析构函数了,省内存。

到这里,我们很自然的就能知道为什么

new



delete

以及

new[]



delete[]

要配套使用了。

后话


new



delete

是很基础的知识了,日常只需要记住配套使用,不遗漏delete就足够了。

可以想想,下面这段代码运行时会有问题么,会破坏malloc内存管理结构、内存泄漏或者踩内存么?

A* a = new A;

a->~A();
delete[] (char*)a;

转载于:https://www.cnblogs.com/yizui/p/10601424.html