想要对象吗?教你如何用C++new一个完美的对象(内存管理)

想要对象吗?教你如何用C++new一个完美的对象(内存管理)_第1张图片

目录

一、C/C++内存分布

1. 栈(Stack)

2. 堆(Heap)

3. 全局区/静态区(Global Area/Static Area)

4. 常量区(Constant Area)

5. 代码区(Code Area)

二、C语言中动态内存管理方式

1. malloc函数

 2. calloc函数

3. realloc函数

4. free函数

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

四、operator new与operator delete函数

⭕operator new

⭕operator delete

五、new和delete的实现原理

1.内置类型

2.自定义类型

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

总结

malloc/free和new/delete的区别

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


 前言

        前面我们讲了C语言的基础知识,也了解了一些数据结构,并且讲了有关C++的命名空间的一些知识点以及关于C++的缺省参数、函数重载,引用 和 内联函数也认识了什么是类和对象。也相信大家都掌握的不错,接下来博主将会带领大家继续学习有关C和C++比较重要的知识点——内存管理。下面话不多说坐稳扶好咱们要开车了

一、C/C++内存分布

         C/C++的内存分布较为复杂,大概分为五个部分它们分别是:(Stack)、(Heap)、全局区/静态区(Global Area/Static Area)、常量区(Constant Area)、代码区(Code Area)下面我会分别介绍他们各自的属性。

1. 栈(Stack)

栈是用来存储函数的局部变量、函数参数和函数调用的返回地址等信息的内存空间。
栈空间由编译器自动管理,它的分配和释放是自动进行的,不需要程序员手动操作。
每当一个函数被调用时,编译器会在栈上为该函数分配一块特定大小的空间,称为函数帧或活动记录(Activation Record)。
栈是一种高效的存储方式,由于栈上的内存是连续分配的,因此访问速度较快。然而,栈的大小是有限的,过多的栈帧可能导致栈溢出的错误。

2. 堆(Heap)

堆是用来动态分配内存的空间,通过使用malloc、free、new、delete等操作来分配和释放内存。
堆的分配是手动进行的,程序员需要显式地分配和释放内存。
堆是一种灵活的存储方式,允许动态调整分配的内存大小。
堆上的内存分配通常是不连续的,因此访问速度稍慢于栈
如果使用不当,堆上的内存可能导致内存泄漏或内存碎片化等问题

3. 全局区/静态区(Global Area/Static Area)

全局区用来存储全局变量和静态变量
全局变量在程序开始执行时分配,在程序结束时被释放,因此它们的生命周期与整个程序的运行周期相同
静态变量有不同的存储位置和生命周期,具体取决于其作用域和存储类型(例如静态局部变量、静态全局变量)。
全局区的内存空间在程序启动时由操作系统分配,并在程序退出时由操作系统释放

4. 常量区(Constant Area)

常量区用来存储常量数据,例如字符串字面量和其他常量
常量区的内存通常是只读的,因此程序不能对其进行修改
常量区的数据在程序运行期间保持不变

5. 代码区(Code Area)

代码区用来存储程序的机器指令,也称为可执行代码
代码区通常是只读的,存储程序的执行逻辑
代码区的内存空间在程序启动时被分配,并且在程序运行期间保持不变

想要对象吗?教你如何用C++new一个完美的对象(内存管理)_第2张图片

         注意:具体的内存布局和分配方式可能因编译器、操作系统和硬件平台的不同而有所差异。此外,在多线程和多进程的情况下,还需要考虑线程栈、共享内存等特殊情况。

二、C语言中动态内存管理方式

1. malloc函数

  • malloc 函数用于动态地分配指定大小的内存空间。
  • 它接受一个参数,即需要分配的内存大小(以字节为单位),并返回所分配内存的起始地址。
    int ptr = (int)malloc(sizeof(int));
  • malloc 函数在堆上找到足够大的连续内存空间进行分配,如果找不到足够大的连续空间,则返回 NULL 
  • 分配的内存是未初始化的,即其中的数据是不确定的,需要手动初始化。
#include 
#include 

