C++内存管理

说到内存管理,首先要明确内存管理的目的是什么,当然是为了节省运行时间和内存空间了。可能很多人会觉得有些过时了,他们的观点是:一方面现在的内存动辄几个G或者十几G,不在乎那一点点的内存分配的多少,大不了花钱加内存条;另一方面现在的高级语言(例如Java,C#)很多都有自动内存回收机制,不需要用户自己手动分配和释放内存,系统GC机制会自动释放内存。是的,确实现在的开发者可以把更多的精力放在处理真正的业务逻辑上,快速迭代软件开放周期,毕竟在移动互联网时代,时间就是金钱和资源。但是,现在也是大数据和人工智能时代,内存多了数据也更大了;还有,垃圾回收机制尽管可以自动回收内存,相应的,程序员也控制不了回收的时机,在一定程度上失去了对程序的可控权。如果要写出更加优秀的程序,明白内存分配的原理,做到心中自有丘壑,应用程序可以有更好的用户体验,尤其是C++开发者。

那么,C++中的内存管理需要了解那些知识呢?我觉得最起码需要了解:

  • 标准库std::allocator
  • new new[] new(), operator new() 与对应的delete 版本
  • mallocfree机制。

就像候捷老师的PPT说的内存分配的途径:

上面三个分配内存的路径是我们程序员可以控制的,也是可以直接调用的。操作系统调用的HeapAlloc是不可移植的,但是上面的三种分配内存的方式最终都是调用操作系统的方法。下面我们从最简单的new delete 开始说起内存分配的那些事儿。

new / delete 里里外外

new / delete 小试牛刀

c++ 中 new 相信大家都非常熟悉,就是在堆内存上替一个对象分配内存并返回指向该对象的指针。基本代码就是 A* p = new A(), 与new 对应的就是delete , 释放该指针指向的内存, 用法 delete p
这是大家都知道的用法,不过这里有几点值得注意:

  1. new delete 必须成对使用,不然会内存泄漏。
  2. new delete 不是函数(c语言中的malloc 与 free是函数),而是表达式(expression)。
  3. 数组内存分配用 array new,即 new[] , 对应释放用 delete[] ,也要成对使用。

new 三重门

小试牛刀是基本内容,相信大家都明白,也很好理解。但是newmalloc 到底有啥不同呢?我们知道malloc 分配的单位是字节,而且是一块没有初始化的内存。但是new 除了给对象分配内存外,还在该内存上初始化了对象,即调用对象的构造函数,然后返回指针给用户。

1. throw or nothrow operator new

假设我们有这样的代码:

A* p = new A();

编译器会把上面的语句理解为:

A* p = nullptr;
void *mem = operator new(sizeof(A)); // 1 alloc 调用对应的重载版本
p = static_cast<A*>(mem);            // 2 cast
p->A:A();                            // 3 construct

注意上面的代码 最后一行用户是不能执行的(本机测试在vs2015可以,但是gnu 5.4不行,C++标准是不允许的,所以也不建议使用),但是可以理解成编译器是可以内部调用的。所以很清楚new 表达式分成三步执行:

  1. 分配内存(placement new 除外)
  2. 转换指针类型
  3. 执行构造函数

其中第二步是简单的指针类型转换,没什么可细说的,第一步operator newnew 运算符,它和+ - 等运算符一样是可以被重载的,C++ 默认重载了三种。上面重载的版本内部默认调用了 c 语言的 malloc 分配内存。

void *mem = operator new(sizeof(A)); // 1 alloc

上面的代码的默认实现如下:

// throwing allocation
void* operator new(size_t size) throw (std::bad_alloc)
{
    void *p;
    while((p = malloc(size)) == 0)
    {
        if(new_handler_call != nullptr)
        {
            new_handler_call();
        }
        else 
        {
           throw std::bad_alloc();
        }
    }
    return p;
}

如果是抑制抛出异常的版本:

A* p = new(std::nothrow) A();

编译器会把上面的语句理解为:

A* p = nullptr;
void *mem = operator new(sizeof(A), std::nothrow)(sizeof(A)); // 1 alloc 调用对应版本
p = static_cast<A*>(mem);                          // 2 cast
p->A:A();                                          // 3 construct

其中第二句分配内存的代码:

void *mem = operator new(sizeof(A), std::nothrow)(sizeof(A)); // 1 alloc

默认实现的如下:

// nothrow allocation
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw()
{
    void *p;
    while((p = malloc(size)) == 0)
    {
        if(new_handler_call != nullptr)
        {
            new_handler_call();
        }
        else
        {
            return nullptr;
        }
    }
    return p;
}

上面的代码出现了类型std::new_handler 回调函数,其实质是一个无参返回void的函数指针类型,即 typedef void (*new_handler)() 类型,new_handler_call是其一个实例, 当内存分配失败时则会调用,用户可以通过全局函数set_new_hanlder 设置, 后面会详细说明。

2. placement new

这里我们有必要总结一下:

operator new 是可以重载的运算符。默认的重载有三种:

//throwing (1)
void* operator new (std::size_t size) throw (std::bad_alloc);
//nothrow (2)   
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw(); 
//placement (3)
void* operator new (std::size_t size, void* ptr) throw(); 

第一种 throwing 和第二种 nothrow 上面刚刚说过。第三种是placement new 它的默认实现是:

void* operator new (std::size_t size, void* ptr) throw()
{
    return ptr;
}

对!什么也没做,直接返回指针,所以如果我们 执行语句:

A a;
//... 这里改变a的值
A* p = new(&a) A(); // 

则编译器同样会把上面的代码new表达式翻译成:

A* p = nullptr;
void *mem = operator new(sizeof(A) ,&a);         // 1 直接返回指针
p = static_cast<A*>(mem);                        // 2 cast
p->A:A();                                        // 3 construct

需要注意,1. placement new的内存可以是栈空间, 2.没有分配新内存,只是在a的地址上重新构造函数
具体可访问 cplusplus

3. custom new (自定义 new)

当然我们也可以重载自己的 operator new 运算符,但是要满足条件就是: 第一个参数必须是 std::size_t 。例如,我们重载自己的 operator new 原型如下:

void* operator new(std::size_t size, int extra);

这时,再假设我们有这样的语句:

A* p = new(1) A(2);

编译器会把上面的语句理解为:

A* p = nullptr;
void *mem = operator new(sizeof(A) , 1);  // 1 alloc 调用对应的重载版本
p = static_cast<A*>(mem);                        // 2 cast
p->A:A(2);                                       // 3 construct

注意, 上面的第二句就会执行我们重载的operator new 代码来分配内存,第四句构造函数也有相应的参数了。当然我们可以重载更多的自定义operator new,例如 :

void* operator new(std::size_t size, ***);//***标识其他类型

如果要想调用自己的operator new 就需要有对应的 new(***) Type()
至此,默认的三种operator new 和 我们自己重载的版本都讲了。啰嗦了这么多,总之一句话,! new expression(new表达式)会被编译器理解成三个语句,其中第一个语句operator new 是可以被重载和重写的,调用哪个版本的operator new函数由new 表达式的形式决定。 即:

A* p = new(***) A(xxx)

被编译器翻译成:

// 第二行***与第四行xxx与new表达式对应
A* p = nullptr;
void *mem = operator new(sizeof(A) , ***);  
p = static_cast<A*>(mem);                   
p->A:A(xxx);

array new

上面给出了new 表达式的三个默认版本和一个自定义版本,以及对应的operator new 函数,可我们也知道new 也可以分配数组内存,称之为array new, 即 new[] , array new 表达式首先是分配一大块可以容纳所有数组的内存,然后在相应内存上调用每个对象的构造函数。与上面的new 对应,假设我们有下面的代码:

A* p = new A[10];

则编译器会理解为:

A* p = nullptr;
void *mem = operator new[] (sizeof(A) * 10);  
p = static_cast<A*>(mem);

A* tp = p;
for(int i = 0; i < 10; ++i)
{
     tp->A:A();
     tp = tp + 1;
}                 

如同我们刚刚所说的,首先是通过operator new 分配一大块内存,然后移动指针在内存块上初始化数组对象,注意这是编译器内部对new 表达式的实现,我们不可以重新实现,但是我们可以重载上面的第二个语句operator new[]。 和上面的new 一样,operator new[] 也有四种重载方式:

//throwing (1)
void* operator new[] (std::size_t size) throw (std::bad_alloc);
//nothrow (2)   
void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) throw(); 
//placement (3)
void* operator new[] (std::size_t size, void* ptr) throw(); 
//costom (4)
void* operator new[] (std::size_t size, user-defined-args...);

