内存机制.md 8.2 KB

内存管理

Primitives 基础写法

new / delete

#include <iostream>
#include <complex>

using namespace std;

int main() {

	void* p1 = malloc(521);
	free(p1);

	complex<int>* p2 = new complex<int>;
	delete p2;

	void* p3 = ::operator new(512);
	::operator delete(p3);

#ifdef _MSC_VER
	// 以下两个函数都是non-static,一定要通过object调用,以下分配3个int空间
	int* p4 = allocator<int>().allocate(3, (int*)0);		
    // 上面的 (int*)0 暂时没有用
    // 通过 allocator<int>() 创建要给临时对象,执行临死对象的allocate方法
	allocator<int>().deallocate(p4, 3);						
    // 通过 allocator<int>() 创建要给临时对象,执行临死对象的deallocate方法
#endif // _MSC_VER
#ifdef __BORLANDC__
	// 以下两个函数都是non-static,一定要通过object调用,以下分配5个int空间
	int* p4 = allocator<int>().allocator(5);
	allocator<int>().deallocate(p4, 5);
#endif // __BORLANDC__
#ifdef __GNUC__
	// 以下两个函数都是static,可通过全名调用,一下分配512bytes
	void* p4 = alloc::allocate(512);
	alloc::deallocate(p4, 512);
#endif // __GNUC__


	return 0;
}

上述代码为四种内存分配的基本用法


class Complex {
public:
	Complex(int x, int y) {
		m_x = x;
		m_y = y;
		std::cout << "constructor Complex" << std::endl;
	}
	Complex() {
		m_x = 0;
		m_y = 0;
		std::cout << "constructor Complex" << std::endl;
	}

private:
	int m_x;
	int m_y;
};

void func_new() {
	Complex* p1 = new Complex(1, 2);
	delete p1;

    // 等价于
	Complex* p2;
	try
	{
		void* mem = operator new(sizeof(Complex));
		p2 = static_cast<Complex*>(mem);
		p2->Complex::Complex(1, 2);
		operator delete(p2);
        // 这里直接调用构造函数的动作 只有编译器才可以这么写 自己写不同平台可能报错
	}
	catch (std::bad_alloc)
	{
		
	}
}

tip: 上述代码中说明了不可直接pc->Complex::Complex(1,2)方法,如果想直接调用构造函数可以运用placement new,写法是new(p)Complex(1,2)


// gcc 中 operator new的实现方式
void *operator new(std::size_t size) _THROW_BAD_ALLOC
{
    if (size == 0)
        size = 1;
    void* p;
    while ((p = ::malloc(size)) == nullptr)
    {
        // If malloc fails and there is a new_handler,
        // call it to try free up memory.
        std::new_handler nh = std::get_new_handler();
        if (nh)
            nh();
        else
#ifndef _LIBCPP_NO_EXCEPTIONS
            throw std::bad_alloc();
#else
            break;
#endif
    }
    return p;
}

从上面的代码可以看到,new的实现方式就是调用了operator new方法,在强转之后执行了对应的构造函数

operator new方法中,真正分配内存的是malloc方法

网上看到的资料


Complex* pc = new Complex(1, 2);
// ... some operation
delete pc;

// 编译器将delete转换成下面操作
pc->~Complex();         // 先析构
operator delete(pc);    // 然后释放内存

上述为delete的操作,这里直接调用析构函数不会报错

void operator delete(void* ptr) noexcept
{
    ::free(ptr);
}

operator delete底层通过free释放内存


上述代码中pc->Complex::Complex(1, 2)直接执行构造函数没有报错,并不能表示我们可以都这么直接调用构造,下面代码就是一个反例

string* pstr = new string;
pstr->string::string();
pstr->~string();
delete pstr;

这里的代码会明显报错class std::basic_string<char> has no member named string
这里报错的原因是string本来的名字其实叫basic_string,是被typedefstring,所以string的构造函数应该是basic_string()才对

除了typedef导致构造函数名字不对的问题之外,编译器的严格程度不同也会出现报错,即存在平台差异性,所以不推荐手动调用构造函数

