C++语法|深入理解 new 、delete

在开发过程中,非常重要的语法就有我们new和delete,周所周知在C++中最为强大的能力就是对内存的控制,所以我们再怎么强调new和delete都不为过

1.new和delete基本语法

在本节中,我们会通过对比malloc和free来对比讲解new和delete的基本语法。

new和malloc的区别是什么?

我觉得想要完整描述他们两个的区别就是要从以下四个层次来进行描述:
开辟单个内存、开辟数组内存、开辟结构体内存和开辟类类型的内存。

还有个一句话总结,
malloc是一个库函数,而new是运算符,所以调用的方法肯定是不一样的。
malloc按字节开辟内存,只分配原始内存,不会调用构造函数,所以在不涉及到类类型的时候,其实用哪个语法上都差不多。
new不仅会分配内存内存,还会调用析构函数,并且对应的delete也会去调用析构山函数,但是free一个类类型首先需要显式调用析构函数,然后free这块内存。

所以说申请一段内存就调用malloc(更轻便),如果为类对象申请内存,请使用new!

(1)开辟单个元素的内存差别

malloc是按照字节来开辟内存的,返回值是void。需要对返回的类型进行一个强转*。并且对于开辟内存失败的语法就是通过返回值来进行检测的。
并且,我们需要对这块内存尽兴初始化,因为malloc可不帮我们进行初始化,他只负责给我们开辟内存

int *p = (int*)malloc(sizeof(int)); //molloc按照
if (p == nullptr) { //返回值是空说明内存开辟失败
    return -1; 
}
*p = 20;

然后它直观开辟内存,他不管初始化,我们需要单独用*p的代码来进行一个初始化的操作,释放这个p的内存需要一个free的库函数。它的语法也是把这块内存的起始地址传进来。

free(p)

也就是说new不仅可以做内存开辟,还可以做内存初始化操作,并且new可以直接指定开辟内存的类型,所以也不需要强转; 可以直接用小括号指定初始值

而new开辟内存失败是通过抛出bad_alloc类型的异常来判断。一般是使用try 和 catch

try {
	int *p1 = new int(20);
} catch (const bad_alloc &e) {

}

释放内存直接delete p1即可。

delete p1;

(2)开辟数组内存语法差别

分别使用malloc和new开辟数组内存如下代码:

int *q = (int*)malloc(sizeof(int) * 20);
if (q == nullptr) {
    return -1;
} 
free(q);

//int *q1 = new int[20];这样写的话堆上只负责开辟数组而不进行初始化
int *q1 = new int[20](); //20个int 。并且初始化所有元素为0
delete[]q1;

在开辟数组内存的语法中,malloc仍然遵循——指定要开辟的内存大小即可;
但是new却连语法都一样了,需要用到一个[]中括号来指明需要开辟的数组个数。并且new只支持数组元素初始化为0。

为什么C++里面要把释放单个元素内存和数组内存分开呢?
原因不体现在简单类型上,而是体现在面向对象的类类型上,我们在new的函数重载中会讲到讲到。

(3)开辟类类型的语法差别

我们一定要抓住关键点:
malloc 是 C 库函数,用于在堆上分配指定字节数的内存,但它只分配内存,不调用类的构造函数。因此,如果你使用 malloc 来分配一个类对象的内存,你需要手动调用构造函数(通常通过 placement new)并且手动管理初始化和销毁。

如果使用new来申请一个类类型,那就简单多了:

(4) 为结构体类型分配内存

其实如果在C++中,如果结构体类型没有写构造函数,编译器会自动生成一个默认构造函数。默认构造函数会将所有内置类型的成员初始化为未定义值(对于基本数据类型)。
此时malloc和new的使用没有什么本质上的区别。

new有多少种?

  • 抛出异常版本的new
int *p1 = new int(20);
  • 不抛出异常版本的new
