c和c++中的内存管理

本文详细解释了C/C++中的内存管理,包括内存划分(栈、堆、数据段和代码段),静态分配与动态分配的区别,以及C语言和C++中malloc、free、new和delete的关键作用及其实现原理。同时讨论了new[]和delete[]的匹配使用以及常见问题,如析构函数调用次数的确定和内存操作的正确性。
摘要由CSDN通过智能技术生成


1.内存管理

程序内存划分

在C++中,内存地址空间(在进程层面,体现为进程的虚拟地址空间中的用户空间)被划分为四部分:

  • Stack
  • Heap
  • 数据段 Data Segment
  • 代码段 Code Segment

image-20240329161552901

  1. :从高地址向低地址增长。在程序运行过程中开辟函数栈帧,保存创建的临时变量(函数的非静态局部变量、函数参数、返回值等),当函数执行结束时,这些存储单元自动被释放。详见

  2. :从低地址向高地址增长。用于程序运行时的动态内存分配,在C/C++中,由malloc/new创建的数据对象就保存在堆上,与栈不同的是,堆上开辟的空间必须由程序员自己管理,即用完要调用free/delete释放,否则会造成程序运行过程中的内存泄露(若程序退出,OS回收自动空间了)。

  3. 数据段:数据段保存的是全局数据(global data)和静态数据(static data),在C++中包括已初始化数据和未初始化数据,C语言则有所区分。

  4. 代码段:代码段也可以称为常量数据区,用于保存可执行代码和只读常量。

静态分配和动态分配

内存分配的方式有两种:静态分配和动态分配。

  • 对于堆,堆都是动态分配的,堆上的空间都是在运行时被分配的,需要程序员手动分配和释放。

  • 对于栈,栈可以是静态分配也可以是动态分配,静态分配由编译器完成,如局部变量的创建、函数调用信息、函数栈帧大小;动态分配由alloca函数进行分配,与堆不同的是,栈上动态的分配内存由编译器释放。

如何理解静态分配和动态分配?

静态分配:静态分配是在程序运行前,即编译时的内存分配。程序编译时,编译器为程序员声明的局部变量分配内存空间,具体大小根据变量类型而定。内存单元一旦被静态分配,大小无法再修改。(通俗理解,静态分配只是在编译时确定变量未来存储在内存空间中的地址和大小,形成相应的机器代码,并且在生成的机器代码中使用已知的内存地址来引用这些变量。等到程序加载时,CPU只需根据编译时已确定的地址为变量开空间和赋值,不需要进行额外的内存管理操作)

在C/C++程序的运行过程中,函数栈帧的创建、局部变量的分配一系列对栈空间的分配行为虽然看起来是动态的,但是内存“分配多大”、“分配到哪”都是编译时已经决定了,运行时无法改变,所以栈的分配方式看作是静态分配。

动态分配:动态分配是在程序运行时的内存分配。C库函数例如 calloc()malloc() 或者操作符 new 均支持分配动态内存。动态分配的内存空间,通过这些函数或操作符的返回值赋值给指针变量。


2. C语言的内存控制

C语言的内存动态分配:malloccalloc, realloc等函数分配内存,用free函数释放内存。这是老生常谈了,但是这几天产生一些对于free()函数的疑惑。下面是mallocfree()的函数原型。

void *malloc(size_t size);
void free(void *ptr);

malloc的参数是一个整型size,用于指定开辟空间的大小,这很好理解。但是,free的参数只有一个指针ptr,指向即将释放空间的起始位置,它是如何知道要释放多大的空间呢?经了解,不同的malloc算法实现方式,大概有两种主流的做法:

  1. malloc时,在分配的空间前另外开辟一个整型空间(或者是一个存放内存块信息的结构体),用于保存这块空间的大小,这样一来,free只需回退指针参数ptr,找到要释放的空间大小即可。

  2. 起始地址ptr可以直接通过某种算法转换为空间大小。

参考文章:Understanding glibc malloc


3. C++的内存控制

new和delete

C++中通过new和delete两个关键字进行动态内存管理。

Why

相比于C语言,C++引入了类对象的概念,对象需要在创建时调用构造函数,销毁时调用析构函数,那么C语言中的动态内存管理接口如mallocfree就无法满足这种需求,因为它们只是简单的从堆上开辟空间和释放空间!

new和delete的使用

