C/C++内存管理

C/C++内存分布

我们先来复习一下C/C++的内存分布

数据的存储和用途分类

在C/C++程序中,从数据的存储和用途的角度来看,程序中的数据主要分为以下几类:

  1. 局部数据
    • 定义在函数内部或代码块内部的数据。
    • 每次函数调用时创建,函数返回时销毁。
  2. 静态数据
    • 使用static关键字修饰的数据。
    • 只在一个局部域(如函数内部)或当前文件内使用,生命周期贯穿整个程序运行期间。
  3. 全局数据
    • 定义在所有函数外部的数据。
    • 在整个程序中都可以访问。
  4. 常量数据
    • 使用const关键字修饰的数据。
    • 在程序运行期间值不变。
  5. 动态申请的数据
    • 使用动态内存分配函数(如malloccallocnew等)申请的数据。
    • 需要程序员手动管理内存(分配和释放)。

程序的存储与运行

  • 程序存储
    • 我们平时写的代码以.c.cpp等源文件形式存在磁盘上。
    • 编译后生成.exe可执行程序文件,该文件也存储在磁盘上。
  • 程序运行
    • 将可执行文件加载到内存中,以进程的形式在操作系统上运行。

虚拟进程地址空间划分

当程序运行起来后,操作系统会为其分配一个虚拟进程地址空间,该空间划分为以下几个区域:

  1. 又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。(注意:栈是自动管理的,不会出现空间碎片的情况。栈可以通过函数_alloca进行动态分配,但是所分配的空间不能通过frew或delete释放。)
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
  3. 用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段--存储全局数据和静态数据。
  5. 代码段--可执行的代码/只读常量。

C/C++动态内存管理方式

回顾:C语言中动态内存管理方式 

我们先来看一段代码:

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(p3 );
}

我们来回顾一下:

1.malloc/calloc/realloc的区别是什么?

malloc

  • 用于分配指定字节数的内存空间。
  • 内存空间的内容是未初始化的(即内存中的值是不确定的)。

calloc

  • 用于分配指定数量(num)的元素,每个元素的大小为size字节的内存空间。
  • 内存空间的内容被初始化为零。
  • 实际上,calloc(4, sizeof(int))等价于malloc(4 * sizeof(int)),但前者会将分配的内存全部初始化为0。

realloc

  • 用于调整之前通过malloccalloc分配的内存块的大小。
  • 如果新大小大于旧大小,则扩展内存区域(可能会也可能不会移动内存块的位置)。
  • 如果新大小小于旧大小,则收缩内存区域,并可能释放多余的空间。
  • 注意:如果realloc失败,它将返回NULL,并且原来的内存块(ptr指向的)仍然有效。因此,在使用realloc时,通常的做法是将原始指针保存在一个临时变量中,以便在realloc失败时能够释放它。

2.这里需要free(p2)吗?

p2是通过calloc分配的内存,然后p2被传递给realloc以调整大小。在这种情况下,你不需要(也不应该)在调用realloc之后立即释放p2,因为realloc可能会返回p2指向的同一个内存地址(如果内存块没有移动的话),或者它可能会返回一个新的地址(如果内存块被移动了)。无论哪种情况,你都应该使用realloc返回的指针(p3)来访问重新分配的内存,并在不再需要时释放它。

C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因

此C++又提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理

1.new/delete操作内置类型

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

    delete ptr4;
    delete ptr5;
    delete[] ptr6;
}

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

2.newdelete操作自定义类型

注意:在申请自定义类型的空间时,new和delete会调用构造函数初始化、析构函数清理,而mallocfree不会

class A{
public:
    A(int a = 0): _a(a){
        cout << "A():" << this << endl;
    }

    ~A(){
        cout << "~A():" << this << endl;
    }

private:
    int _a;
};

int main() {

    cout << "new / delete 和 malloc / free最大区别是 new / delete对于自定义类型除了开空间," << endl;
    cout << "还会调用构造函数和析构函数:" << endl;
    A* p1 = (A*)malloc(sizeof(A)); // C
    A* p2 = new A(1);
    free(p1);
    delete p2;

    cout << "内置类型是几乎是一样的!" << endl;
    int* p3 = (int*)malloc(sizeof(int)); // C
    int* p4 = new int;
    free(p3);
    delete p4;

    cout << "申请开辟连续的空间:" << endl;
    A* p5 = (A*)malloc(sizeof(A) * 10); // C
    A* p6 = new A[10];
    free(p5);
    delete[] p6;

    return 0;
}

