C++内存管理—揭开new和delete的神秘面纱

1. 引言

在 C++ 编程的世界里,内存管理是一项至关重要且充满挑战的任务。正确地管理内存不仅能够确保程序的高效运行,还能避免诸如内存泄漏、悬空指针等严重的错误。在 C++ 中,newdelete 这两个运算符扮演着核心角色,它们为我们提供了在堆上动态分配和释放内存的能力。然而,它们的工作机制却常常让人感到困惑。本文将深入探讨 C++ 内存管理,详细剖析 newdelete 的工作原理,帮助你揭开它们的神秘面纱。

2. C++ 内存区域划分

在深入了解 newdelete 之前,我们需要先了解 C++ 程序的内存区域划分。一个 C++ 程序的内存通常被划分为以下几个区域:

  1. 栈区:由编译器自动分配和释放,存放函数的参数值、局部变量等。栈区的内存分配和释放速度快,遵循后进先出(LIFO)的原则。
  2. 堆区:由程序员手动分配和释放,用于存储动态分配的对象。堆区的内存分配和释放相对较慢,但可以根据程序的需要灵活地分配内存。
  3. 全局/静态区:存放全局变量和静态变量。在程序启动时分配内存,在程序结束时释放内存。
  4. 常量区(代码段):存放常量字符串和程序的可执行代码等常量数据,这些数据在程序运行期间不可修改。

这有一个小题考考大家

int globalVar = 1;
 static int staticGlobalVar = 1;
 void Test()
 {
	 static int staticVar = 1;
	 int localVar = 1;
	 int num1[10] = { 1, 2, 3, 4 };
	 char char2[] = "abcd";
	 const char* pChar3 = "abcd";
	 int* ptr1 = (int*)malloc(sizeof(int) * 4);
	 int* ptr2 = (int*)calloc(4, sizeof(int));
	 int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	 free(ptr1);
	 free(ptr3);
 }

选择题:
A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?___
*pChar3在哪里?___
ptr1在哪里?____
*ptr1在哪里?____

答案:C、C、C、A、A、A、A、A、D、A、B

3. C 语言中的内存管理方式

在 C 语言中,我们使用 malloccallocreallocfree 等函数来进行内存管理。
我们来回顾一下,下面是这些函数的简单介绍:

1. malloc 函数

malloc 函数用于在堆上分配指定大小的内存块,返回一个指向该内存块的指针。如果分配失败,返回 NULL

#include <iostream>
#include <cstdlib>

int main() {
    int* ptr = (int*)malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 10;
        std::cout << "Allocated value: " << *ptr << std::endl;
        free(ptr);
    }
    return 0;
}

2. calloc 函数

calloc 函数用于在堆上分配指定数量和大小的内存块,并将其初始化为 0。返回一个指向该内存块的指针。如果分配失败,返回 NULL

#include <iostream>
#include <cstdlib>

int main() {
    int* arr = (int*)calloc(5, sizeof(int));
    if (arr != NULL) {
        for (int i = 0; i < 5; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
        free(arr);
    }
    return 0;
}

3. realloc 函数

realloc 函数用于重新分配已经分配的内存块的大小。如果新的大小比原来的大小大,可能会将内存块移动到新的位置。返回一个指向新内存块的指针。如果分配失败,返回 NULL

#include <iostream>
#include <cstdlib>

int main() {
    int* ptr = (int*)malloc(2 * sizeof(int));
    if (ptr != NULL) {
        ptr = (int*)realloc(ptr, 4 * sizeof(int));
        if (ptr != NULL) {
            // 使用新分配的内存
            free(ptr);
        }
    }
    return 0;
}

4. free 函数

free 函数用于释放由 malloccallocrealloc 分配的内存块。

#include <iostream>
#include <cstdlib>

int main() {
    int* ptr = (int*)malloc(sizeof(int));
    if (ptr != NULL) {
        free(ptr);
    }
    return 0;
}

4. C++ 中的 new 和 delete 运算符

4.1 new 运算符

new 运算符用于在堆上动态分配内存,并调用对象的构造函数进行初始化。new 有以下几种使用方式:

(1)单个对象的动态分配
#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;
}
(2)数组的动态分配
#include <iostream>

int main() {
    int* arr = new int[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = i;
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    delete[] arr;
    return 0;
}
(3)带初始值的动态分配
#include <iostream>

int main() {
    int* num = new int(10);
    std::cout << *num << std::endl;
    delete num;
    return 0;
}

4.2 delete 运算符

delete 运算符用于释放由 new 分配的内存,并调用对象的析构函数。对于单个对象,使用 delete;对于数组,使用 delete[]

#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;

    MyClass* arr = new MyClass[3];
    delete[] arr;
    return 0;
}

