所谓的 new 表达式,就是我们做常用的用于分配动态对象的表达式,如下操作就是运用new表达式进行动态对象的分配。
int* p = new int(1024);
string* p1 = new string("一条舔狗!");
string *p2 = new string[10];//将会待用3次,string的默认构造函数
采用new 表达式分配的对象将会分配在堆上,记得对它进行delete。
delete p;
delete p1;
delete [] p2;
如果分配的是数组则应该用 delete [ ] 。
operator new / delete 负责向系统申请内存,一般由operator new / delete 申请的内存为原始内存,在该内存上并没有特定的对象,因此,对于用该运算符申请得到的内存我们需要手动为其构造对象,并手动的调用对象的析构函数。
operator new / delete 可以重载,通过重载,可以改变向内存申请内存的方式。在c++编译器的内部,实际上operator new / delete是通过调用C语言函数malloc / free 实现堆内存的申请和释放的。
//向系统申请一块原始内存,大小为10 个string对象的大小
void* rawMem = operator new(10 * sizeof(string));
通过operator new / delete分配的内存还需要通过强化类型转换,最后可以调用placement new(下文讲述)在其上构造对象。
placement new 用于在一块已经分配了内存上构造某个对象,这就是为什么它会被称为placement new, palcement new 实际上并没有向内存申请内存空间,他仅是负责在一块已经分配的内存上构造对象。上面的代码我们已经获得一块原始内存,因此我们需要在上述的代码中构造相应的对象,因此我们需要先将原始内存强制转换相应的类型,然后通过placement new在上面构造对象。
//向系统盛情一块原始内存,大小为10 个string对象的大小
void* rawMem = operator new(10 * sizeof(string));
string * str = static_cast<string*>(rawMem);//将指针转换为string类型,将会转换为10个
//调用palacement new 构造对象
for (int i = 0; i < 10; ++i) {
new (str + i)string(to_string(i));
}
new (str + i)string(to_string(i));
表示在 str+i 所指向内存处构造一个string对象,传入的构造参数为to_string(i)。
对于通过上面的方式获得的内存我们需要将其进行释放,但是我们不能直接调用
delete[] str;//错误,因为指针str指向的内存并非通过new直接得到。
因为placement new 没有申请空间, 因此我们不能直接delete转型后得到的指针,我们应该先将在该内存中的对象析构,然后delete 原始内存。
//先析构对象在释放内存
for (int i = 9; i >= 0;i--) {
str[i].~string();//调用析构函数
}
//释放原始内存
delete[](rawMem);
这样便完成了依次动态对象的申请和释放,那么我们的new expression 和new operator 以及placement new之间到底有什么关系?
实际上,我们常用的new expression 是通过调用new opreator获得一块内存,然后在该内存上构造一个对象,左后返回构造好对象的指针。而delete expression 则是先析构对象然后再将对象的内存释放。
new expression 的过程:如 *pc = new Complex(1,2);
上述语句相当于相当于以下的几步操作:
1.void * mem = opreator new(sizeof(Complex));// 获得一块大小为Complex类大小的原始内存
2.pc = static_cast(mem)// 将指针转型为Complex类的指针
3. pc->Complex::Complex(1,2); //调用构造函数(或许是调用placement new)
我们说过operator new 是通过调用malloc实现内存的封装的,那么我们就说说malloc分配的内存的缺点,malloc最大的缺点就是每次申请内存都会附加一些字节用于记录本次分配和格式的信息。如下图,假如一次申请对象,那么就会多出两个cookie的内存,假如每个cookie 4个字节,那么,如果我们调用malloc一百万次,我们将会浪费八百万个字节,这对于系统来说将是一种极大的浪费,然而我们的常用的对象一般都偏小,所以我们一般会采用其他的方式进行内存分配。
c++标准库中的allocator就是采用一次分配很大的空间,然后当需要得到时候在对空间中的进行切分,这样,可以减少因cookie而产生的浪费。对每一次分配的内存,一般用链表将其连接起来,每次申请内存时取下一块内存,当释放内存时便将内存放回链表中,以便下次使用。
下面我们通过一个简单的alloctor类,只要每个类重载operator new 便可以更改申请内存的方式。
class Allocator {
private:
//定义一个节点指针,指向下一个未分配的内存空间,被称为嵌入式指针
struct node{
struct node * next;
};
//表示某个类对应的预先分配空间的大小
int chunk = 5;
//指向分配的内存的起始位置
node* ptr = nullptr;
public:
void* allocate(size_t size);
void delocate(void *,size_t);
};
//分配空间,大小为 size * chunck,每次调用allocate将会得到一块内存
void* Allocator::allocate(size_t size) {
node* p;
if (ptr == nullptr) {//如果链表为空,则先申请一整块的内存
p = ptr = (node*)malloc((size ) * chunk);
for (int i = 0; i < chunk - 1; i++) {
p->next = (node*)((char*)p + size);//一个字符的大小为一个字节,相当于将p后移动size个字节
p = p->next;
}
//处理最后一个节点的指针,以防野指针
p->next = nullptr;
}
p = ptr;
ptr = ptr->next;
return p;
}
void Allocator::delocate(void * p,size_t size) {
//将p放回链表的前端
((node*)p)->next = ptr;
ptr = (node * )p;
}
//a如果一个类重载了operator new 那么它申请原始内存的方式将会被修改
class A {
public:
A(int s_) :s(s_) {}
void print() {
cout << "舔狗 id: " <<s<< endl;
}
private:
int s;
public:
static Allocator alloc;
//重载operator new
static void* operator new(size_t size){
cout << "舔狗归来" << endl;
return alloc.allocate(size);
}
//重载operator delete
static void operator delete(void * p, size_t size) {
cout << "舔狗离开" << endl;
alloc.delocate(p, size);
}
};
//静态变量初始化
Allocator A::alloc;
int main()
{
vector<A*>vec;
for (int i = 0; i < 10; i++ ) {
vec.push_back(new A(i));
}
for (auto i : vec) {
i->print();
}
for (int i = 0; i < 10; i++) {
delete vec[i];
}
return 0;
}
当某个类重载new operator ,那么当使用new expression 动态分配该类的对象时,将会通过重载的new、delete operator获取内存,而一次性获取大块的内存然后在进行切割,这样便可以减少cookie所占用的内存。
本文内容主要来自参考侯捷老师的课程:课程百度网盘连接如下
链接:https://pan.baidu.com/s/1cZeAwWvmQLXjk7S93kfN-Q
提取码:pukq