ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++的内存管理

C++的内存管理 内存分布高地址 ↑ ├─────────────┤ │ 栈 │ ← 向下增长局部变量、函数参数 ├─────────────┤ │ │ │ │ ↓ │ ├─────────────┤ │ 内存映射段 │ ← 共享库、共享内存 ├─────────────┤ │ ↑ │ │ │ │ ├─────────────┤ │ 堆 │ ← 向上增长动态分配 ├─────────────┤ │ 数据段 │ ← 全局变量、静态变量 ├─────────────┤ │ 代码段 │ ← 可执行代码、常量 └─────────────┘ 低地址 includeiostream using namespace std; 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); // 注意ptr2被realloc处理了不需要再free } int main() { Test(); return 0; }说明栈又叫做堆栈——存储非静态局部变量/函数参数/返回值等向下增长内存映射段是高效的I/O映射方式用于装载一个共享的动态内存库。堆是用于内存的动态内存分配向上增长系统一般给的内存都比较大数据段用于存储全局变量和静态变量代码段用于可执行的代码/只读常量动态内存管理malooc/calloc/realloc的区别#includeiostream using namespace std; void Test() { // 1. malloc - 分配未初始化的内存 int* p1 (int*)malloc(sizeof(int) * 4); if (p1 NULL) { perror(malloc fail); return; } // 2. calloc - 分配并初始化为0 int* p2 (int*)calloc(4, sizeof(int)); // calloc(4, sizeof(int)) 等价于 malloc(4 * sizeof(int)) 内存置0 // 3. realloc - 调整已分配内存的大小 int* p3 (int*)realloc(p2, sizeof(int) * 10); // 注意如果realloc成功p2会被自动处理不需要free(p2) if (p3 NULL) { free(p2); // realloc失败时原内存不变需要手动释放 return; } // 释放内存 free(p1); free(p3); // p2已经被realloc处理不需要单独free }内存管理方式C语言的内存管理方式在C也可以使用但是也有一些欠缺点用起来比较麻烦。因此C有自己新的内存管理方式——通过操作符new和delete用法讲解#includeiostream using namespace std; int main() { // 1. 基本用法申请和释放单个对象 int* p1 new int; // 申请一个 int未初始化 int* p2 new int[10]; // 申请 10 个 int 的数组未初始化 delete p1; // 释放单个对象 delete[] p2; // 释放数组 // 2. 初始化用法申请时初始化 int* p3 new int(0); // 申请一个 int初始化为 0 int* p4 new int[10]{ 0 }; // 申请 10 个 int全部初始化为 0 int* p5 new int[10]{ 1, 2, 3, 4, 5 }; // 前5个初始化后面默认为0 delete p3; // 释放单个对象 delete[] p4; // 释放数组 delete[] p5; // 释放数组 return 0; }注意new 和 delete 必须配对使用new - deletenew[] - delete []应用示例struct ListNode { int val; ListNode* next; ListNode(int x) : val(x) , next(nullptr) {} }; int main() { // 1. 自定义类型 A 的 new/delete假设 A 存在 // A* p1 new A; // 调用默认构造 // A* p2 new A(1); // 调用带参构造 // delete p1; // delete p2; // 2. 创建链表节点 ListNode* n1 new ListNode(1); ListNode* n2 new ListNode(2); ListNode* n3 new ListNode(3); ListNode* n4 new ListNode(4); // 3. 连接节点形成链表 n1-next n2; n2-next n3; n3-next n4; n4-next nullptr; // 尾节点指向 nullptr已经由构造函数设置 // 4. 遍历链表 ListNode* cur n1; while (cur ! nullptr) { cout cur-val - ; cur cur-next; } cout nullptr endl; // 5. 释放链表内存重要 cur n1; while (cur ! nullptr) { ListNode* next cur-next; // 保存下一个节点 delete cur; // 释放当前节点 cur next; // 移动到下一个 } return 0; }可以看到写链表的时候可以直接传参初始化不需要写Init。从应用层面我们可以看到创造new和delete的原因是为了给自定义类型使用的因为自定义类型往往都需要调用默认构造函数更为方便对应内置类型和malloc和free的区别不大自定义类型int main() { // 1. 单个对象使用 new 申请并构造 A* p1 new A(1); // 调用 A(int) 构造函数 A* p2 new A(2, 2); // 调用 A(int, int) 构造函数 // 2. 对象数组使用已有对象拷贝构造 A aa1(1, 1); A aa2(2, 2); A aa3(3, 3); A* p3 new A[3]{ aa1, aa2, aa3 }; // 调用拷贝构造 // 3. 对象数组使用临时对象 A* p4 new A[3]{ A(1, 1), A(2, 2), A(3, 3) }; // 4. 对象数组使用聚合初始化C11 列表初始化 A* p5 new A[3]{ {1, 1}, {2, 2}, {3, 3} }; // 释放内存 delete p1; delete p2; delete[] p3; delete[] p4; delete[] p5; return 0; }具体用法详解对单个对象class A { public: A(int x) : _a(x) { cout A(int) 构造 endl; } A(int x, int y) : _a(x), _b(y) { cout A(int,int) 构造 endl; } ~A() { cout ~A() 析构 endl; } private: int _a; int _b; }; int main() { // 1. 单个对象调用构造函数 A* p1 new A(10); // 调用 A(int) A* p2 new A(20, 30); // 调用 A(int, int) // 2. 释放单个对象调用析构函数 delete p1; // 先调用 ~A()再释放内存 delete p2; return 0; }对象数组int main() { // 方式1默认构造需要 A 有默认构造 A* p1 new A[3]; // 调用 3 次默认构造 // 方式2使用已有对象拷贝构造 A a1(1, 1), a2(2, 2), a3(3, 3); A* p2 new A[3]{ a1, a2, a3 }; // 调用 3 次拷贝构造 // 方式3使用临时对象 A* p3 new A[3]{ A(1, 1), A(2, 2), A(3, 3) }; // 方式4列表初始化C11 A* p4 new A[3]{ {1, 1}, {2, 2}, {3, 3} }; // 释放数组必须用 delete[] delete[] p1; // 先调用 3 次析构再释放内存 delete[] p2; delete[] p3; delete[] p4; return 0; }关键点——new和delete的底层调用的是operatror new和operator deletenew的执行过程A* p new A(1, 2); // 执行步骤 // 1. 调用 operator new 分配内存类似 malloc // 2. 在分配的内存上调用 A 的构造函数 // 3. 返回指向该对象的指针 new A(1, 2) │ ├─ 1. 调用 operator new (全局函数) │ │ │ └─ 调用 malloc() 分配原始内存 │ │ │ ├─ 成功 → 返回内存地址 │ └─ 失败 → 执行用户设置的应对措施 │ └─ 仍失败 → 抛出 bad_alloc 异常 │ ├─ 2. 在分配的内存上调用 A 的构造函数 │ └─ A::A(1, 2) 初始化对象 │ └─ 3. 返回指向构造完成的对象的指针delete的执行过程delete p; // 执行步骤 // 1. 调用 p 指向对象的析构函数 // 2. 调用 operator delete 释放内存类似 free delete p │ ├─ 1. 调用 p 指向对象的析构函数 │ └─ A::~A() 清理对象资源 │ └─ 2. 调用 operator delete (全局函数) │ └─ 调用 free() 释放原始内存new[]和delete[]的执行过程A* p new A[3]{ A(1,1), A(2,2), A(3,3) }; // 执行步骤 // 1. 分配能容纳 3 个 A 对象的内存 额外空间存储数组大小 // 2. 按顺序调用 3 次构造函数 delete[] p; // 执行步骤 // 1. 按逆序调用 3 次析构函数先析构最后一个 // 2. 释放内存注意事项new和delete的配对// 错误 A* p1 new A(1); delete[] p1; // 错误单个对象不能用 delete[] A* p2 new A[3]; delete p2; // 错误数组必须用 delete[] // 正确 A* p1 new A(1); delete p1; A* p2 new A[3]; delete[] p2;自定义类型必须要有正确的构造函数class B { public: B(int x) : _x(x) {} // 只有带参构造没有默认构造 private: int _x; }; int main() { B* p1 new B(10); // 可以 // B* p2 new B; // 错误没有默认构造 // B* p3 new B[3]; // 错误数组需要默认构造 delete p1; return 0; }内存泄漏问题/ 内存泄漏 void BadFunction() { A* p new A(1); // 没有 delete p函数结束导致内存泄漏 } // 正确 void GoodFunction() { A* p new A(1); delete p; } // 更好使用智能指针 #includememory void BestFunction() { unique_ptrA p make_uniqueA(1); // 自动释放 }关于operator new 和 operator deletevoid* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { void* p; // 尝试分配内存 while ((p malloc(size)) 0) { // 如果分配失败执行用户设置的 new_handler if (_callnewh(size) 0) { // 用户没有设置应对措施或措施失败抛出异常 static const std::bad_alloc nomem; _RAISE(nomem); } } return p; } void operator delete(void* pUserData) { if (pUserData NULL) return; // 直接调用 free 释放内存 free(pUserData); }核心newoperator new分配内存构造函数delete析构函数operator delete释放内存new[]operator new[]分配内存N次构造函数delete[]N次析构函数operator delete[]释放内存
返回列表