欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > C/C++内存管理

C/C++内存管理

2025/2/26 6:33:01 来源:https://blog.csdn.net/bbppooi/article/details/145777169  浏览:    关键词:C/C++内存管理

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在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?____
*pChar3在哪里?____
ptr1在哪里?____
*ptr1在哪里?____
  • globalVar:这是一个全局变量,且没有使用 static 修饰。全局变量存储在数据段(静态区)
    答案:C

  • staticGlobalVar:这是一个静态全局变量,存储在数据段(静态区),因为静态全局变量不属于某个函数的局部作用域,且在程序执行期间会一直存在。
    答案:C

  • staticVar:这是一个函数内部的静态局部变量,存储在数据段(静态区),因为静态局部变量的生命周期是整个程序运行期间,但作用域仅限于所在的函数。
    答案:C

  • localVar:这是一个普通的局部变量,存储在上,局部变量的生命周期只在函数调用期间有效。
    答案:A

  • num1:这是一个局部的数组,存储在上。数组在栈上分配空间,其生命周期与函数调用的作用域一致。
    答案:A

  • char2:这是一个字符数组,存储在上。虽然它初始化了字符串,但它本身是一个局部数组。
    答案:A

  • *char2: 是一个字符数组 char2 中的元素,它表示指向 char2 数组的第一个字符的指针。char2 在栈上,而 *char2 访问的是这个数组的第一个字符,因此它的存储位置是

    答案:A(栈)

  • pChar3:这是一个指针,它本身存储在上,指向字符串常量 "abcd",而字符串常量存储在代码段(常量区)
    答案:A(指针 pChar3 存储在栈上)
    答案:D"abcd" 存储在代码段)

  • ptr1ptr2ptr3:这三个指针都通过 malloccallocrealloc上分配内存,因此它们指向的内存区域是在堆上。
    答案:Bptr1ptr2ptr3 的内存分配是在堆上)
    答案:B*ptr1*ptr2 指向的内容在堆上)

由此引入

内存段及其用途

在程序执行时,操作系统会为程序分配不同的内存区域,这些区域被称为内存段。不同的内存段有不同的作用和特性。以下是常见的内存段及其功能:

1. 栈(Stack)
  • 定义:栈用于存储函数的局部变量、函数参数和返回值等。它的内存分配是由系统自动管理的。当函数被调用时,相应的局部变量会被推入栈中;当函数返回时,栈会释放这些变量。

  • 特点

    • 自动管理:栈内存由编译器自动分配和释放,无需程序员干预。
    • 内存分配与释放速度快:栈的内存分配和释放是按照“先进后出”的顺序进行的,这使得栈操作非常高效。
    • 栈的增长方向:栈是向下增长的,即每次分配新的内存时,会从更低的地址开始。
2. 内存映射段(Memory Mapped Segment)
  • 定义:内存映射段是用来存放共享的动态库或者文件映射内存的。它用于高效的 I/O 映射,允许多个进程共享同一块内存区域,从而进行进程间通信。

  • 特点

    • 共享内存:多个进程可以通过映射相同的内存区域,达到共享数据的目的。
    • 进程间通信:在没有显式传输数据的情况下,不同的进程可以直接通过内存共享区域进行数据交换。
3. 堆(Heap)
  • 定义:堆用于程序运行时动态内存的分配。堆的内存分配是由程序员手动控制的,需要使用 newmalloc 等操作来分配内存,而释放内存则使用 deletefree

  • 特点

    • 手动管理:堆内存需要程序员明确申请和释放,如果不释放内存则可能导致内存泄漏。
    • 堆的增长方向:堆是向上增长的,即每次分配新的内存时,会从更高的地址开始。
4. 数据段(Data Segment)
  • 定义:数据段用于存储全局变量和静态变量。它包括已初始化的数据和未初始化的数据。

  • 特点

    • 已初始化数据:存储程序中定义并初始化的全局变量和静态变量。
    • 未初始化数据:通常存储 staticglobal 类型的变量,它们的初始值是零。

5. 代码段(Code Segment)
  • 定义:代码段存储程序的机器代码,即程序的可执行指令和常量数据。代码段是只读的,避免程序修改自己的代码。常量(如字符串常量)也通常存储在代码段中。

  • 特点

    • 只读:为了避免程序修改自己的代码,代码段通常是只读的。
    • 存储执行指令:包含程序中所有的执行指令。

2.C语言中动态内存管理方式:malloc/calloc/realloc/free 

在C语言中,动态内存管理用于在程序运行时分配和释放内存。主要有四个函数:

  • malloc (memory allocation):分配指定大小的内存块,返回指向该内存块的指针,未初始化,可能包含垃圾值。