4.3 实现原理

4.3.1 内置类型

如果申请的是内置类型的空间,newmallocdeletefree基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
这又蹦出个新概念——异常
简单来说,异常是一种处理程序运行时错误与异常状况的机制。当程序遇到无法处理的情况时,会抛出一个异常,程序的控制权会从当前位置转移到能够处理该异常的地方。
具体我会在后面详细给大家谈谈C++中的异常处理,目前做一个了解就好。

4.3.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来释放空间

那啥是operator newoperator delete?这不,往下看,正准备说嘛

5. new 和 delete 的工作原理

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

5.1 new 的工作原理

new 运算符的工作过程可以分为以下两个步骤:

(1)调用 operator new 函数分配内存

operator new 是一个全局函数,用于在堆上分配指定大小的内存块。它的原型如下:

void* operator new(size_t size);
(2)调用对象的构造函数进行初始化

在分配好内存后,new 运算符会调用对象的构造函数对内存进行初始化。

看不懂源代码没关系,那是因为里面定义了很多宏,我们只需要看我们看得懂的哪怕一个单词,能够大致明白其中原理就绰绰有余啦
operator new源代码:

/*
 operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;
 申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,
 则继续申请,否则抛异常。
*/
 void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
 { 
	 // try to allocate size bytes
	 void *p;
	 while ((p = malloc(size)) == 0)
	 if (_callnewh(size) == 0)
	 {
		 // report no memory
		 // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
		static const std::bad_alloc nomem;
		 _RAISE(nomem);
	 }
	 return (p);
 }

我们可以很清晰的看到new就是调用的malloc,那么这时候就有人要问了,那delete是不是调用的free呢?没错!

5.2 delete 的工作原理

delete 运算符的工作过程可以分为以下两个步骤:

(1)调用对象的析构函数进行清理

在释放内存之前,delete 运算符会调用对象的析构函数,释放对象所占用的资源。

(2)调用 operator delete 函数释放内存

operator delete 是一个全局函数,用于释放由 operator new 分配的内存块。它的原型和源代码如下:

void operator delete(void* ptr);
 /*
 operator delete: 该函数最终是通过free来释放空间的
*/
 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 new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。

6. new 和 delete 与 malloc 和 free 的区别

1. 功能方面

  • newdelete 是 C++ 的运算符,会自动调用对象的构造函数和析构函数;而 mallocfree 是 C 语言的函数,不会调用对象的构造函数和析构函数。
  • new 可以进行类型转换,返回的指针类型与对象类型匹配;而 malloc 返回的是 void* 类型的指针,需要进行显式的类型转换。

2. 异常处理方面

  • new 在内存分配失败时会抛出 std::bad_alloc 异常;而 malloc 在内存分配失败时返回 NULL

3. 扩展性方面

  • newdelete 可以进行重载,以实现自定义的内存分配和释放行为;而 mallocfree 是标准库函数,不能进行重载。

7. 内存泄漏与避免

1. 内存泄漏的概念

内存泄漏是指程序在运行过程中,动态分配的内存没有被正确释放,导致这部分内存无法被再次使用。内存泄漏会导致程序占用的内存不断增加,最终可能导致系统资源耗尽。

2. 内存泄漏的原因

  • 忘记调用 deletedelete[] 释放内存。
  • 异常导致 deletedelete[] 没有被执行。

3. 避免内存泄漏的方法

智能指针也会在后续的文章中加以说明,在此大家了解足矣

  • 使用智能指针(如 std::unique_ptrstd::shared_ptr)来管理动态分配的内存,智能指针会在对象的生命周期结束时自动释放内存。
#include <iostream>
#include <memory>

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

int main() {
    std::unique_ptr<MyClass> obj = std::make_unique<MyClass>();
    return 0;
}
  • 在使用 newdelete 时,确保 newdeletenew[]delete[] 成对出现。
  • 在异常处理中,确保在异常发生时也能正确释放内存。

看到这里,我们已经深入了解了 C++ 内存管理的基本概念,以及 newdelete 运算符的工作原理。newdelete 为我们提供了在堆上动态分配和释放内存的强大能力,但同时也需要我们谨慎使用,避免内存泄漏等问题。在实际编程中,我们可以根据具体的需求选择合适的内存管理方式,同时结合智能指针等工具,提高程序的健壮性和可靠性。希望这篇文张能够帮助你揭开 newdelete 的神秘面纱,更好地掌握 C++ 内存管理。

那么咱们异常处理再见咯~~~
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值