int *p2 = new (nothrow) int; 
  • 常量new:在开辟常量new的时候,我们一定要用常量指针来指向这块内存,不然就会抛出错误。
//抛出错误,在堆上抛出了一个常量
//int *p3 = new const int(40);
//既然是使用常量我们就不能把它当一个普通内存,我们必须使用常量指针去指向这块内存
const int *p3 = new const int(40); 
  • 定位new:常常用在类类型的构造和销毁
int data = 0;
int *p4 = new (&data) int (50);

首先我们知道这里的data是在栈内存上的。当我们使用定位new,此时new就不会再在堆上为我们分配内存了,而是在已经存在的内存地址(即&data)上构造对象。
也就是说new (&data) int(50) data 的地址上构造一个 int,并初始化为 50。并且data仍然在栈上。

八股背诵总结:
malloc和new的区别
1.malloc按字节开辟内存,new开辟内存时要指定类型,所以malloc开辟内存返回的都是void* ,new的返回值自动转换成我们指定的指针类型operator new -> int*
2.malloc只负责开辟空间,new不仅仅有malloc的功能,还可以进行数据的初始化
3.malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常
free和delete区别:
delete:调用析构函数,free不会调用

2.new和delete的函数重载

从汇编代码上可以看出,new的调用本质上是对operator new函数的调用。delete的调用本质上是对operator delete函数的调用。所以我们其实可以自己进行该函数的重载。

重载operator new 和 operator delete

//先调用oeprator new开辟内存空间、然后调用对象的构造函数(初始化)
void* operator new(size_t size) {
	void *p = malloc(size);
	if (p == nullptr)
		throw bad_alloc;
	cout << "operator new addr: " << p << endl;
	return p;
}

// delete p;调用p指向对象的析构函数、再调用operator delete释放内存空间
void operator delete (void *ptr) {
	cout << "operator delete addr: " << ptr << endl;
	free(ptr);
}

int main () {
	int *p = new int;
	delete p;
	return 0;
}

现在编译器链接的就是我们自己实现的operator new 和 operator delete,而不会链接C++库的这两个函数
如果想判断是否开辟内存失败:

try {
	int *p = new int;
	delete p;
} catch (const bad_alloc &err) {
	cerr << err.what() << endl;
}

重载new[]和delete[]

void* operator new[] (size_t size) {
	void *p = malloc(size);
	if (p == nullptr)
		throw bad_alloc;
	cout << "operator new[] addr: " << p << endl;
	return p;
}

void operator delete[] (void *ptr) {
	cout << "operator delete[] addr: " << ptr << endl;
	free(ptr);
}

我们new一个数组进行一下测试:

try {
	int *p = new int;
	delete p;

	int *q = new int[10];
	delete[] q;
} catch (const bad_alloc &err) {
	cerr << err.what() << endl;
}

可以看出结果非常完美

什么时候要重载operator new 和 operator delete呢?
我们的应用或者整个的系统整个内存管理有我们自己的实现方案,比如说实现一个内存池,否则我们就规规矩矩使用库里面的这些函数即可。
除非我保证当前应用我自己解决内存管理的方案比new和delete都好,否则就用库里面的new和delete即可。
(内存池的使用场景:对于实时性要求比较强的,对于小块内存频繁进行分配和释放的应用最好是实现内存池)

总而言之,如果我们重写operator new和operator delete我们就可以接管整个程序的内存管理。

重载 new 和delete的典型应用

如何设计一个程序可以让其检查C++程序内存泄漏的问题
有内存泄漏那就说明new操作肯定没有对应的delete,我们可以在全局去重写 operator new 和 operator delete函数,在new操作里面我们用映射表记录一下我们都有哪些内存被开辟过了,然后释放的时候把相应的内存资源删除掉。如果我们映射表中表示还有一些内存没有被释放,那就肯定存在内存泄漏的问题。

#include <iostream>
#include <unordered_map>
#include <cstdlib>

std::unordered_map<void*, size_t> allocations;