输出:

接下来,我们来看一下C语言版本的申请链表节点和C++版本的申请链表节点的代码:

C语言版本的:

struct ListNode
{
    int _val;
    struct ListNode* _next;
};

struct ListNode* BuyListNode(int x)
{
    // 单纯开空间
    struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
    // 检查
    newnode->_next = NULL;
    newnode->_val = x;

    return newnode;
}

int main()
{
    struct ListNode* n1 = BuyListNode(1);
    struct ListNode* n2 = BuyListNode(2);
    struct ListNode* n3 = BuyListNode(3);

    return 0;
}

C++版本的:

struct ListNode
{
    int _val;
    struct ListNode* _next;

    ListNode(int x)
        :_val(x)
        ,_next(NULL)
    {}
};

int main()
{
    // 开空间+调用构造函数初始化
    ListNode* nn1 = new ListNode(1);
    ListNode* nn2 = new ListNode(2);
    ListNode* nn3 = new ListNode(3);

    return 0;
}

我们发现,C++版本的确实方便和简洁很多。

总结:malloc/freenew/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在释放空间前会调用析构函数完成空间中资源的清理

new和delete实现原理

1.operator newoperator delete函数(重点)

newdelete是用户进行动态内存申请和释放的操作符operator new operator delete是系统提供的全局函数new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。它们与C语言中的 malloc 和 free 类似,但有一些重要的区别,它们并不是让我们直接去使用的。