void* malloc(size_t size);
  •  calloc (contiguous allocation):分配指定数量的内存块,并将其初始化为零。它是 malloc 的扩展,通常用于数组分配。
void* calloc(size_t num, size_t size);
  • realloc (reallocation):重新调整之前分配的内存块大小。如果扩展内存,则可能会移动原来的内存块到新地址,并返回新地址。
void* realloc(void* ptr, size_t size);
  • free (free memory):释放之前通过 malloccallocrealloc 分配的内存,避免内存泄漏。
void free(void* ptr);

简而言之:

  • malloccalloc 用于分配内存,calloc 会初始化为零。
  • realloc 用于改变已分配内存的大小。
  • free 用于释放内存。

3. C++内存管理方式 

C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
C++ 又提出了自己的内存管理方式: 通过 new delete 操作符进行动态内存管理

3.1 new/delete操作内置类型

void Test(){// 动态申请一个int类型的空间int* ptr4 = new int;// 动态申请一个int类型的空间并初始化为10int* ptr5 = new int(10);// 动态申请10个int类型的空间int* ptr6 = new int[3];delete ptr4;delete ptr5;delete[] ptr6;
}

 

注意:申请和释放单个元素的空间,使用 new delete 操作符,申请和释放连续的空间,使用
new[] delete[] ,注意:匹配起来使用。

3.2 newdelete操作自定义类型

#include <iostream>
using namespace std;class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;
};int main()
{// new/delete 和 malloc/free 最大区别是 new/delete 对于【自定义类型】除了开空间// 还会调用构造函数和析构函数// 使用 malloc 分配内存并强制转换为 A* 类型A* p1 = (A*)malloc(sizeof(A));// 使用 new 调用 A 的构造函数A* p2 = new A(1);// 释放使用 malloc 分配的内存,不会调用析构函数free(p1);// 释放使用 new 分配的内存,会调用析构函数delete p2;// 内置类型的 malloc 和 new 几乎是一样的int* p3 = (int*)malloc(sizeof(int)); // 使用 malloc 分配内存int* p4 = new int; // 使用 new 分配内存// 释放内存,free 和 delete 区别同样存在free(p3);delete p4;// 使用 malloc 分配多个 A 类型对象的内存A* p5 = (A*)malloc(sizeof(A) * 10);// 使用 new 分配多个 A 类型对象的内存A* p6 = new A[10];// 释放内存free(p5);delete[] p6;return 0;
}
注意:在申请自定义类型的空间时, new 会调用构造函数, delete 会调用析构函数,而 malloc
free 不会

4. operator newoperator delete函数

4.1 operator newoperator delete函数

