3.C++内存管理初步探索


在这里插入图片描述

0.前言

大家好啊,肝了几天才肝完的类与对象终于结束,学校作业有点多,难搞,数电写了3个多钟才搞定,一写完就马不停蹄的来搞内存管理了。。

C++中的内存管理与C中的有何不同呢?
new delete 和 malloc free 有啥区别?
诸君且往下看👇

1.虚拟内存分段

image-20220305152013116

,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
栈是向下生长的,后开辟的变量一般地址比之前的小,栈一般会规定大小。Linux是8M

,malloc申请的内存在堆上,使用free释放。堆(heap)是c语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当程序运行时调用malloc()时就会从中分配,调用free可把内存释放。
堆是向上生长的,堆一般2G大小。
一般情况下后申请的比先申请的地址大,但也不一定,要考虑到中间就有free的情况的。

自由存储区,new申请的内存在自由存储区,用delete释放。而自由存储区是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。

全局/静态存储区**,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
静态区也叫数据段

常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。程序编译出的指令也放在这块。
常量区也叫代码段,其实就是二进制代码。

堆&自由存储区的区别与联系

基本上,所有的C++编译器默认用堆来实现自由存储区,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来实现,这时由new运算符分配的对象,说它在堆上也对,说它在自由存储区也对。

总的来说

(1)堆是c语言和操作系统的术语,是操作系统维护的一块内存。自由存储是C++中通过new和delete动态分配和释放对象的抽象概念。
(2)new所申请的内存区域在C++中称为自由存储区,编译器用malloc和free实现new和delete操作符时,new申请的内存可以说是在堆上。
(3)堆和自由存储区有相同之处,但并不等价。

栈与堆区别

int* p = (int*)malloc(sizeof(int) * 4);//p在栈区,指向开辟的空间在堆区

image-20220304211119485

image-20220304202908387

注意:const定义的变量所在趋于取决于变量本身,如果修饰的是局部变量,那么依然在栈区,如果修饰的是全局变量,那么就在静态区。

void f2()
{
	int b = 0;
	cout << &b << endl;
}
void f1()
{
	int a = 0;
	cout << &a << endl;
	f2();
}
int main()
{
	f1();
	return 0;
}
int main()
{
	int* p1 = (int*)malloc(4);
	int* p2 = (int*)malloc(4);
	cout << "p1:" << p1 << endl;
	cout << "p2:" << p2 << endl;
	return 0;
}

举个例子:

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[] = "hello";//char2在栈,*char2也在栈上,在栈上开了个数组,把hello内容都拷贝过去了,hello是常量区的内容
	const char* pChar3 = "hello";//pChar3在栈区,*pChar3在常量区
	int* ptr1 = (int*)malloc(sizeof(int) * 4);//ptr1在栈区,指向开辟的空间在堆区
	int* ptr2 = (int*)calloc(4, sizeof(int));//
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);//
	free(ptr1);
	free(ptr2);
	free(ptr3);
}

内存的静态分配和动态分配的区别主要是两个:

  1. 时间不同。静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。

  2. 空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由函数malloc进行分配。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手动实现。

2.动态内存管理

c语言动态内存管理

int main()
{
    int* ptr1 = (int*)malloc(sizeof(int) * 4);//ptr1在栈区,指向开辟的空间在堆区
    int* ptr2 = (int*)calloc(4, sizeof(int));//
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);//
    free(ptr1);
    free(ptr2);
    free(ptr3);
}

malloc,calloc,realloc区别?

  1. malloc在堆上动态开辟空间
  2. calloc堆上动态开辟空间+初始化为0 相当于 malloc + memset
  3. realloc针对已有空间进行扩容(原地扩容或异地扩容)

不太熟悉的朋友参考这篇博客:C语言动态内存管理

这里需要free ptr2吗?
也是需要的!

C++动态内存管理

语法

开辟单个对象
type* name = new type(content);
delete name;

开辟多个对象
type* name = new type[n]
delete[] name;

注意:这里的 new delete是操作符

针对内置类型

new/delete malloc/free 针对内置类型的处理没有差别,只是用法不一样