/*
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);
}

/*
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)

2.newdelete的实现原理 

(1)内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete
申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

(2)自定义类型

讲解

我们前面说过,malloc主要做的事是开空间,new在开空间的基础上还要调用构造函数。在处理开空间失败的情况,而面向对象语言处理失败不喜欢用返回值,而是使用抛异常(通过上面的代码可见),我们new一个对象的时候,它首先调用 operator new 来分配内存,这背后会调用 malloc,然后调用对象的构造函数来初始化该内存区域中的对象。当使用 delete 释放一个对象时,它首先调用对象的析构函数来清理对象占用的资源,然后调用 operator delete 来释放内存,这背后会调用 free 。

例子

接下来,我们通过一个Stack的部分实现代码来理解一下new和delete的实现原理:

class Stack
{
public:
    Stack(size_t capacity = 3)
    {
        cout << "Stack(size_t capacity = 3)" << endl;

        _array = (DataType*)malloc(sizeof(DataType) * capacity);
        if (NULL == _array)
        {
            perror("malloc申请空间失败!!!");
            return;
        }

        _capacity = capacity;
        _size = 0;
    }

    void Push(DataType data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }

    // 其他方法...

    ~Stack()
    {
        cout << "~Stack()" << endl;

        if (_array)
        {
            free(_array);
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
    }

private:
    DataType* _array;
    int       _capacity;
    int       _size;
};

int main()
{
    // 需要申请一个堆上的栈对象
    Stack* p1 = new Stack;
    delete p1;
 
    return 0;
}

使用 new 时:

当执行 Stack* p1 = new Stack; 时,涉及以下步骤和函数调用:

1.operator new:

  • 作用:分配足够的内存空间以存储 Stack 对象。
  • 执行顺序:首先被调用。
  • 作用对象:为 Stack 对象分配内存。
  • 注意:operator new 最终会调用 malloc 来分配内存,但这一步在标准C++中通常是隐藏的,用户代码不会直接调用 malloc。然而,由于 Stack 的构造函数中显式调用了 malloc,这里区分了构造函数内部的 malloc 和 operator new 调用的内存分配。

2.构造函数 Stack(size_t capacity = 3):

  • 作用:初始化 Stack 对象。
  • 执行顺序:在 operator new 之后调用。
  • 作用对象:对分配的内存进行初始化,设置 _array、_capacity 和 _size 成员变量。
  • 注意:构造函数内部调用了 malloc 来为 _array 分配内存。

使用 delete 时:

当执行 delete p1; 时,涉及以下步骤和函数调用:

1.析构函数 ~Stack():

  • 作用:清理 Stack 对象,释放资源。
  • 执行顺序:首先被调用。
  • 作用对象:释放 _array 指向的内存,并重置 _array、_capacity 和 _size 成员变量。

2.operator delete:

  • 作用:释放 Stack 对象所占用的内存。
  • 执行顺序:在析构函数之后调用。
  • 作用对象:释放 Stack 对象所占用的内存空间。
  • 注意:operator delete 最终会调用 free 来释放内存,但这一步在标准C++中通常是隐藏的。由于 Stack 的析构函数已经显式调用了 free 来释放 _array,这里的 operator delete 负责释放整个 Stack 对象所占用的内存(即 operator new 分配的内存)。

定位new表达式(placement-new)(了解)

作用

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象,通常在显示调用构造函数和析构函数中应用的比较多。

使用格式

new (place_address) type;
//或者
new (place_address) type(initializer-list);
//place_address必须是一个指针,initializer-list是类型的初始化列表

应用场景:池化技术

池化技术概述

池化技术是一种资源管理技术,用于提高资源使用的效率和性能。常见的池化技术包括内存池、线程池、连接池等。

内存池与定位new的应用

我们会以内存池为例进行讲解:

内存池主要是用于管理内存资源的分配和释放,以减少频繁向操作系统申请和释放内存带来的开销。想象这样一个场景,如果现在有一个应用,需要频繁地申请和释放内存,这会十分麻烦。为了解决这种麻烦,我们可以提前申请一个比较大的内存,将这个比较大的内存放在内存池,我们在申请内存的时候就不用找堆,而是找内存池要内存,这样效率会高些(补充一句:内存池中的内存用完了,还是会找堆要内存)。由于内存池中的内存不是通过new操作符分配的,因此不会自动调用构造函数进行初始化。同样地,释放内存时也不会自动调用析构函数。为了解决这个问题,我们可以使用定位new来在内存池中分配对象,并显式调用析构函数来销毁对象。

#include <iostream>
using namespace std;

class MyClass {
public:
    int value;

    // 默认构造函数
    MyClass() : value(0) {
        cout << "Default constructor called, value = " << value << endl;
    }

    // 带参数的构造函数
    MyClass(int v) : value(v) {
        cout << "Parameterized constructor called, value = " << value << endl;
    }

    // 析构函数
    ~MyClass() {
        cout << "Destructor called, value = " << value << endl;
    }
};

int main() {
    // 分配一块未初始化的内存(假设足够大以容纳MyClass对象)
    char buffer[sizeof(MyClass)];

    // 使用定位new在buffer上调用默认构造函数
    MyClass* obj1 = new (buffer) MyClass();

    // 使用定位new在buffer上调用带参数的构造函数
    // 注意:这里需要重新分配一块未初始化的内存,因为buffer已经被obj1使用了
    // 在实际使用中,你可能会维护一个内存池来管理这些内存块
    char buffer2[sizeof(MyClass)];
    MyClass* obj2 = new (buffer2) MyClass(42);

    // ... 使用obj1和obj2 ...

    // 显式调用析构函数
    obj1->~MyClass();
    obj2->~MyClass();

    // 注意:这里没有使用delete,因为内存是通过定位new在预先分配的内存上分配的
    // 也不应该使用delete,因为这会导致未定义行为(尝试释放非堆内存)

    return 0;
}

内存泄漏

什么是内存泄漏

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而
造成了内存的浪费。

内存泄漏的危害

在我们平时写代码时,如果我们申请了内存却不去释放它,进程正常结束后操作系统会帮我们释放资源。但是,在服务器上的服务程序,如果不去释放申请的内存会有很大的危害,因为服务器的服务程序是长期运行的(例如:游戏服务、电商服务……),它们在非升级期间是不能停机的。最怕的是每天泄露一点点内存,这不会影响平时的正常使用,但是用久了服务器就会十分卡。

所以,长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会

导致响应越来越慢,最终卡死。

如何避免内存泄漏

  1. 事前预防型。如智能指针等。
  2. 事后查错型。如泄漏检测工具。

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值