【C++入门到精通】C++入门 —— 内存管理(new函数的讲解)下

简介: 【C++入门到精通】C++入门 —— 内存管理(new函数的讲解)下

三、C++中动态内存管理


       C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。new 操作符在C++中用于在堆上动态分配内存,可以用于分配单个对象、对象数组以及动态创建对象。它调用对象的构造函数进行初始化,并需要使用 delete 操作符手动释放分配的内存。


new 操作符的一般语法为:


ptr = new T;
delete ptr; // 释放已分配的内存

其中,T 是要分配的对象的类型,ptr 是一个指针变量,用于存储分配对象的地址。


int* ptr = new int;  // 分配一个int类型的内存
*ptr = 10;  // 向分配的内存存储数据
// 动态申请一个int类型的空间并初始化为10
int* ptr1 = new int(10);
delete ptr; // 释放已分配的内存
delete ptr1; // 释放已分配的内存

在上述示例中,new int 用于分配一个 int 类型的内存,并返回一个指向该内存的指针,然后将值 10 存储到该内存中。


如果需要分配一个数组,可以使用new[]操作符:


int size = 5;
int* arr = new int[size]; // 分配一个包含5个int元素的数组内存
for (int i = 0; i < size; i++) {
    arr[i] = i;
}
delete[] arr; // 释放已分配的数组内存

       在上面的代码中,new int[size] 用于分配包含5个 int 元素的数组,并返回指向该数组首元素的指针。然后,通过循环将每个元素赋值为其索引。


需要注意以下几点:


  • 使用new分配的内存位于堆上,直到使用delete手动释放或对象超出作用域时才会被释放。
  • 当使用new操作符分配内存时,会调用对象的构造函数来初始化该对象。
  • 分配的内存应使用相应的delete或delete[]操作符进行释放,以避免内存泄漏。
  • new操作符分配内存失败时,会抛出std::bad_alloc异常,因此需要进行错误处理。

除了上面提到的使用new来分配内存,还可以使用new操作符来动态创建对象并初始化,例如:


class MyClass {
public:
    MyClass(int value) : m_value(value) {
        std::cout << "Constructor called." << std::endl;
    }
    ~MyClass() {
        std::cout << "Destructor called." << std::endl;
    }
    void PrintValue() {
        std::cout << "Value: " << m_value << std::endl;
    }
private:
    int m_value;
};
int main() {
    MyClass* obj = new MyClass(42);  // 动态创建MyClass对象,并通过构造函数进行初始化
    obj->PrintValue();
    delete obj;  // 手动释放内存,调用析构函数
    return 0;
}


       在上面的代码中,通过 new 动态创建一个 MyClass 对象,并调用构造函数进行初始化。通过指针访问对象的成员函数,并最后使用 delete 释放所分配的内存。在申请自定义类型的空间时,new 会调用构造函数,delete 会调用析构函数,而 malloc 与 free 不会


       在使用 new / delete 或 new[] / delete[] 进行动态内存管理时,应该准确匹配每个 new 操作符和相应的 delete 操作符,或者每个 new[] 操作符和相应的 delete[] 操作符。不正确的匹配可能会导致内存泄漏或未定义行为。


四、operator new与operator delete函数


       new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。


⭕operator new


       该函数实际通过 malloc 来申请空间,当 malloc 申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。


void* operator new(size_t size) {
    // 调用底层的内存分配函数,例如 malloc
    void* ptr = malloc(size);
    if (ptr == nullptr) {
        // 内存分配失败,抛出异常
        throw std::bad_alloc();
    }
    return ptr;
}
void operator delete(void* ptr) noexcept {
    // 调用底层的内存释放函数,例如 free
    free(ptr);
}
int main() {
    int* ptr = new int;
    *ptr = 10;
    delete ptr;
    return 0;
}


       在上面的代码中,operator new 函数调用了底层的  malloc 函数来分配内存,并在分配失败时抛出 std::bad_alloc 异常。 operator delete 函数调用了底层的   free 函数来释放内存。上面的这些只是概念性的代码,实际的底层实现可能更加复杂,并可能涉及到处理内存对齐、内存池、内存追踪等操作。具体的底层代码会因编译器、操作系统和编译选项的不同而有所差异。


       总的来说,operator new 实际也是通过 malloc 来申请空间,如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。


⭕operator delete


该函数最终是通过free来释放空间的,operator delete 函数的底层逻辑:


  • operator delete函数负责释放通过operator new分配的内存。它会调用底层的内存释放函数,如free。free函数是操作系统提供的用于释放内存的函数。
  • operator delete函数将传入的内存指针作为参数,并将其传递给底层的free函数来释放内存。
  • operator delete函数不会关心所释放的内存大小。因此,释放内存时必须确保传入的内存指针是通过对应的operator new分配的,并且内存大小与分配时的大小匹配。
  • C++标准库还提供了operator delete[]函数,用于释放通过operator new[]分配的数组内存。
void operator delete(void *pUserData)
{
    _CrtMemBlockHeader * pHead;
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
        return;
    _mlock(_HEAP_LOCK); /* block other threads */
    __TRY
        /* get a pointer to memory block header */
        pHead = pHdr(pUserData);
        /* verify block type */
        _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
        _free_dbg( pUserData, pHead->nBlockUse );
    __FINALLY
        _munlock(_HEAP_LOCK); /* release other threads */
    __END_TRY_FINALLY
    return;
}
/*
free的实现
*/
#define     free(p)     _free_dbg(p, _NORMAL_BLOCK)