new/delete new[]/delete[] 一定要匹配,否则可能会出bug。
基本类型的对象没有析构函数,所以回收基本类型组成的数组空间用 delete 和 delete[] 都是可以的;但是对于类对象数组,只能用 delete[]。

int main()
{
	//多个对象
	int* p1 = (int*)malloc(sizeof(int) * 10);
	int* p2 = new int[10];//new是操作符
	free(p1);
	delete[] p2;
    int* p5 = new int[10]{10}; // new十个int对象,初始化第一个对象为10,剩下的都是0
    
	//单个对象
	int* p3 = (int*)malloc(sizeof(int));
	int* p4 = new int;
	free(p3);
	delete p4;
    int* p5 = new int(10); // new一个int对象,初始化为10
	return 0;
}

针对自定义类型

针对自定义类型, new 会去调用构造函数完成初始化 delete会先析构再释放空间

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

    ListNode(int val = 0)//构造函数
        :_next(nullptr)
            ,_prev(nullptr)
            ,_val(val)
        {
            cout << "ListNode(int val = 0)//构造函数" << endl;
        }
    ~ListNode()
    {
        cout << "~ListNode()" << endl;
    }
};

int main()
{
    //c malloc只是开空间 free 仅仅是释放空间
    struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
    free(n1);

    //C++ new 针对自定义类型 开空间+调用构造函数完成初始化
    //delete 针对自定义类型,先析构 再释放空间
    ListNode* n2 = new ListNode;
    ListNode* n3 = new ListNode(1);
    delete n2;
    return 0;
}

image-20220304215127119
举个Stack的例子👇

class Stack
{
private:
    int* _a;
    int _capacity;
    int _top;
public:
    Stack(int capacity = 10)
    {
        _a = new int[capacity];
        _capacity = capacity;
        _top = 0;
    }
    ~Stack()
    {
        delete[] _a;
        _capacity = _top = 0;
    }
};
int main()
{
    Stack* st1 = (Stack*)malloc(sizeof(Stack));
    Stack* st2 = new Stack;

    free(st1);
    delete st2;
    return 0;
}

new 创建自定义对象,会调用其构造函数完成初始化,而malloc不会完成初始化。

此外,free会直接释放整个空间,而==_a指向的那个空间还存在==,因此就产生内存泄露了。而delete会去调用其析构函数完成资源清理,不会内存泄露。

在这里插入图片描述

此外,new还可以传值进去

ListNode* n2 = new ListNode(5);//如果不传就用默认的
//相当于C中的BuyListNode(5);

image-20220304215703359

多个对象

int main()
{
	struct ListNode* arr3 = (struct ListNode*)malloc(sizeof(struct ListNode)*4);
	free(arr3);

	ListNode* arr4 = new ListNode[4];//开辟4个对象就调用4次构造函数
	delete[] arr4;//delete时也析构4次
	//delete arr4;// 这样写就不匹配,会崩溃
	return 0;
}

C++11还支持一次赋多个初值

ListNode* arr4 = new ListNode[4]{1234};

ListNode* arr4 = new ListNode[4]{123};//如果不足4个,后面的就会用0补充

image-20220304220533506

整型也能这么玩

int* p1 = new int(0);
int* p2 = new int[4]{1,2,3,4};

总结:

  1. C++如果申请内置类型的对象或数组,malloc和new没什么区别
  2. 如果是自定义类型,new,delete 是开空间+初始化,析构清理+释放空间;malloc free 仅仅是开空间 释放空间
  3. 建议都尽量使用new delete

malloc/new 最大能申请多大的空间呢?

int main()
{
	void* p1 = malloc(1024 * 1024 * 1024);//1GB
	cout << p1 << endl;
	return 0;
}

image-20220304221807732

32位下,最大能申请2G左右,因为申请的一整块连续的空间,完整的2G是申请不到的,连续的1.9G成功了

那如何 malloc 4G大小的空间呢?
换成64位平台即可

int main()
{
	void* p1 = malloc(0xffffffff);//4GB
	cout << p1 << endl;
	return 0;
}

其实这里所谓的64位平台,也只是虚拟地址空间,即使电脑物理空间只有4G也是能申请成功的