int main() {
    int size;
    int* arr;

    printf("Enter the size of the array: ");
    scanf("%d", &size);

    // 使用malloc函数动态分配内存
    arr = (int*)malloc(size * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!");
        return 1;
    }

    printf("Enter %d elements:\n", size);
    for (int i = 0; i < size; i++) {
        scanf("%d", &arr[i]);
    }

    printf("The elements you entered are: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }

    // 释放已分配的内存
    free(arr);

    return 0;
}

上面的程序实现了以下步骤:

  1. 用户输入数组的大小。
  2. 使用 malloc 函数动态分配足够大的内存空间来存储整数数组。
  3. 检查内存分配是否成功,如果分配失败,则输出错误信息并结束程序。
  4. 用户输入数组的元素。
  5. 打印用户输入的数组元素。
  6. 使用 free 函数释放分配的内存空间。

 2. calloc函数

  •  calloc 函数用于动态地分配指定数量和大小的内存空间,并将其初始化为0
  • 它接受两个参数,第一个是需要分配的元素数量,第二个是每个元素的大小(以字节为单位)。
    int ptr = (int)calloc(5, sizeof(int));
  •  calloc 函数在堆上找到足够大的内存空间进行分配,并将所有字节初始化为0
  • 分配的内存是初始化过的,不需要手动初始化。
#include 
#include 

int main() {
    int size;
    int* arr;

    printf("Enter the size of the array: ");
    scanf("%d", &size);

    // 使用calloc函数动态分配内存,并将内存初始化为0
    arr = (int*)calloc(size, sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!");
        return 1;
    }

    printf("Enter %d elements:\n", size);
    for (int i = 0; i < size; i++) {
        scanf("%d", &arr[i]);
    }

    printf("The elements you entered are: ");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }

    // 释放已分配的内存
    free(arr);

    return 0;
}

        上面的代码功能跟上面 malloc 函数的示例表达的功能一样这里不做过多的解释,记得在完成使用动态分配的内存后,使用 free 函数释放已分配的内存,以免出现内存泄漏。

3. realloc函数

  •  realloc 函数用于重新调整之前分配的内存空间的大小。
  • 它接受两个参数,第一个是之前分配内存的起始地址,第二个是需要调整的新大小(以字节为单位)
    ptr = (int*)realloc(ptr, 10 * sizeof(int));
  •  realloc 函数将尝试在原始内存空间上重新调整大小,如果成功,则返回调整后的内存地址;如果原始内存空间不够大或者其他错误,则返回 NULL 
  • 若新大小大于原始大小,则新增的字节未初始化;若新大小小于原始大小,则超出新大小的部分将被丢弃。
#include 
#include 

int main() {
    int size;
    int* arr;

    printf("Enter the size of the array: ");
    scanf("%d", &size);

    // 使用malloc函数动态分配内存
    arr = (int*)malloc(size * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed!");
        return 1;
    }

    printf("Enter %d elements:\n", size);
    for (int i = 0; i < size; i++) {
        scanf("%d", &arr[i]);
    }

    // 重新分配内存,将数组大小调整为10
    int newSize = 10;
    int* newArr = (int*)realloc(arr, newSize * sizeof(int));
    if (newArr == NULL) {
        printf("Memory reallocation failed!");
        free(arr);  // 释放之前分配的内存
        return 1;
    }

    // 更新指针arr的引用
    arr = newArr;

    // 打印数组元素
    printf("The elements you entered are: ");
    for (int i = 0; i < newSize; i++) {
        printf("%d ", arr[i]);
    }

    // 释放分配的内存
    free(arr);

    return 0;
}

         首先使用 malloc 函数动态分配内存,并在之后的 realloc 操作中进行了适当的错误处理。如果 realloc 函数返回 NULL ,表示内存调整失败,此时会输出错误信息并释放之前分配的内存。如果 realloc 函数成功,则将新分配的内存地址赋给 arr 指针,并继续使用调整后的内存。在最后,使用 free 函数释放分配的内存空间,并返回 0 表示程序正常结束

4. free函数

  •  free 函数用于释放之前动态分配的内存空间
  • 传入 free 函数的参数是之前分配的内存块的起始地址。
  • 调用free函数将释放所指定的内存空间,使其可供之后的malloccallocrealloc等函数重新使用。

        这些动态内存管理函数提供了灵活的内存分配和释放能力,可以根据需要动态调整内存的大小。然而,需要确保正确使用,避免内存泄漏和野指针等问题。建议在每次分配内存后检查分配是否成功,以及在适当的时候使用 free 函数释放不再需要的内存。

void Test ()
{
    int* p1 = (int*) malloc(sizeof(int));
    free(p1);


    int* p2 = (int*)calloc(4, sizeof (int));
    int* p3 = (int*)realloc(p2, sizeof(int)*10);

    // 这里需要free(p2)吗?  答:不用
    free(p3 );
}

        上面的代码中不需要 free(p2) 因为当调用realloc函数时,如果成功调整了内存大小,那么原始内存区域的内容将被复制到新分配的内存,并且原始的内存空间会被释放。因此,在这种情况下,不需要再手动调用 free(p2) 来释放原始的内存空间,因为realloc函数已经负责处理了

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

        C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过 newdelete 操作符进行动态内存管理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 会调用析构函数,而 mallocfree 不会

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

四、operator new与operator delete函数

        newdelete 是用户进行动态内存申请和释放的操作符,operator newoperator 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分配的内存。它会调用底层的内存释放函数,如freefree函数是操作系统提供的用于释放内存的函数。
  • 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 newoperator 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 

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++、算法和编程的奥秘。祝您生活愉快,排便顺畅!

想要对象吗?教你如何用C++new一个完美的对象(内存管理)_第3张图片
 

你可能感兴趣的:(C++,c++,开发语言)