🔥个人主页:Quitecoder
🔥专栏:c++笔记仓
朋友们大家好,本篇文章我们详细讲解c++中的动态内存管理
目录
- 1.C/C++内存分布
- 2.C语言中动态内存管理方式:malloc/calloc/realloc/free
- 3.c++内存管理方式
- 3.1new/delete对内置类型的操作
- 3.1.1抛异常
- 3.2new/delete对自定义类型的操作
- 4.operator new与operator delete函数
- 5.new和delete的实现原理
- 6.简单了解定位new表达式(placement-new)
- 7.概念辨析
- 7.1 malloc/free和new/delete的区别
- 7.2 内存泄漏
1.C/C++内存分布
我们来看内存区域划分
数据段就是我们所说的全局变量,代码段是我们所说的常量区,我们需要重点关注的是堆区,这部分是由我们自己控制的
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在哪里?__1__ staticGlobalVar在哪里?__2__ staticVar在哪里?__3__ localVar在哪里?__4__ num1 在哪里?__5__ char2在哪里?__6__ *char2在哪里?_7__ pChar3在哪里?__8__ *pChar3在哪里?__9__ ptr1在哪里?_10___ *ptr1在哪里?__11__
我们来依次讨论:
- globalVar 是全局变量,不是静态的,所以它存储在数据段(静态区)
- staticGlobalVar 也是全局变量,但它是静态的,因此它同样存储在数据段(静态区)
- staticVar 是函数内的静态变量,所以它存储在数据段(静态区),因为它的生命周期贯穿程序的整个执行期
- localVar 是局部变量,存储在栈上
- num1 是局部变量,它是数组,存储在栈上
- char2 是局部变量,它是数组首元素的地址,存储在栈上
- *char2(即char2数组的内容)存储在栈上,因为char2本身就在栈上
- pChar3 是局部指针变量,存储在栈上
- *pChar3 指向的内容(即字符串"abcd")存储在代码段(常量区)
- ptr1 是局部指针变量,存储在栈上
- *ptr1 指向的内容(即通过malloc分配的内存)存储在堆上
-
*char2(局部字符数组)
当你声明一个局部字符数组并用一个字符串字面量初始化它,如char char2[] = "abcd";时,编译器在栈上为数组分配内存,然后将字符串字面量的内容(包括结尾的\0字符)复制到这块内存中。因此,char2和它的内容(*char2指向的内容)都存储在栈上
-
*pChar3(字符串字面量指针)
另一方面,当你使用指针指向一个字符串字面量,如const char* pChar3 = "abcd";时,这个字符串字面量存储在程序的只读数据段(或称为代码段、常量区)中。pChar3本身作为一个局部指针变量存储在栈上,但它指向的字符串(“abcd”)实际上存储在常量区。这是因为字符串字面量被视为常量数据,编译器会将它们放在程序的常量区域内,这个区域通常是只读的,以防止程序意外修改它的内容。因此,尽管pChar3是一个指针,存储在栈上,但它指向的字符串内容存储在常量区
总结:
- *char2不在常量区,因为char2是局部字符数组,其内容直接存储在栈上。
- *pChar3在常量区,因为它指向的是一个字符串字面量,字符串字面量被存储在程序的常量区域,这部分内存是只读的。
当我们讨论变量存储在哪里时,通常涉及到几个关键区域:栈(Stack)、堆(Heap)、数据段(Data Segment,又称静态区)、和代码段(Code Segment,又称常量区)。每种类型的变量根据其特性和声明周期被存储在这些区域中的相应位置
-
栈是用于存储局部变量、函数参数等的内存区域。当一个函数被调用时,其局部变量和一些书keeping信息被推入栈中;当函数执行完成,这些信息被从栈上弹出。栈是自动管理的,开发者无需手动分配或释放内存。
-
堆是用于动态内存分配的内存区域。不同于栈,开发者需要显式地从堆上分配内存(如使用malloc或new),并在不再需要时释放这些内存(如使用free或delete)。
-
数据段,又称为静态区,用于存储全局变量、静态变量等。这些变量的生命周期贯穿整个程序执行期,因此它们被存储在一个特定的、持久的内存区域中。
-
代码段,又称为常量区,用于存储程序的执行代码和常量数据,如字符串字面量。这部分内存是只读的,用来保证程序代码的安全性
2.C语言中动态内存管理方式:malloc/calloc/realloc/free
在C语言中,动态内存管理是通过一组标准库函数完成的,包括malloc, calloc, realloc, 和 free。这些函数允许程序在运行时动态地分配、调整和释放堆内存,这是对于管理变化的数据量和大小特别有用的能力。下面是这些函数的基本用法和它们之间的区别:
malloc
- 用法:void* malloc(size_t size);
- 功能:分配指定字节数的未初始化内存。它返回一个指向分配的内存的指针。如果分配失败,返回NULL。
- 示例:int* ptr = (int*)malloc(sizeof(int) * 4); 这行代码为4个整数分配了内存
calloc
- 用法:void* calloc(size_t num, size_t size);
- 功能:为指定数量的元素分配内存,每个元素的大小也在参数中指定,并自动初始化所有位为0。如果分配失败,返回NULL。
- 示例:int* ptr = (int*)calloc(4, sizeof(int)); 这行代码为4个整数分配了内存,并将它们初始化为0。
realloc
- 用法:void* realloc(void* ptr, size_t size);
- 功能:调整之前调用malloc或calloc分配的内存块的大小。如果新的大小大于原始大小,可能会移动内存块到新的位置以提供足够的连续空间。如果realloc的第一个参数是NULL,它的行为就像malloc。
- 示例:ptr = (int*)realloc(ptr, sizeof(int) * 8); 这行代码将之前分配的内存大小调整为8个整数的大小。
free
- 用法:void free(void* ptr);
- 功能:释放之前通过malloc, calloc, 或 realloc分配的内存。一旦内存被释放,那块内存就不能再被访问了。
- 注意:尝试释放未经分配的内存块或多次释放同一个内存块是不安全的,可能导致未定义行为
注意
- 在使用这些函数时,确保正确处理内存分配失败的情况,并在内存不再需要时使用free来避免内存泄露。
- 当使用realloc时,如果分配失败,原始内存不会被释放。因此,建议先将realloc的返回值赋给一个临时指针,以检查是否分配成功,再重新赋值给原始指针,以避免内存泄漏。
- 始终确保只对通过malloc, calloc, 或 realloc分配的指针使用free,并且每个分配的内存块只被free一次
3.c++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理
3.1new/delete对内置类型的操作
new的基本用法
Type* variable = new Type(arguments);
- Type:要分配的对象类型
- variable:指向分配的内存的指针
- arguments:传递给构造函数的参数(如果需要的话)
示例:
int* ptr1 = new int;
在堆上分配了一个int大小的内存
int* ptr2 = new int[10];
加上方括号[ ]表示分配了十个int大小的内存
释放:
对于ptr,我们直接delete
delete ptr1;
释放数组对象的内存ptr2,我们需要加上方括号:
delete [] ptr2;
我们也可以分配内存的同时直接初始化:
int* ptr5 = new int(5);
动态申请一个int类型的空间并初始化为5
我们也可以同时开辟多个空间完成初始化:
int* ptr6 = new int[10] {1,2,3,4,5};
后面的空间默认初始化为零
- 尽管new和delete提供了对象构造和析构的自动管理,但程序员仍然需要负责确保每个用new分配的内存都被对应的delete释放,以避免内存泄露
- 与malloc和free一样,试图delete一个未经new分配的指针,或者对同一个指针执行多次delete,都是未定义行为,并且可能导致程序崩溃
- 当使用new[]分配数组时,必须使用对应的delete[]来释放内存。使用错误的delete形式也是未定义行为
来看下面的代码:
struct ListNode { ListNode* _next; int _val; ListNode(int val) :_next(nullptr) ,_val(val) {} }; struct ListNode* CreateListNode(int val) { struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode)); if (newnode == NULL) { perror("malloc fail"); return NULL; } newnode->_next = NULL; newnode->_val = val; return newnode; }
这是c语言构造一个节点并完成初始化的过程,我们来看c++的实现:
int main() { ListNode* node1 = new ListNode(1); return 0; }
这行代码自动为ListNode对象分配了内存,并调用了其构造函数进行初始化。这种方式更简洁,也更安全,因为它保证了对象在使用前被正确初始化,注意这里ListNode是自定义类型,除了开空间还会调用构造函数
只要我们写好构造函数,我们发现new的使用是十分方便的
我们来构建一个链表:
ListNode* CreateList(int n) { ListNode head(-1); // 哨兵位 ListNode* tail = &head; int val; printf("请依次输入%d个节点的值:>", n); for (size_t i = 0; i > val; tail->_next = new ListNode(val); tail = tail->_next; } return head._next; }
我们输入五个值,1 2 3 4 5
哨兵节点:ListNode head(-1);这行代码创建了一个局部的哨兵节点,它的值被设为-1(这个值通常是任意的,因为哨兵节点本身不存储任何有意义的数据)。哨兵节点的主要目的是简化在链表头部的插入和删除操作,因为你总是有一个非空的节点作为链表的起始点,从而避免了处理空链表的特殊情况
最后,函数通过return head._next;返回新构建链表的头节点。由于head是一个哨兵节点,它的_next成员实际上指向链表的第一个真实节点(如果有的话),或者是nullptr(如果n为0或用户没有输入任何有效数据)
3.1.1抛异常
我们不用手动检查new是否开辟成功,new失败了会抛出异常
void func() { int n = 1; while (1) { int* p = new int[1024 * 1024*100]; cout int n = 1; while (1) { //int* p = new int[1024 * 1024 * 100]; int* p = (int*)malloc(1024 * 1024 * 400); cout func(); } catch (const exception& e) { cout public: A(int a = 0) : _a(a) { cout cout A* p1 = new A(1); delete p1; return 0; } void* p; while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); } _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; } public: A(int a = 0) : _a(a) { cout cout A* p1 = new A(1); delete p1; return 0; } public: Stack() { _a = (int*)malloc(sizeof(int) * 4); _top = 0; _capacity = 4; } ~Stack() { free(_a); _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack* pst = new Stack; delete pst; return 0; } public: A(int a = 0) : _a(a) { cout cout A* p1 = new A; A* p2 = new A[10]; delete p1; delete[]p2; return 0; } public: A(int a = 0) : _a(a) { cout cout int* p1 = new int[10]; return 0; }
-