array new / array delete

Complex* pca = new Complex[3];    // 唤起三次构造函数
// ... some operation
delete[] pca;                     // 唤起三次析构

如果写的是delete而不是delete[],那么编译器只会执行一次析构函数,而不是三次,也就是说有两个对象的析构函数没有执行,如果对象中存在new操作,这样会导致无法执行delete进而导致内存泄漏

从上图中可以看到,new出的内存块除了包含三个Complex对象外,还包含一些cookie信息,这些信息帮助操作系统释放整块内存,而deletedelete[]区别仅仅在于数组中所有对象是否都执行析构

class A{
public:
    int id;

    A(): id(0) { std::cout << "default " << this << " " << this->id << std::endl; };
    A(int i): id(i) { std::cout << this << " "  << "int cons" << i << std::endl; }
    ~A() { std::cout << " ~ " << this << " "  << this->id << std::endl; }
};

void test_placement_new() {

	A* buf = new A[3];              // 调用A的默认构造函数,创建对象
	A* tmp = buf;

	for (int i = 0; i < 3; i++) {
		new(tmp++)A(i);             // placement new的写法,在tmp地址的构建A对象,调用A的构造函数
	}

	delete[] buf;
}

default 00A64CF4 0
default 00A64CF8 0
default 00A64CFC 0
00A64CF4 int cons0
00A64CF8 int cons1
00A64CFC int cons2
~ 00A64CFC 2
~ 00A64CF8 1
~ 00A64CF4 0

通过查看输出,可以发现这上述代码的执行顺序

placement new

  • placement new 允许哦我们将object建构于allocated memory(已经分配的内存)中
  • 没有placement delete,因为placement new根本没有分配内存

因为是构建于已经分配的内存中,所以需要一个现成的被分配的内存空间的指针

#include <new>

void func(){
    char* buf = new char[sizeof(Complex) * 3];
    Complex* pc = new(buf)Complex(1, 2);
    // ....
    delete[] buf;
}
void func_1(){
    Complex* pc = new(buf)Complex(1, 2);
    // 上面一行代码 等价下面的代码  

    Complex* pc;
    try{
        void* mem = operator new(sizeof(Complex), buf);
        pc = static_cast<Complex*>(mem);
        pc->Complex::Complex(1, 2);
    }
    catch(std::bad_alloc){

    }
}

// 下面为当 operator new的两个参数为size_t和void*的源码
void* operator new(size_t, void* loc){
    return loc;
}

分析上述代码可以发现

  1. operator new(size_t, void* loc)什么都没做,因为此时loc是已经分配好了的内存地址,所以不需要再分配内存
  2. placement new其实就是执行了一次构造函数

C++分配内存的途径

一般来说,我们会重载类的Foo::operator newFoo::operator delete,将对象的创建和内存分配自己来控制(通过自己控制可以减少内存块上的cookie的使用)

Foo*p = (Foo*)malloc(sizeof(Foo));
new(p)Foo(x);
// ...
p->~Foo();
free(p);

当然也可以重载全局::operator new / ::operator delete

// 测试重写代码

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

void myFree(void* ptr){
    return free(ptr);
}

inline void* operator new(size_t size){
    cout << "my new" << std::endl;
    return myAlloc(size);
}

inline void* operator new[](size_t size){
    cout << "my new[]" << std::endl;
    return myAlloc(size);
}

inline void operator delete(void* ptr){
    cout << "my delete" << std::endl;
    myFree(ptr);
}

inline void operator delete[](void* ptr){
    cout << "my delete[]" << std::endl;
    myFree(ptr);
}

更常见的写法是类中重载

class Foo{
public:
    void* operator new(size_t size);
    void operator delete(void*[,size_t]); // 第二个参数size_t可有可无,写不写都行
    void* operator new[](size_t size);
    void operator delete[](void*[, size_t]);
}
分配 释放 类型 可否重载
malloc() free() C函数 不可
new delete C++表达式 不可
::operator new() ::operator delete() C++函数
allocator::allocate() allocator::deallocate() C++标准库 可自由设计并与之搭配任何容器