void test2()
{
    //开辟一个int对象,值为默认(0)
    int* pa1 = new int;

    //开辟一个int对象,指定值为1
    int* pa2 = new int(1);

    //开辟int数组
    int* parr1 = new int[10];
    int* parr2 = new int[5]{1,2,3,4,5};//since c++11
    int* parr3 = new int[5]{1,2,3};since c++11, 后两个数是默认值0

    int* parr4 = new int[0];//可以, 但解引用行为是未定义的
    /*If this argument is zero, the function still returns a distinct non-null pointer 
    on success (although dereferencing this pointer leads to undefined behavior).*/

    //释放空间
    delete pa1;
    delete pa2;

    delete[] parr1;
    delete[] parr2;
    delete[] parr3;
    delete[] parr4;
}

//对于内置类型,new和delete的使用亦是如此,new无非就是开一块空间,然后拿一个值去初始化,delete时就释放空间就行。而对于自定义类型,new除了开辟空间,初始化的操作需要调用类的构造函数,delete释放空间之前也要先调用析构函数。

void test3()
{
    People* p1 = new People;//调用默认构造函数
    People* p2 = new People(18, "Jcole");//传参调用构造函数
    People* p3 = new People[3];
    People* p4 = new People[3]{{10,"Bill"}, {20, "Tyler"}, {30, "Lamar"}};

    delete p1;
    delete p2;
    delete[] p3;
    delete[] p4;
}

test3的执行结果

default constructor #p1
call the constructor of People #p2
default constructor #p3
default constructor
default constructor
call the constructor of People #p4
call the constructor of People
call the constructor of People
call the destructor of People #delete
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People
call the destructor of People

operator new和operator delete

newdelete是C++中用于动态管理内存的两个操作符(关键字),而其底层分别调用了operator newoperator delete,这是两个全局函数,operator new用于开辟新的空间,operator delete用于释放空间。下面是两个函数的源代码。

operator new

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 new 实际是调用malloc开辟空间,当申请空间成功时,直接返回指针;如果申请失败,尝试执行预设的应对措施,若用户并无预设应对措施,则抛异常

operator delete

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 delete实际是调用free释放空间

new和delete的实现原理

至此,对于一个动态内存管理的自定义对象(内置类型没有构造和析构),它在程序中的”历程“是这样的

image-20240402170620826

  1. 对于new T[N],底层是调用operator new[]。先在operator new[]中实际调用operator new函数完成N个对象空间的申请,然后在申请的空间上调用N次构造函数。

  2. 对于delete[],底层是调用opertor delete[]。先在即将释放的空间上调用N次析构函数清理资源,然后调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。

值得注意的一点是,operator new[]的参数是申请空间的字节大小size,但是使用new[]分配数组时是指定数组元素个数。不必担心,当你使用 new[] 来分配一个数组时,编译器会根据数组类型的大小和元素数量计算所需的总字节大小,并将其传递给 operator new[]

特别注意,必须严格遵守new with delete和new[] with delete[]的匹配使用,否则会导致不可预期的后果!!

4.Some Problems

Problem #1: delete[]怎么知道调用多少次析构函数

当你使用 new[] 分配一个数组array,array使用结束后,就必须使用delete[]释放,如下:

class Cls
{
public:
    Cls(int i = 0):i_(i){}

    ~Cls()
    {
        std::cout << counter++ << " call ~Cls()" << std::endl; 
    }
private:
    int i_;
    static int counter;
};
int Cls::counter = 0;//计数器,记录delete[]时调用析构函数的次数

const long long len = 10;

void test1()
{
    Cls* array = new Cls[len];
    delete[] array;
}

int main()
{
    test1();
    return 0;
}

运行结果

1 call ~Cls()
2 call ~Cls()
3 call ~Cls()
4 call ~Cls()
5 call ~Cls()
6 call ~Cls()
7 call ~Cls()
8 call ~Cls()
9 call ~Cls()
10 call ~Cls()

那么问题来了,当我们创建Cls类数组时,显式传入了数组长度10,这非常正确,operator new能够根据传入的数组长度,申请合适的内存空间。然而,当array使用结束后释放时,并没有传入数组长度或空间大小,那么delete[]怎么知道要调用多少次析构函数呢?根据运行结果,delete[]确实正确地调用了10次析构函数。

实际上,在C++中,new T[]申请数组空间时,会在头部额外开辟一个8字节(x64)的空间,用以存储long long类型的数组长度(元素个数),但返回指针时跳过了这一段空间,返回的是有效空间的首地址。

image-20240403102353978

因此,当调用delete[]时,函数拿到了有效空间的首地址ptr,只需让ptr回退8个字节(一个long long类型的长度),找到数组长度,即可知道需要调用多少次析构函数!

