c和c++中的内存管理

admin2024-04-03  0

文章目录

  • 1.内存管理
    • 程序内存划分
    • 静态分配和动态分配
  • 2. C语言的内存控制
  • 3. C++的内存控制
    • new和delete
    • operator new和operator delete
    • new和delete的实现原理
  • 4.Some Problems
    • Problem #1: delete[]怎么知道调用多少次析构函数
    • Problem #2: new/delete和new[]/delete[]的匹配使用
    • Problem #3: new/delete和malloc/free的区别

1.内存管理

程序内存划分

c和c++中的内存管理,image-20240329161552901,第1张

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

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

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

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

静态分配和动态分配

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

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

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


2. C语言的内存控制

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

Why

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

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

new和delete的实现原理

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

c和c++中的内存管理,image-20240402170620826,第2张

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

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

特别注意,必须严格遵守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()

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

c和c++中的内存管理,image-20240403102353978,第3张

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

验证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[]的匹配使用

例如,使用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返回的就是空间中对象的类型指针。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!