new delete 是用户进行 动态内存申请和释放的操作符 operator new operator delete
系统提供的 全局函数 new 在底层调用 operator new 全局函数来申请空间, delete 在底层通过
operator delete 全局函数来释放空间。
/*
operator new:该函数实际通过 malloc 来申请空间,当 malloc 申请空间成功时直接返回;
申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,
否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid *p;// 循环申请空间,直到成功或无法分配while ((p = malloc(size)) == 0){// 如果 malloc 失败,再尝试用户设置的应对措施if (_callnewh(size) == 0){// 如果申请内存失败了,抛出 bad_alloc 异常static const std::bad_alloc nomem;_RAISE(nomem);}}return p;
}/*
operator delete: 该函数最终是通过 free 来释放空间的
*/
void operator delete(void *pUserData)
{_CrtMemBlockHeader *pHead;// 调用用户定义的回调函数RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));// 如果传入指针为 NULL,直接返回if (pUserData == NULL)return;_mlock(_HEAP_LOCK); /* 阻塞其他线程 */__TRY/* 获取内存块头指针 */pHead = pHdr(pUserData);/* 验证块类型 */_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));/* 释放内存块 */_free_dbg(pUserData, pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK); /* 释放其他线程 */__END_TRY_FINALLYreturn;
}/*
free 的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
  • operator new:这个版本的 operator new 使用 malloc 来申请内存。如果 malloc 申请失败,它会尝试调用用户自定义的内存分配处理函数(_callnewh)。如果该处理函数也无法解决问题,则抛出 std::bad_alloc 异常。

  • operator delete:该函数通过 _free_dbg 来释放内存,并且在释放之前,会获取内存块的头指针,验证内存块的类型,确保其有效性。_mlock_munlock 用于保证在多线程环境下操作的原子性,避免多个线程同时释放内存。

  • free:重定义了 free_free_dbg,用于调试模式下释放内存,提供更多的调试信息。

通过上述两个全局函数的实现知道, operator new 实际也是通过 malloc 来申请空间 ,如果
malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施
就继续申请,否则就抛异常。 operator delete 最终是通过 free 来释放空间的

5. newdelete的实现原理

5.1 内置类型

如果申请的是内置类型的空间,newmallocdeletefree 基本类似,不同的地方是:

  • new/delete 申请和释放的是单个元素的空间,new[]delete[] 申请的是连续空间。而 new 在申请空间失败时会抛出异常(std::bad_alloc),malloc 只会返回 NULL

 例如

int* p1 = (int*)malloc(sizeof(int)); // C
int* p2 = new int; // C++
free(p1);  // C
delete p2; // C++

5.2 自定义类型

new的原理
  1. 申请空间:调用 operator new 来申请内存,实际过程是通过 malloc 或平台特定的内存分配机制分配空间。
  2. 构造对象:在已分配的内存空间上调用构造函数,完成对象的初始化。只有这样,对象的成员变量才会被正确初始化。
    void* operator new(size_t size) {void* p = malloc(size);if (!p) throw std::bad_alloc();return p;
    }
    
delete的原理
  1. 析构对象:在对象被销毁时,首先会调用析构函数,清理对象的资源(例如释放内存、关闭文件等)。
  2. 释放内存:调用 operator delete 来释放内存空间。这里的 operator delete 通常通过 free 来释放内存。
    void operator delete(void* p) {if (p) {free(p); // 释放内存}
    }
    
new[]的原理

new[] 用来分配数组类型的内存:

  1. 申请空间:调用 operator new[] 来分配一个连续的内存块。实际上,它会多分配一些额外空间,用来存储数组的大小信息(此过程由编译器实现)。
  2. 构造对象:在分配的空间上,调用每个元素的构造函数。这样,数组中的每个对象都被正确地初始化。
    void* operator new[](size_t size) {void* p = malloc(size);if (!p) throw std::bad_alloc();return p;
    }
    
delete[]的原理

delete[] 用来释放数组类型的内存:

  1. 析构对象:在释放内存之前,会先调用数组中每个对象的析构函数。注意,析构函数会被调用 N 次,其中 N 是数组的大小。
  2. 释放内存:调用 operator delete[] 来释放内存。这实际上会调用 operator delete 来释放内存。
    void operator delete[](void* p) {if (p) {free(p); // 释放内存}
    }
    

5.3 内存管理与异常处理

  • 异常处理: 如果在调用 new 进行内存分配时发生了异常(如 std::bad_alloc),内存分配失败将导致程序终止。如果使用 new[],每个数组元素的构造函数也有可能抛出异常,因此需要合理地处理异常,确保资源不会泄漏。

  • 内存泄漏: 如果 new 分配的内存没有正确的释放,程序会发生内存泄漏。为了避免这种情况,可以使用智能指针(如 std::unique_ptrstd::shared_ptr)来自动管理资源。

  • 内存对齐: newdelete 运算符通常会处理内存对齐问题(如 8 字节对齐)。而 mallocfree 在一些平台上可能需要特别的对齐处理。

6. 定位new表达式(placement-new) 

定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。其格式如下:
new (place_address) type;

 或者:

new (place_address) type(initializer-list);
  • place_address 必须是一个指针,指向已分配的内存区域。
  • initializer-list 是类型的初始化列表,用于给构造函数传递参数。

使用场景:

定位 new 表达式在实际中一般配合 内存池 使用。内存池分配出的内存没有初始化,如果要在其上创建自定义类型的对象,必须显式调用构造函数,因此可以使用 placement-new 来初始化对象。

#include <iostream>
using namespace std;class A {
public:A(int a) : _a(a) {cout << "A constructed with " << _a << endl;}~A() {cout << "A destructed" << endl;}
private:int _a;
};int main() {// 手动分配内存空间void* rawMemory = operator new(sizeof(A));// 使用 placement-new 在已分配的内存上构造对象A* p = new (rawMemory) A(10);// 手动调用析构函数并释放内存p->~A();operator delete(rawMemory);return 0;
}

  • 定位 new 与常规 new 的区别: 定位 new 只是将对象构造在已经分配的内存上,不负责分配内存空间;而常规的 new 会先分配内存然后再调用构造函数。
  • 内存池配合使用: 在内存池的管理中,通常我们会预先分配一定的内存块,这些内存块可能并不包含对象的构造。此时需要使用定位 new 来显式地构造对象,而不是通过 new 来直接申请内存。
  • 手动管理内存: 使用定位 new 时,内存管理更加精细。对象的析构必须通过显式调用析构函数 p->~A(),然后再手动调用 operator delete 来释放内存空间。

注意事项:

  1. 内存空间已经分配: 使用定位 new 时,目标内存空间必须已经分配好,并且足够容纳对象的内存,否则会导致未定义行为。
  2. 没有自动调用析构函数: 定位 new 不会自动调用析构函数,因此在销毁对象之前需要手动调用析构函数。
  3. 适用场景: 定位 new 主要用于特定场景,如自定义内存池、优化内存管理、构造大型对象时需要复用内存块等。
#include <iostream>
using namespace std;class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}private:int _a;
};// 定位new/replacement new
int main()
{// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行A* p1 = (A*)malloc(sizeof(A));  // malloc分配内存new(p1) A;  // 在p1指向的内存上调用构造函数A()p1->~A();  // 手动调用析构函数free(p1);  // 释放内存A* p2 = (A*)operator new(sizeof(A));  // 使用operator new分配内存new(p2) A(10);  // 在p2指向的内存上调用带参数的构造函数A(10)p2->~A();  // 手动调用析构函数operator delete(p2);  // 释放内存return 0;
}
  • malloc(sizeof(A))operator new(sizeof(A))

    • 这两者都分配了足够的内存空间,但并不会调用 A 类的构造函数。
  • new(p1) Anew(p2) A(10)

    • 这些语句在 p1p2 指向的内存空间中调用了构造函数。
    • p1 上调用的是默认构造函数,而在 p2 上调用的是带参数的构造函数 A(10)
  • p1->~A()p2->~A()

    • 这些语句手动调用析构函数,释放 p1p2 上对象的资源。
  • free(p1)operator delete(p2)

    • free 用来释放 malloc 分配的内存,operator delete 用来释放 operator new 分配的内存。

其中 0x123456780x23456789 会是实际的内存地址。 

7. malloc/freenew/delete 的区别

共同点:
  • 都是从堆上申请内存,并且都需要手动释放内存。
区别:
  1. mallocfree 是函数,newdelete 是操作符

    • mallocfree 是库函数,需要包含 <cstdlib> 头文件。
    • newdelete 是 C++ 中的操作符,不需要头文件,且具有更强的类型安全性。
  2. malloc 申请的空间不会初始化,new 会初始化

    • malloc 分配的内存是未初始化的(内容是随机的)。
    • new 会初始化内存,对于内置类型会将其默认初始化为零,对于类类型会调用构造函数。
  3. malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型

    • malloc 需要传递内存大小(sizeof)。例如,malloc(sizeof(A)) 申请一个 A 类型对象的空间。
    • new 自动计算所需内存大小。例如,new A() 会分配一个 A 类型对象所需的空间。
  4. malloc 的返回值为 void*,在使用时必须强制类型转换,new 返回指定类型的指针

    • malloc 返回 void*,必须显式地强制转换为目标类型指针。
    • new 返回的是具体类型的指针,无需强制类型转换,确保类型安全。
  5. malloc 申请空间失败时返回 NULLnew 会抛出异常

    • malloc 如果无法分配内存,会返回 NULL,因此需要手动检查返回值。
    • new 如果无法分配内存,会抛出 std::bad_alloc 异常,除非在 new 后加上 nothrow,否则默认会抛异常。
  6. malloc/free 只会分配和释放内存,不会调用构造和析构函数,new/delete 会调用构造和析构函数

    • mallocfree 只进行内存的分配和释放,不会处理对象的构造和析构。
    • new 会调用类的构造函数初始化对象,delete 会调用类的析构函数,释放对象时清理资源。
  7. mallocfree 是 C 语言中的内存管理函数,newdelete 是 C++ 提供的内存管理操作符

    • mallocfree 是为 C 语言设计的,并且支持 C 风格的内存管理。
    • newdelete 是 C++ 中的一部分,能够更好地与面向对象编程结合,支持对象的构造和析构。
  8. 内存分配的灵活性

    • mallocfree 只负责内存的分配和释放,不会涉及到对象的生命周期管理,适用于简单的内存操作。
    • newdelete 可以与 C++ 中的对象模型紧密结合,不仅负责内存分配,还能确保在构造和析构期间正确管理资源,适用于复杂的对象生命周期。
  9. 多重分配

    • new[]delete[] 用于处理数组类型的内存分配和释放,能够调用每个对象的构造函数和析构函数,而 mallocfree 不能直接处理对象数组的构造和析构。

进一步补充:

  • malloc/free 无法处理 C++ 类中重载的 newdelete 操作符,而 new/delete 支持自定义内存管理(重载 newdelete)。

  • 在使用 mallocfree 时,如果程序员没有注意类型匹配或内存大小,容易导致潜在的内存错误或未定义行为。而 new/delete 操作符更能提供类型安全,且会自动处理内存的分配与清理过程。

  • mallocfree 仅在 C 语言和一些老的 C++ 代码中使用较多,C++ 现代编程中推荐使用 newdelete,因其与 C++ 面向对象特性更兼容。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词