阅读 156

[C++从进阶到入门] —— operator/placement new

malloc、free

C语言中,我们使用malloc分配内存,使用free释放内存。下面代码我们申请了10个字节的空间,但实际上我们获得的空间会大于10个字节。

char* s = (char*)malloc(sizeof(char) * 10);
free(s);
s = NULL;
复制代码

下面的结构体用来存储申请的内存块信息。

struct mem_control_block {
	int is_available;
    int size;
}
复制代码

free根据size来释放空间,标记is_available为可用。

void free(void* p) {
	struct mem_control_block* mcbp;
    mcbp = p - sizeof(mem_control_block);
    mcbp -> is_available = 1;
}
复制代码

free函数对这一点的实现没有统一的标准,在不同的编译程序中,甚至同一编译程序的不同版本中,其实现都有可能不同。它可能会使用一张记录所分配内存地址和长度的表,或将有关数据存放在内存块,或用一个指针存储相关管理信息等等。 —— 《UNIX环境高级编程》

new、delete

C++语言中,我们使用new/new[]分配内存,使用delete/delete[]释放内存。

char* s = new char[10];
delete[] s;
s = nullptr;

int* i = new int(0);
delete i;
i = nullptr;
复制代码

对于内置对象而言,new操作符会调用默认构造函数,对于自定义对象而言,new操作符会调用我们编写的构造函数,相比于malloc,new操作符多了初始化的操作。相应的delete操作符除了释放空间外,也会调用析构函数。

new操作符都干了什么事情?

string* sp = new string("hello world");

void* p = operator new(sizeof(string));		// 内部调用malloc分配内存
new(p) string("hello world");				// 在p上调用构造函数
string* sp = static_cast<string*>(p);		// void* to string*
复制代码

operator new不是操作符而是函数,内部也是使用malloc来分配内存,如果类中没有重载operator new函数,new操作符会缺省调用::operator new函数,这个全局operator new函数也可以被重载,这样影响的就是所有对象的内存分配。

void* operator new(size_t sz) { // new
    void *p = malloc(sz);
    return p;
}

void* operator new[](size_t sz) { // new[]
    void *p = malloc(sz);
    return p;
}
复制代码

operator new函数的第一个参数必须是size_t,可以自定义后续参数,size_t由new操作符计算并自动传入。例如new int()调用operator new中sz的值为sizeof(int)

但是new int[10]调用operator new[]中sz的值并不是sizeof(int)*10,会多分配4个字节用于存储长度。new[]操作符会根据这4个字节构造所有对象。

new操作符是先调用operator new分配内存,再调用构造函数;delete操作符则是先调用析构函数,再调用operator delete释放内存。

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

void operator delete[](void* p) {
	free(p);
}
复制代码

delete[]操作符会根据多出来个4个字节来去析构所有的对象。

placement new

我们每次使用new去创建对象时,会自动寻找一块空闲内存,那么我们能不能只调用构造函数,不申请内存呢?这就需要placement new,在已经存在的合法空间上去构造对象。

需要注意的是,placement new并不是指有一个名为placement new的函数。

int* p = (int*)malloc(sizeof(int));
new(p) int(0);
复制代码

上面的代码就是placement new,先创建一块内存,然后在这块内存上去调用构造函数。之所以可以new(p)这样使用的原因是因为重载了operator new函数。

void* operator new(size_t, void* p) {
	return p;
}

void* operator new[](size_t, void* p) {
	return p;
}
复制代码

上面的代码中,已经不需要size_t参数了,所以只做占位用。我们也可以继续添加其他参数,来实现更多的功能。

void* operator new(size_t, void* p, int i) {
	return p + i;
}
复制代码

于是我们可以像这样使用new操作符。

T* memo = (T*)malloc(sizeof(T)*10);
new(memo, 3) T();
复制代码

上面的代码我们在长度为10的T数组中的第三个位置,构造了一个T对象。placement new使我们可以在一片空间上反复构造新的对象,省去了申请空间所需的时间,大大提高了程序的运行效率。