void* operator new(size_t size) {
    void* ptr = std::malloc(size);
    if (ptr) {
        allocations[ptr] = size;
    }
    return ptr;
}

void operator delete(void* ptr) noexcept {
    auto it = allocations.find(ptr);
    if (it != allocations.end()) {
        allocations.erase(it);
    }
    std::free(ptr);
}

int main() {
    int* p = new int[10];

    // 故意不释放内存以测试泄漏检测
    // delete[] p;

    // 检查内存泄漏
    if (!allocations.empty()) {
        std::cout << "Memory leaks detected:" << std::endl;
        for (const auto& alloc : allocations) {
            std::cout << "Leaked " << alloc.second << " bytes at " << alloc.first << std::endl;
        }
    } else {
        std::cout << "No memory leaks detected." << std::endl;
    }

    return 0;
}

3.面试问题:new\new[]和delete\delete[]能混用吗(C++为什么区分单个元素和数组的内存分配和释放)?

如果是开辟单个的元素和数组的话,new和delete混用是没有任何问题的,因为此时他们只剩malloc 和 free 的功能

但是如果设计到类类型,那就不行了。我们还是跟之前一样实现operator new 和 operator delete
比如说我们设计以下类:

void* operator new(size_t size) {
	void *p = malloc(size);
	if (p == nullptr)
		throw bad_alloc;
	cout << "operator new addr: " << p << endl;
	return p;
};
void operator delete (void *ptr) {
	cout << "operator delete addr: " << ptr << endl;
	free(ptr);
};
class Test {
public:
	Test(int data = 10) { cout << "Test()" << endl}
	~Test() { cout << "~Test()" << endl;}
private:
	int ma_;
}

首先我们来测试operator new和operator delete[]的混用。

int main () {
	Test *p1 = new Test();
	//delete[] p1; 混用会报错,无法正常析构
	delete p1;
}

如果使用delete[] p1,会不停去析构Test而无法正常去释放内存。这是为什么呢?因为我们在开辟内存类对象和数组对象的时候,系统会为我们在返回给我们的指针的前面开辟一块内存(可能4字节、可能16字节)来存储我们开辟对象的个数,但是我们使用的是new而不是new[],所以前面根本就没有存储对象个数的内存,然而我们却调用delete[]来释放内存,系统就会去找存储对象内存个数的那块内存,所以酷酷报错。


现在我们来测试operator new[]和operator delete

int main () {
    Test *p2 = new Test[5];
    //delete p2;
    delete[] p2;
}

我们构造了5个Test类并且为它分配内存,但是如果使用delete p2,只能析构最后构造出来的Test类。(有的编译器会直接报错,进程直接崩溃)
结果如下:

operator new addr: 0x12f606de0
Test()
Test()
Test()
Test()
Test()
~Test()
operator delete addr: 0x12f606df0

我们从0x12f606de0地址但是却从0x12f606df0释放,有16个字节的内存泄漏!

总结:

  • 对于不同的编译器内置类型,new和delete[]混用 或者是 new[]和delete混用是完全没问题的,因为内置类型没有构造析构可言,只涉及内存开辟释放,而内存开辟释放的底层就是malloc和free。
  • 如果我们自己定义了类类型,就必须搭配使用而不能混用。因为其实我们调用new[]时,系统不仅仅会为我们开辟对应个数的内存空间,而且还会在开辟地址的首地址记录存储对象的个数(该区域一般占4个字节,我的电脑上占16个字节)。
    如果我们用new[]开辟,但是用delete来释放内存,系统会以为我们只开辟了一个类对象的内存,所以只会释放一个类对象的内存,就让我们上文中的输出operator new addr: 0x12f606de0 operator delete addr: 0x12f606df0
  • 自定义的类类型,有析构函数,为了调用正确的析构函数,那么开辟对象数组的时候,回多开辟4个字节记录对象的个数。所以我们不能随意混用
  • 33
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值