32位平台 2^32byte --> 4GB
64位平台 2^64byte --> 2^32*4GB -- > 2^34GB 160亿GB左右

  • malloc开辟失败返回空指针
  • new开辟失败抛异常

3.new delete底层实现

operator new & operator delete

这是系统提供的全局函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层调用 operator delete 全局函数来释放空间

operator new 实际通过 malloc 来申请空间,申请成功直接返回,申请失败抛异常
operator delete 实际通过 free 来释放空间
我们也可以直接使用👇

int main(int argc, char const *argv[])
{
	ListNode* p1 = (ListNode*)malloc(sizeof(ListNode));
	free(p1);

	//用法与malloc free一样
	ListNode* p2 = (ListNode*)operator new(sizeof(ListNode));
	operator delete(p2);
	return 0;
}

差异:
开辟失败的处理方式不一样,malloc失败了返回空指针,operator new失败以后抛出异常。
像Java、Python、Go等后来的语言,都比较喜欢抛异常的机制,毕竟像C语言这种返回错误码其实是不太舒服,因此C++也改善了。因此operator new不需要检查开辟失败。

int main(int argc, char const *argv[])
{
	void *p3 = malloc(0xffffffff);
	if (p3 == NULL)
	{
		cout << "malloc fail" << endl;
	}

	try
	{
		void *p4 = operator new(0xffffffff);
	}
	catch (const std::exception &e)
	{
		std::cerr << e.what() << '\n';
	}
	system("pause");
	return 0;
}

image-20220305090003312

源码

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

new 底层

调用operator new + 构造函数 (自定义类型)

image-20220305154332082

如何验证呢?
通过反汇编查看:
image-20220305154523766

delete 底层

同理,如果开辟的空间是自定义类型,delete就先调用其析构函数释放相关资源,再调用operator delete 释放空间。

class Stack
{
public:
	Stack()
		: nums(new int[10])
            , top(0)
            , capacity(10)
	{
	}
	~Stack()
	{
		delete[] nums;
		top = capacity = 0;
	}

private:
	int *nums;
	int top;
	int capacity;
};

int main()
{
	Stack stack;
    delete stack;
	return 0;
}

以上面的代码举例:
如果delete不先调用析构函数,那么释放了stack对象后,在堆区里面的nums将会继续存在,导致内存泄露

new [ ] 底层

  1. 先调用 operator new[] 函数,在operator new[] 中实际调用 operator new 函数完成N个对象空间的申请,
  2. 再在申请的空间上执行N次构造函数

delete [ ] 底层

  1. 先在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  2. 再调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

4.operator new 类专属重载

如果反复的在堆上开辟空间和释放空间,效率比较低下。

举个例子👇

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _data;
    
	ListNode(int data = 0)
		:_next(nullptr)
		, _prev(nullptr)
		, _data(data)
	{
		cout << "ListNode()" << endl;
	}
};

class List
{
public:
	List()
	{
		_head = new ListNode;  // operator new + 构造函数
		_head->_next = _head;
		_head->_prev = _head;
	}

	void PushBack(int val)
	{
		ListNode* newnode = new ListNode;
		ListNode* tail = _head->_prev;

		// head	tail	newnode
		tail->_next = newnode;
		newnode->_prev = tail;
		newnode->_next = _head;
		_head->_prev = newnode;
	}

	void Clear()
	{
		ListNode* cur = _head->_next;
		while (cur != _head)
		{
			ListNode* next = cur->_next;
			delete cur;
			cur = next;
		}

		_head->_next = _head;
		_head->_prev = _head;
	}

	~List()
	{
		Clear();
		delete _head;
		_head = nullptr;
	}

private:
	ListNode* _head;
};

int main()
{
    // 反复的开辟空间和释放空间
	List l;
	int n;
	for (int i = 0; i < n; ++i)
	{
		l.PushBack(i);
	}
	cout << endl << endl;
	l.Clear();
	cin >> n;
	for (int i = 0; i < n; ++i)
	{
		l.PushBack(i);
	}
	return 0;
}

借助内存池去调用专属的operator new。

