C/C++内存管理

文章目录

  • 前言
  • [1. C/C++内存分布](#1. C/C++内存分布)
  • [2. C语言中动态内存管理方式](#2. C语言中动态内存管理方式)
    • [2.1 malloc/calloc/realloc和free](#2.1 malloc/calloc/realloc和free)
  • [3. C++内存管理方式](#3. C++内存管理方式)
    • [3.1 new/delete操作内置类型](#3.1 new/delete操作内置类型)
    • [3.2 new和delete操作自定义类型](#3.2 new和delete操作自定义类型)
  • [4. operator new与operator delete函数(重要点进行讲解)](#4. operator new与operator delete函数(重要点进行讲解))
    • [4.1 operator new与operator delete函数(重点)](#4.1 operator new与operator delete函数(重点))
    • [4.2 operator new 、malloc、new之间的关系和区别,delete同理](#4.2 operator new 、malloc、new之间的关系和区别,delete同理)
    • [4.3 operator new与operator delete的类专属重载(了解)](#4.3 operator new与operator delete的类专属重载(了解))
  • [5. new和delete的实现原理](#5. new和delete的实现原理)
    • [5.1 内置类型](#5.1 内置类型)
    • [5.2 自定义类型](#5.2 自定义类型)
  • [6. 定位new表达式(replacement-new) (了解)](#6. 定位new表达式(replacement-new) (了解))
  • [7. 常见问题](#7. 常见问题)
    • [7.1 malloc/free和new/delete的区别](#7.1 malloc/free和new/delete的区别)
    • [7.2 内存泄漏](#7.2 内存泄漏)
      • [7.2.1 什么是内存泄漏,内存泄漏的危害](#7.2.1 什么是内存泄漏,内存泄漏的危害)
      • [7.2.2 内存泄漏分类(了解)](#7.2.2 内存泄漏分类(了解))
      • [7.2.3 如何检测内存泄漏(了解)](#7.2.3 如何检测内存泄漏(了解))
      • 7.2.4如何避免内存泄漏
    • [7.3 如何一次在堆上申请4G的内存?](#7.3 如何一次在堆上申请4G的内存?)
  • 总结

前言

提示:这里可以添加本文要记录的大概内容:
内存管理是C和C++编程中至关重要的一部分,直接关系到程序的性能、稳定性和可维护性。在这个博客中,我们将深入探讨C/C++中的内存管理机制,包括动态内存分配、指针操作、内存泄漏的预防与排查等方面。了解内存管理的原理和最佳实践,将帮助程序员更好地规划和优化代码,提高程序的效率和可靠性。

提示:以下是本篇文章正文内容,下面案例可供参考

1. C/C++内存分布

我们先来看下面的一段代码和相关问题

cpp 复制代码
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";
 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);


1. 选择题:
 选项: A.栈 B.堆 C.数据段 D.代码段
 globalVar在哪里?__C__ staticGlobalVar在哪里?__C__
 staticVar在哪里?__C__ localVar在哪里?_A___
 num1 在哪里?__A__
 
 char2在哪里?__A__ *char2在哪里?__A_
 pChar3在哪里?__A__ *pChar3在哪里?__D__
 ptr1在哪里?__A__ *ptr1在哪里?__B__	
}

2. 填空题:(假设是在32位平台下)
 sizeof(num1) = __40__; 
 sizeof(char2) = __5__; strlen(char2) = __4__;
 sizeof(pChar3) = __4__; strlen(pChar3) = __4__;
 sizeof(ptr1) = __4__

解决上面的问题前,我们需要一些基本知识


这里有人可能会疑惑,为什么phcar3位于栈区,但是pchar3位于代码段,这是因为phcar3是个指针,是个临时变量所以位于栈区,但是pchar3是指针所指向的内容,而指针所指向的内容是一个字符串常量,只读常量位于代码段。
【说明】

  1. 栈又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段--存储全局数据和静态数据。
  5. 代码段--可执行的代码/只读常量。

2. C语言中动态内存管理方式

2.1 malloc/calloc/realloc和free

可以看这篇博客,讲的很详细!!!

3. C++内存管理方式

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

3.1 new/delete操作内置类型

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
	//使用C函数进行动态内存分配
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)malloc(sizeof(int)*10);
	free(p1);
	free(p2);

	//使用C++操作符进行动态内存分配
	int* p3 = new int;//申请一个int,4个字节空间,不初始化
	int* p4 = new int(10);//申请一个int,4个字节空间,初始化为10
	int* p5 = new int[10];//申请10个int,40个字节空间,不初始化
	delete p3;
	delete p4;
	delete[] p5;
	return 0;
}

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

3.2 new和delete操作自定义类型

cpp 复制代码
#include <iostream>
using namespace std;
class Test
{
public:
	Test()
	{
		cout << "构造函数" << endl;
	}
	~Test()
	{
		cout << "~析构函数" << endl;
	};

private:
	int _data;
};

void test1()
{
	Test* t1 = (Test*)malloc(sizeof(Test));//申请一个Test类型的空间
	Test* t2 = (Test*)malloc(sizeof(Test)*10);//申请10个Test类型的空间
	free(t1);//释放空间
	free(t2);//释放空间
}

void test2()
{
	Test* t3 = new Test;//申请空间+构造函数初始化
	Test* t4 = new Test[4];//申请空间+构造函数初始化
	delete t3;//析构函数清理+释放空间
	delete[] t4;//析构函数清理+释放空间
}
int main()
{
	test1();
	test2();
	return 0;
}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。

总结:
思考一个问题,既然已经有了malloc和free,new和delete存在的意义是什么??或者说两者的区别是什么?
答:1、对于上面的内置类型,他们的效果是一样的。
2、对于自定义类型,效果就不一样了。malloc只申请空间,但是new是申请空间+构造函数初始化,free只释放空间,delete是析构函数清理+释放空间
3、结论就是:在CPP中,建议使用new和delete

4. operator new与operator delete函数(重要点进行讲解)

4.1 operator new与operator delete函数(重点)

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

cpp 复制代码
/*
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)

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

4.2 operator new 、malloc、new之间的关系和区别,delete同理

来看一段代码

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	A() {};//无参构造
	~A() {};//析构函数
private:
	int _a;
};
int main()
{
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;
	size_t size = 2;
	void* p4 = malloc(size * 1024 * 1024 * 1024);
	cout << p4 << endl;//失败会返回nullptr(面向过程中错误的处理方式)

	try
	{
		void* p5 = operator new(size * 1024 * 1024 * 1024);
		cout << p5 << endl;//失败会抛出异常(面向对象中错误的处理方式)
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
}

结果:

这里直接说明了一个结论:关于operator new和malloc的区别? 答案是:使用方式都一样,只是对错误处理的方式,一个是返回空指针,一个是抛出异常,这也体现了面向过程编程和面向对象编程不同的编程思想!!

最后总结一下区别:

区别如下(有一个小的书写错误,最后一行应该是operator delete):

正常情况下,释放空间不会出错,除非你只归还了一部分空间给操作系统,如果释放空间失败,那么会直接终止进程

4.3 operator new与operator delete的类专属重载(了解)

下面代码演示了,针对链表的节点ListNode通过重载类专属 operator new/ operator delete,实现链表节点使用内存池申请和释放内存,提高效率

cpp 复制代码
struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _data;
	void* operator new(size_t n)
	{
		void* p = nullptr;
		p = allocator<ListNode>().allocate(1);
		cout << "memory pool allocate" << endl;
		return p;
	}
	void operator delete(void* p)
	{
		allocator<ListNode>().deallocate((ListNode*)p, 1);
		cout << "memory pool deallocate" << endl;
	}
};
class List
{
public:
	List()
	{
		_head = new ListNode;
		_head->_next = _head;
		_head->_prev = _head;
	}
	~List()
	{
		ListNode* cur = _head->_next;
		while (cur != _head)
		{
			ListNode* next = cur->_next;
			delete cur;
			cur = next;
		}
		delete _head;
		_head = nullptr;
	}
private:
	ListNode* _head;
};
int main()
{
	List l;
	return 0;
}

【补充】

cpp 复制代码
struct ListNode_CPP
{
	int _val;
	struct ListNode_CPP* _next;//兼容C struct的用法
	ListNode_CPP* _prev;//在CPP中,struct已经可以认为是一个类了,和class一样,区别就是struct中默认访问限定符是public,而class是private
};

//在CPP中,struct已经可以认为是一个类了,和class一样,区别就是struct中默认访问限定符是public,而class是private

5. new和delete的实现原理

5.1 内置类型

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

5.2 自定义类型

  • new的原理
    • 调用operator new函数申请空间
    • 在申请的空间上执行构造函数,完成对象的构造
  • delete的原理
    • 在空间上先执行析构函数完成资源的清理化工作
    • 再调用operator delete函数释放对象的空间
  • new T[N]的原理
    • 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
    • 在申请的空间上执行N次构造函数
  • delete[]的原理
    • 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    • 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

6. 定位new表达式(replacement-new) (了解)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

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

使用场景:

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化
示例:

cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	A(int a=0) 
	{};//无参构造
	~A() {};//析构函数
private:
	int _a;
};
int main()
{
	A* p1 = new A;

	//模拟上面的行为
	A* p2 = (A*)operator new(sizeof(A));
	//对已经存在的一块空间调用构造函数初始化,定位new/replacement new
	new(p2)A(10);
	return 0;
}

7. 常见问题

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

7.2 内存泄漏

7.2.1 什么是内存泄漏,内存泄漏的危害

概念:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死

cpp 复制代码
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;
 }

7.2.2 内存泄漏分类(了解)

C/C++程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap leak)
    堆内存指的是程序执行中依据需要分配,通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏
    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

7.2.3 如何检测内存泄漏(了解)

7.2.4如何避免内存泄漏

以下是一些常见的方法和最佳实践来帮助减少内存泄漏的发生:

  1. 使用智能指针:

    • C++11及更高版本引入了智能指针(Smart Pointers),如 std::shared_ptrstd::unique_ptr,它们能够自动管理内存释放。使用智能指针可以避免手动管理 newdelete,从而降低内存泄漏的风险。
    cpp 复制代码
    // 使用智能指针
    std::shared_ptr<int> ptr = std::make_shared<int>(42);
  2. RAII(资源获取即初始化)原则:

    • 利用对象的生命周期管理资源的原则,确保资源的获取和释放在对象的构造和析构中完成。这样可以减少因忘记释放资源而导致的内存泄漏。
    cpp 复制代码
    class ResourceHolder {
    public:
        ResourceHolder() {
            resource = new Resource();
        }
    
        ~ResourceHolder() {
            delete resource;
        }
    
    private:
        Resource* resource;
    };
  3. 避免裸指针的滥用:

    • 尽量使用智能指针或其他RAII类,减少手动管理内存的机会。如果使用裸指针,确保在适当的时机进行释放。
  4. 注意循环引用:

    • 在使用智能指针时,注意避免循环引用,因为它可能导致资源无法正常释放。使用 std::weak_ptr 来打破循环引用。
  5. 使用工具进行内存检测:

    • 利用内存检测工具,如Valgrind(Linux)、AddressSanitizer(Clang/GCC)、DebugCRT(Windows),来检测程序运行中的内存泄漏和其他内存错误。
  6. 记录和追踪内存分配:

    • 记录程序中的内存分配和释放操作,通过日志或专业工具进行追踪,有助于发现潜在的内存泄漏问题。
  7. 使用析构函数清理资源:

    • 在类的析构函数中进行资源的释放,确保对象销毁时相关资源被正确释放。
    cpp 复制代码
    class MyClass {
    public:
        ~MyClass() {
            // 清理资源的操作
        }
    };

通过结合使用智能指针、RAII原则、合理使用工具以及编写健壮的代码,可以有效地降低C/C++程序中内存泄漏的风险。

7.3 如何一次在堆上申请4G的内存?

cpp 复制代码
// 将程序编译成x64的进程,运行下面的程序试试?
#include <iostream>
using namespace std;
int main()
{
	size_t i = 4;
	void* p = new char[i*1024*1024*1024];
	cout << "new:" << p << endl;
	return 0;
}

千万不能把i设置的太大,因为根本分配不出来那么多内存 ps:我试过,电脑慢慢会卡死

总结

C/C++内存管理是编程领域中不可忽视的核心概念之一。通过深入了解内存分配和释放、指针的使用、以及内存泄漏的防范措施,我们可以更好地理解程序在内存层面的运行机制。合理的内存管理是保障程序性能、避免潜在风险的必要步骤。通过本博客,读者将收获对C/C++内存管理的深刻理解,为编写高效、稳定的代码打下坚实基础。

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn5 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
青花瓷5 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
睡觉谁叫~~~5 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust