深入理解C++的new和delete

文章目录

一、new 操作符介绍

在 C++ 中,new 和 delete 是用于动态内存管理的操作符。它们允许程序在运行时分配和释放内存,这对需要灵活控制内存生命周期的场景非常重要。new 用于在堆(heap)上动态分配内存,并调用构造函数(如果是对象)。

语法:

cpp 复制代码
类型* 指针 = new 类型;
类型* 指针 = new 类型(初始化值);
类型* 数组指针 = new 类型[数组大小];

功能:

1.动态分配内存:new 会在堆上分配内存。

2.调用构造函数(如果适用):为对象类型调用对应的构造函数。

3.返回指针:返回分配的内存块的指针。

示例:

cpp 复制代码
// 分配单个整数
int* pInt = new int(10); // 分配并初始化为10
std::cout << *pInt << std::endl;

// 分配数组
int* pArray = new int[5]; // 分配5个整数的数组
for (int i = 0; i < 5; ++i) {
    pArray[i] = i + 1;
    std::cout << pArray[i] << " ";
}
std::cout << std::endl;

二、new的多种操作方式

new 操作符有多种变体和用法,主要包括以下几种:
1. 普通 new

用于分配单个对象的内存并调用构造函数。

cpp 复制代码
int* p = new int;         // 分配未初始化的整数内存
int* pInit = new int(10); // 分配并初始化为10

2.定位 new(Placement New)

允许在特定的内存地址上构造对象。

cpp 复制代码
#include <iostream>
#include <new> // 包含 placement new

int main() {
    char buffer[sizeof(int)];    // 提供内存空间
    int* p = new (buffer) int(42); // 在 buffer 中构造整数
    std::cout << *p << std::endl;
    p->~int(); // 手动调用析构函数
    return 0;
}

注意:使用定位 new 时,delete 不能释放这块内存;需要手动调用析构函数并释放内存(如果需要)。

3.异常安全 new

默认情况下,new 分配失败会抛出 std::bad_alloc。使用 nothrow,可以避免抛出异常,而是返回 nullptr。

cpp 复制代码
#include <iostream>
#include <new>

int main() {
    int* p = new (std::nothrow) int; // 分配失败时返回 nullptr
    if (p) {
        *p = 10;
        std::cout << *p << std::endl;
        delete p;
    } else {
        std::cout << "Allocation failed" << std::endl;
    }
    return 0;
}

4.常量new

常量 new 的用法指的是通过 new 分配常量内存,也就是动态分配的内存指向一个 const 类型的值或对象。这可以确保分配的值不会被修改,从而提高程序的安全性和可读性。

cpp 复制代码
#include <iostream>

int main() {
    const int* pConst = new const int(42); // 分配一个常量整数
    std::cout << "Value: " << *pConst << std::endl;

    // *pConst = 10; // 错误!不能修改常量值

    delete pConst; // 内存仍需释放
    return 0;
}

三、new和malloc的区别

1.new被称作运算符,返回指定类型的指针(无需类型转换),且自动调用构造函数(对象);内存释放的时候使用 delete 或 delete[]。使用new开辟内存失败后抛出 std::bad_alloc 异常。而且它支持重载。

2.malloc是C的库函数,返回 void*,需要显式类型转换,不会自动初始化,需手动赋值。内存释放的时候使用另一个库函数free。如果malloc开辟内存失败,返回nullptr。它不支持重载。

new 会在分配内存的同时完成初始化:

cpp 复制代码
int* p = new int(10); // 分配内存并初始化为 10

malloc 不会进行初始化,分配的内存是未定义的:

cpp 复制代码
int* p = (int*)malloc(sizeof(int)); // 内存中的值是随机的

new 根据分配的类型返回特定类型的指针。例如:

cpp 复制代码
int* p = new int(10); // 不需要类型转换

malloc 返回 void*,需要显式类型转换:

cpp 复制代码
int* p = (int*)malloc(sizeof(int)); // 必须转换类型

new 使用 delete 释放单个对象,使用 delete[] 释放数组,并自动调用析构函数(如果是对象类型):

cpp 复制代码
delete p;
delete[] pArray;

malloc 使用 free 释放内存,但不会调用析构函数

cpp 复制代码
free(p);

代码示例:

使用 new 分配对象

cpp 复制代码
#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "Constructor called" << std::endl; }
    ~MyClass() { std::cout << "Destructor called" << std::endl; }
};

int main() {
    MyClass* obj = new MyClass; // 调用构造函数
    delete obj;                 // 调用析构函数
    return 0;
}

使用 malloc 分配内存

cpp 复制代码
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free

struct MyStruct {
    int x;
};

int main() {
    MyStruct* obj = (MyStruct*)malloc(sizeof(MyStruct));
    obj->x = 42; // 手动初始化
    std::cout << "x: " << obj->x << std::endl;
    free(obj); // 释放内存
    return 0;
}

四、深入理解new[]的delete[]的原理

cpp 复制代码
#include <iostream>

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

    return p;
}

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

    return p;
}

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

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

class Test {
public:
    Test(int data = 10) {
        std::cout << "Test()" << std::endl;
    }
    ~Test() {
        std::cout << "~Test()" << std::endl;
    }

private:
    int ma;
};

int main() {
    Test* p2 = new Test[5];
    std::cout << "p2:" << p2 << std::endl;
    delete[] p2;

	
	return 0;
}

输出结果:

从上面的输出可以看出,申请自定义数组对象的时候,会多开辟4个字节的内存,用来存储对象的个数,假设这个地址如上所示为015C0880,而返回给p2的地址则是在起始地址的基础上再移动4个字节,即015C0884,记录对象个数的原因是在释放内存的时候,要根据对象个数进行对象的析构。因为在释放内存的时候,肯定需要从开辟的起始地址释放内存,也就是从015C0880开始释放,如果使用delete p2去释放内存,那么就只会释放掉p2[0]的内存而已,从而造成内存泄漏的问题。

相关推荐
天堂的恶魔9463 小时前
C++封装
java·开发语言·c++
阿猿收手吧!3 小时前
【CPP】CPP经典面试题
开发语言·c++·面试·协程
xianwu5434 小时前
反向代理模块anns
数据库·c++·python·mysql·django
linhhanpy5 小时前
自制虚拟机(C/C++)(一、分析语法和easyx运用,完整虚拟机实现)
c语言·汇编·c++·单片机·操作系统
半桔6 小时前
七大排序思想
c语言·开发语言·数据结构·c++·算法·排序算法
牵牛老人6 小时前
C++ 使用CURL开源库实现Http/Https的get/post请求进行字串和文件传输
c++·http·https
水饺编程7 小时前
MFC 学习笔记目录
c语言·c++·windows·visual studio
比特在路上8 小时前
蓝桥杯之c++入门(六)【string(practice)】
c++·职场和发展·蓝桥杯
霜雪殇璃8 小时前
2025.2.6(c++杂项补充及qt基础介绍)
开发语言·c++·qt·学习