operator new[]接收到的参数size_t size是实际数组长度,包括额外的头部;operator delete[]接收到的参数void* o指向的是包含额外头部的整个数组空间。因此可以通过重载operator new[]operator delete[],观察动态数组的实际长度和实际释放地址。

验证demo:

class Cls
{
public:
    Cls(int i = 0):i_(i){}
    ~Cls(){}
private:
    int i_;
};


void* operator new[](size_t sz)
{
    printf("数组实际长度: %ld\n", sz);
    void* p = malloc(sz);
    return p;
}

void operator delete[](void* fp)
{
    printf("实际释放的地址: %p\n", fp);
    free(fp);
}

const long long len = 10;

void test1()
{
    Cls* array = new Cls[len];
    char* ptr = (char*)array;
    printf("数组预期长度: %d\n", len*sizeof(Cls));
    printf("数组首地址: %p\n", ptr);
    delete[] array;
}

验证结果

数组实际长度: 48
数组预期长度: 40
数组首地址: 0x18b3c28
实际释放的地址: 0x18b3c20

Problem #2: new/delete和new[]/delete[]的匹配使用

newdelete是用于动态分配单个对象的内存,而new[]delete[]则用于动态分配数组的内存。如果你在分配内存时使用了new,却使用了delete[]来释放内存,或者使用了new[],却使用了delete来释放内存,会导致未定义的行为(Undefined Behavior)。这种不匹配的使用会导致内存泄漏或者程序崩溃等严重后果

例如,使用new[]分配内存,却使用了delete来释放内存。

void test4()
{
    // 对于内置类型
    int* p1 = new int[10];
    delete p1;
    // 可能不会出错,因为delete内置类型对象时,只需回收空间,不用调用析构函数

    // 对于自定义类型
    Cls* p2 = new Cls[10];
    delete p2;
    // 出错,delete p2只会调用一次析构函数,其它对象都没有析构,释放空间后可能会导致内存泄漏
}

又如,使用new分配内存,却使用了delete[]来释放内存。

void test4()
{
    //未定义
    int* p3 = new int;
    delete[] p3;

    //可能在非法空间调用了析构函数
    Cls* p4 = new Cls;
    delete[] p4;
}

在不同平台上,上述两种情况产生的后果可能会不同,可能会出错,也可能“侥幸”成功运行,因为这是未定义行为(Undefined Behavior)。因此,必须严格遵守new with delete和new[] with delete[]的匹配使用。

Problem #3: new/delete和malloc/free的区别

  1. malloc()free()是函数,newdelete是操作符。
  2. newmalloc()都是在堆上申请动态内存,并返回指向这块空间的指针。new在申请空间成功后,会进行初始化操作:对于内置类型,赋值初始化;对于自定义类型,调用构造函数对其进行初始化。malloc()却不会初始化。
  3. deletefree()都是对动态内存进行回收。而delete会在回收空间前,调用析构函数,进行对象的资源清理,free()却不会。
  4. new在分配失败时会抛出std::bad_alloc异常,而malloc()在分配失败时返回空指针。delete在删除空指针时是安全的,而free()不接受空指针。
  5. newdelete的操作对象是类的实例对象,而malloc()free()的操作对象是一块内存空间。因此:
    • 使用malloc()开辟空间时需要用户指定空间大小(以字节为单位),而new不用,只需在其后说明空间对象的类型,如果是多个对象,在[]中指定个数即可`
    • malloc()返回的是void*类型的指针,使用时需要手动类型强转;而new返回的就是空间中对象的类型指针。
      成功后,会进行初始化操作:对于内置类型,赋值初始化;对于自定义类型,调用构造函数对其进行初始化。malloc()却不会初始化。
  6. deletefree()都是对动态内存进行回收。而delete会在回收空间前,调用析构函数,进行对象的资源清理,free()却不会。
  7. new在分配失败时会抛出std::bad_alloc异常,而malloc()在分配失败时返回空指针。delete在删除空指针时是安全的,而free()不接受空指针。
  8. newdelete的操作对象是类的实例对象,而malloc()free()的操作对象是一块内存空间。因此:
    • 使用malloc()开辟空间时需要用户指定空间大小(以字节为单位),而new不用,只需在其后说明空间对象的类型,如果是多个对象,在[]中指定个数即可`
    • malloc()返回的是void*类型的指针,使用时需要手动类型强转;而new返回的就是空间中对象的类型指针。
  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值