调用它们也需要new[] 表达式有相应的格式:

//throwing (1)
A* p = new A[10];
//nothrow (2)   
A* p = new(nothrow) A[10]; 
//placement (3)
A* p = new(&a) A[10]; 
//costom (4)
void* operator(2) new[10];

符合的规则和new 一样:

A* p = new(***) A[n]

调用对应的operator new[] :

void* operator new[] (std::size_t size, ***);

但是有一个不同之处是,new[] 只能对每个对象执行默认构造函数。例如,new A[10](2) 是错误的。

delete / delete[]

前面我们已经知道,new expression 执行三个步骤 ( A* p = new A() ):

  1. 分配内存(placement new 除外)
  2. 转换指针类型
  3. 执行构造函数

编译器理解为:

A* p = nullptr;
void *mem = operator new(sizeof(A)); // 1 alloc 调用对应的重载版本
p = static_cast<A*>(mem);            // 2 cast
p->A:A();                            // 3 construct

同样,delete expression 执行二个步骤 ( delete p ):

  1. 执行析构函数
  2. 回收内存(placement new 除外)

可以看到,delete 表达式执行的操作和 new 表达式顺序刚好相反,即先调用析构函数,后释放内存。同样delete p 可以被编译器理解为:

p->~A();
operator delete(p);                    