struct ListNode
{
	ListNode *_next; // C++写法
	ListNode *_prev;
	int _val;

	//类中重载专属的operator new
	void *operator new(size_t n)
	{
		void *p = nullptr;
		p = allocator<ListNode>().allocate(1);//STL中的空间配置器,其实就是一个简单的内存池
		cout << "memory pool allocate" << endl;
		return p;
	}
	void operator delete(void *p)
	{
		allocator<ListNode>().deallocate((ListNode *)p, 1);
		cout << "memory pool deallocate" << endl;
	}
	ListNode(int val = 0) //构造函数
		: _next(nullptr), _prev(nullptr), _val(val)
	{
		cout << "ListNode(int val = 0)//构造函数" << endl;
	}
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main(int argc, char const *argv[])
{
	ListNode *p = new ListNode(1);
	return 0;
}

这样弄类专属重载的意义何在?

实现了链表结点使用内存池申请和释放内存,提高效率

敲黑板! 这里涉及到了池化技术 (为了提高效率)
做个简单比方:你选择每次要花钱时都找妈妈要钱,妈妈决定一次性给你一定数目的钱,每次要花时让你自己从里面拿,这样就不耽误妈妈工作。

内存池
进程池
线程池
连接池
。。。

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

如果想对已经分配好的空间完成初始化怎么办呢?

placement-new 是在已经分配的原始内存空间中调用构造函数初始化一个对象。

使用格式

new (place_address) type
或者new (place_address) type(initializer-list)

place_address是原始内存空间地址,initializer-list 是类型的初始化列表

placement-new 一般是配合内存池使用,因为内存池分配出的内存没有进行初始化。

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

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

private:
	int _a;
};

int main()
{
	A* p = (A*)malloc(sizeof(A)); //这里只是将开辟的空间强转成A*
	new (p) A; // new(p)A(3);  // 定位new,placement-new,显示调用构造函数初始化这块对象空间

	// 等价于 delete p
	p->~A(); // 析构函数可以显示调用
	operator delete(p);

	return 0;
}
int main()
{
	// 等价于直接用A* p = new A
	A *p = (A *)operator new(sizeof(A));
	new (p) A; // new(p)A(3);  // 定位new,placement-new,显示调用构造函数初始化这块对象空间

	// 等价于 delete p
	p->~A(); // 析构函数可以显示调用
	operator delete(p);

	return 0;
}

6.常考面试题

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

内存泄露

内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄露是指针丢了?还是内存丢了?

指针,内存不会丢

malloc new 申请内存空间的本质是得到一块内存的使用权
free delete 释放内存空间的本质是把使用权交还给系统,那么系统就可以重新分配给别人

注意:

并不是说如果产生了内存泄露就一定会出事,每个进程都会映射一个虚拟地址空间,如果进程正常结束,没有释放的内存也会被释放掉,所以一般程序的内存泄露危害并不大。
僵尸进程除外。

早期的Android不成熟,某些app有内存泄露,用久了就会变很卡,需要隔一段时间就重启一次。
但长期运行的程序,比如服务器上的程序,王者荣耀后台,美团后台服务,滴滴后台服务。。。如果有内存泄露那就完蛋了。

分类

堆内存泄漏

堆内存指的是程序执行中通过动态开辟的函数从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 释放掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间在结束运行前将无法再被使用,也就是堆内存泄漏。

系统资源泄漏

指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

如何避免

  1. 良好的设计规范,良好的编码规范。
  2. 采用 RAII 思想或者智能指针来管理资源。
  3. 公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。
void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int *p1 = (int *)malloc(sizeof(int));
	int *p2 = new int;

	// 2.异常安全问题
	int *p3 = new int[10];

	Func(); //这里Func函数抛异常导致 delete[] p3 未执行 p3没释放
	delete[] p3;
}

关于内存泄露暂且就先说到这里,以后还会详细介绍内存泄露及其处理方式。

好啦,今天的内存管理就到这里了,你学会了吗?写文不易,觉得有帮助的话劳烦点个赞呗~

博主小菜鸟一枚,如有纰漏烦请不吝指出。
附GitHub仓库链接

在这里插入图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值