以下是一个使用重载 operator delete的示例:


void operator delete(void* ptr) noexcept {
    // 自定义内存释放逻辑,例如使用其他的内存释放函数
    myMemoryFreeFunction(ptr);
}
int* ptr = new int;
delete ptr;  // 使用自定义的operator delete进行内存释放

       通过重载operator delete,可以使用自定义的内存释放逻辑来代替默认的内存释放策略。这对于使用自定义的内存分配方案或特定的内存管理需求非常有用。


需要注意的问题有以下几点:


  • operator new和 operator delete函数是全局函数,可以在全局范围内进行重载。
  • 可以根据需要重载不同的版本,在重载时可以提供额外的参数来进行定制化的内存分配或释放操作。
  • 在重载过程中,要遵循相应的内存对齐规则和语义,以确保内存分配和释放的正确性。
  • 使用自定义的operator new和operator delete函数时要注意内存的一致性和正确释放,以避免内存泄漏或未定义行为。


五、new和delete的实现原理


1.内置类型


       如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。


2.自定义类型


  • new的原理

1. 调用operator new函数申请空间

2. 在申请的空间上执行构造函数,完成对象的构造


  • delete的原理
  • 1. 在空间上执行析构函数,完成对象中资源的清理工作

2. 调用operator delete函数释放对象的空间


  • new T[N]的原理

1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请

2. 在申请的空间上执行N次构造函数


  • delete[]的原理

1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间


六、定位new表达式(placement-new)


       定位new表达式(Placement new)是C++中的一种特殊形式的new表达式,用于在已经分配的内存区域上构造对象。它可以在指定的内存地址上调用对象的构造函数,而不是分配新的内存来创建对象。


定位new表达式的语法如下:


new (ptr) Type(arguments);

       其中,ptr 是一个指向已分配内存的指针, Type 是要构造的对象类型, arguments 是传递给对象构造函数的参数。


       使用定位new表达式时,会在给定的内存地址上调用对象的构造函数,将对象在该地址上构造出来。这意味着对象的内存由用户预先分配,而不是由new操作符动态分配。由此,定位new表达式允许在指定的内存位置创建对象,适用于特定的内存分配需求。以下是一个使用定位new表达式的示例:


#include <iostream>
class MyClass {
public:
    MyClass(int value) : m_value(value) {
        std::cout << "Constructor called" << std::endl;
    }
    ~MyClass() {
        std::cout << "Destructor called" << std::endl;
    }
    void printValue() {
        std::cout << "Value: " << m_value << std::endl;
    }
private:
    int m_value;
};
int main() {
    // 分配内存
    void* memory = operator new(sizeof(MyClass));
    // 在已分配的内存上构造对象
    MyClass* obj = new (memory) MyClass(42);
    // 调用对象的成员函数
    obj->printValue();
    // 调用对象的析构函数
    obj->~MyClass();
    // 释放内存
    operator delete(memory);
    return 0;
}


       在上述示例中,我们首先使用 operator new 手动分配了一块内存,然后通过定位new表达式在这个内存地址上构造了一个MyClass对象。然后,我们可以通过对象指针调用成员函数和析构函数。最后,使用 operator delete 释放了这块内存。


       注意:定位new表达式要求用户自行管理内存的分配和释放,确保在构造和析构期间正确处理生命周期。同时,使用定位new表达式时必须保证分配的内存空间足够容纳对象,并且类型对齐正确。


       定位new表达式是C++中的一种特殊形式的new表达式,用于在已经分配的内存区域上构造对象。它使用分配的内存地址来调用对象的构造函数,允许在指定的内存位置创建对象,适用于特定的内存分配需求。使用定位new表达式时,要确保正确管理内存的分配和释放,并确保类型对齐正确。


总结


malloc/free和new/delete的区别


malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。


不同的地方是:


  1. malloc和free是函数,new和delete是操作符。
  2. malloc申请的空间不会初始化,new可以初始化。
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型。
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。


什么是内存泄漏,内存泄漏的危害


       什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

       内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等出现内存泄漏会导致响应越来越慢,最终卡死。


温馨提示


       感谢您对博主文章的关注与支持!在阅读本篇文章的同时,我们想提醒您留下您宝贵的意见和反馈。如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!


       再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!

f3da82c02a7b7a3dc9154983d517186b_78ec7b4c989b4d4d9cf4ccd04b3fc0e0.jpeg


目录
相关文章
|
10天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
25 3
|
2天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
5天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
10 1
|
5天前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
23 0
【C++打怪之路Lv6】-- 内存管理
|
6天前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
13 0
|
6天前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
12 0
|
10天前
|
C语言 C++
深度剖析C++string(中)
深度剖析C++string(中)
32 0
|
10天前
|
存储 编译器 程序员
深度剖析C++string(上篇)(2)
深度剖析C++string(上篇)(2)
30 0
|
10天前
|
存储 Linux C语言
深度剖析C++string(上篇)(1)
深度剖析C++string(上篇)(1)
23 0
|
10天前
|
C++
C/C++内存管理(下)
C/C++内存管理(下)
24 0