其中,第二行的operator delete 负责释放内存,它是可以被重载的,同operator new 一样,有四种重载方式:

//ordinary (1)
void operator delete (void* ptr) throw ();
//nothrow (2)   
void operator delete (void* ptr, const std::nothrow_t& nothrow_value) throw();
//placement (3)
void operator delete (void* ptr, void* place) throw(); 
//costom (4)
void operator delete (void* ptr, user-defined-args...) throw();

但是怎么指定调用哪一个重载函数呢?这里不能像new一样根据new expression 原型来选择相应的版本,因为delete的形式是固定的,只能是delete p ,所以当我们执行 delete p 时默认的释放内存的执行语句总是 void operator delete (void* ptr) throw();,那什么时候调用下面的三种重载方法呢? 答案是,当用 new(xxx) A() 表达式创建对象时,如果构造函数抛出异常,此时对象的内存已经分配,所以会调用与new相匹配的operator delete释放此对象内存。如下面代码:

#include <stdexcept>
#include <iostream>
struct X {
    X() { throw std::runtime_error(""); }

    // custom placement new
    static void* operator new(std::size_t sz, bool b) {
        std::cout << "custom placement new called, b = " << b << '\n';
        return ::operator new(sz);
    }
    // custom placement delete
    static void operator delete(void* ptr, bool b)
    {
        std::cout << "custom placement delete called, b = " << b << '\n';
        ::operator delete(ptr);
    }
    // ordinary delete
    static void operator delete(void* ptr)
    {
        std::cout << "ordinary delete called" << '\n';
        ::operator delete(ptr);
    }
};
int main() {
   try {
     X* p1 = new (true) X;
     //delete p1;
   } catch(const std::exception&) { }
}

因为 new (true) X 在构造函数中抛出异常,所以要调用相应的 operator delete(void* ptr, bool b); ,但是若在构造函数中除去异常,并手动delete p1 则会调用 operator delete(void* ptr); 具体说明大家可以参考cppreference 最后一个代码示例。c++这样设计的初衷就是为了对象构造失败(异常)的时候也能正确释放内存,具体说明可以查看《effective c++》: Item52 “写了placement new也要写placement delete”。下面我们再来看看operator delete[], 即 delete array

//ordinary (1)
void operator delete[] (void* ptr) throw();
//nothrow (2)   
void operator delete[] (void* ptr, const std::nothrow_t& nothrow_value) throw();
//placement (3)
void operator delete[] (void* ptr, void* place) throw(); 
//costom (4)
void operator delete[] (void* ptr, user-defined-args...) throw();

array new 一样,也有四个重载形式,前三个是默认实现了的,值得注意的是,所以的形式都是不能抛出异常的,即释放内存是不要抛出异常,前面的operator new[] 也是一样。同样delete[] p 也可以被编译器理解为:

A* tp = p;
for(int i = 0; i < 10; ++i)
{
     tp->~A();
     tp = tp + 1;
} 
operator delete[](p);                

可以看出,delete[] p 先多次调用析构函数,然后一次释放内存。这里大家可能比较疑惑的是,传入一个指针p是怎么知道调用多少次析构函数的?如果用delete p而不是 delete[] p 释放array new 一定会内存泄漏吗? 这就涉及到下一个话题mallocfree 。请听下回分解!

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值