• C++第七弹 -- C/C++内存管理


    前言

    在C/C++编程中,内存管理是至关重要的一个环节。程序员需要合理地分配和释放内存,以确保程序能够正常运行,避免内存泄漏和崩溃。本文将深入探讨C/C++内存管理机制,从内存分布、动态内存管理方式、new和delete的实现原理到定位new表达式,以及malloc/free和new/delete的区别,全面解析C/C++内存管理的方方面面。

    更多文章, 点击关注博客主页 酷酷学!!!


    正文开始

    一. C/C++内存分布

    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";
     const 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. 填空题:
       sizeof(num1) = ____;40
       sizeof(char2) = ____;5      strlen(char2) = ____;4
       sizeof(pChar3) = ____;4     strlen(pChar3) = ____;4
       sizeof(ptr1) = ____;4
    3. sizeof 和 strlen 区别?
    
    
    
    
    

    char2会在栈中存储, 而*char2内字符串在常量区, 但是因为是使用数组,所以会将abcd\0从常量区拷贝一份到栈区的数组

    在这里插入图片描述
    sizeof 和 strlen 区别?

    1. 定义和类型
      sizeof:是一种运算符,用于计算对象或类型所占的空间大小(以字节为单位)。它在编译时就已经确定了值,并将结果在编译时嵌入到程序中。在头文件中,sizeof 通常被 typedef 为 unsigned int。
      strlen:是一种函数,用于计算字符串的长度,即字符串中字符的数量(不包括结束符 \0)。它定义在 头文件中,其原型为 size_t strlen(const char* str);。strlen 在运行时才能计算字符串的长度。
    2. 用法
      sizeof:
      可以用于基本数据类型(如 int、float 等)。
      可以用于数组、指针、结构体、联合等复合数据类型。
      可以直接作用于变量名或类型名,而不需要变量名或类型名被括号括起来(但使用括号可以提高代码的可读性)。
      对于指针,sizeof 返回的是指针本身的大小,而不是指针所指向的内存区域的大小。
      strlen:
      只能用于字符型指针(char*),且该指针必须指向以 \0 结尾的字符串。
      strlen 的参数必须是字符串的起始地址。
    3. 功能和结果
      sizeof:
      返回的是对象或类型所占的空间大小,单位是字节。
      对于数组,sizeof 返回的是整个数组所占的空间大小,包括所有元素和末尾的 \0(如果数组是字符数组的话)。
      对于指针,sizeof 返回的是指针本身的大小,与指针所指向的数据类型无关。
      strlen:
      返回的是字符串中字符的数量,不包括结束符 \0。
      如果传入的指针不是以 \0 结尾的,strlen 会继续读取内存直到遇到 \0 为止,这可能会导致越界访问。

    在这里插入图片描述

    【说明】

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

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

    malloc/calloc/realloc/free

    void Test ()
    {
     int* p1 = (int*) malloc(sizeof(int));
     free(p1);
     
     // 1.malloc/calloc/realloc的区别是什么?
     int* p2 = (int*)calloc(4, sizeof (int));
     int* p3 = (int*)realloc(p2, sizeof(int)*10);
     
     // 这里需要free(p2)吗?
     free(p3 );
    //不需要,如果realloc成功, 会在p2的基础上进行扩容, 返回的是p2
    //如果扩容失败,realloc会自动释放p2的空间,然后再重新找一块更大的空间,然后返回这块空间的地址.
    }
    

    malloc/calloc/realloc的区别?

    在这里插入图片描述
    在这里插入图片描述
    3. 使用场景
    malloc:适用于需要动态分配内存,但不需要初始化内存内容的场景。
    calloc:适用于需要动态分配内存,并且希望内存内容初始化为0的场景。calloc的初始化特性使得它在分配内存后无需再进行额外的初始化操作。
    realloc:适用于已分配的内存块大小不足以满足当前需求,或者分配了过大的内存块需要缩减的场景。realloc能够调整内存块的大小,同时尽可能保留原内存块中的数据。
    4. 注意事项
    使用malloc、calloc和realloc分配的内存都必须通过free函数来释放,以避免内存泄漏。
    realloc在调整内存块大小时,如果成功则返回新的内存块地址(可能与原地址相同),如果失败则返回NULL,但原内存块仍然保持有效。因此,在使用realloc时,通常需要用一个临时指针来接收realloc的返回值,并在检查返回值后再决定是否更新原指针。
    calloc分配的内存块会被初始化为0,这可能会带来一些额外的性能开销。如果不需要初始化内存内容,使用malloc可能更加高效。

    三. C++中动态内存管理

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

    对于内置类型

    int main()
    {
    	//对于内置类型,除了用法方便,和malloc没什么区别
    	int* p1 = new int;
    	int* p2 = new int[10];
    	//默认不初始化, 但是可以初始化
    
    	int* p3 = new int(10);//初始化为10
    	int* p4 = new int[10] {1, 2, 3, 4};
    
    	delete p1;
    	delete[] p2;
    	return 0;
    }
    

    在这里插入图片描述

    对于自定义类型

    class A
    {
    public:
    	A(int a = 0)
    		:_a(a)
    	{
    		cout << "A(int a)" << endl;
    	}
    
    	A(int a1, int a2)
    	{
    		cout << "A(int a1, int a2)" << endl;
    	}
    
    	A(const A& aa)
    		:_a(aa._a)
    	{
    		cout << "A(const A& aa)" << endl;
    	}
    
    	A& operator=(const A& aa)
    	{
    		cout << "A& operator=(const A& aa)" << endl;
    		if (this != &aa)
    		{
    			_a = aa._a;
    		}
    		return *this;
    	}
    
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    private:
    	int _a;
    };
    
    int main()
    {
    	//对于自定义类型, new能够调用构造初始化,malloc不在适用
    	//A* p1 = new A;
    	//A* p2 = new A(2);//传参构造
    
    	//delete p1;//会调用析构函数
    	//delete p2;
    
    	A* p1 = new A[10];//连续申请10个空间
    	//会调用十次默认构造
    	//不想调用默认构造,下面是拷贝构造
    	A aa1(1);
    	A aa2(2);
    	A aa3(3);
    	A* p2 = new A[10]{ aa1,aa2,aa3 };
    
    	A* p3 = new A[10]{ 1,2,3,4,{6,7} };//也可以直接写,进行隐式类型转化
    	
    	delete p1;
    	delete p2;
    	delete p3;
    }
    

    这里使用new进行内存开辟, 会自动调用它的构造函数, 使用delete也会自动调用它的析构函数

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

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

     //结论:不要错配使用,一定匹配使用,否则结果是不确定
    int main()
    {
    	//对于自定义类型,delete[]需要保存需要析构的次数
    	// 所以会多开辟四个字节进行存储析构的次数
    	// 然后释放空间的时候会-4释放掉多开辟的空间
    	// 而delete和free不会,所以会报错 
    	//A* p1 = new A;
    	A* p2 = new A[10];  //44 or 40
    	delete[] p2;
    
    	delete p2;
    	free(p2);
    
    	//对于内置类型无影响
    	int* p3 = new int[10];  //40
    	//free(p3);
    
    	return 0;
    }
    

    在这里插入图片描述

    四. operator new与operator delete函数

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

    /*
    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来释放空间的。

    五. new和delete的实现原理

    1.内置类型

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

    2. 自定义类型

    new的原理

    1. 调用operator new函数申请空间
    2. 在申请的空间上执行构造函数,完成对象的构造

    delete的原理

    1. 在空间上执行析构函数,完成对象中资源的清理工作
    2. 调用operator delete函数释放对象的空间

    new T[N]的原理

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

    delete[]的原理

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

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

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

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

    举个例子:

    不通过new进行申请, 使用operator new, 跟malloc的用法一致, 只是前者会抛异常, 后者返回NULL, 那我们需要手动调用构造函数, 但是p1->A()这样写编译器不支持, 所以就需要用到定位new, 用法new(p1)A, 定位某个地址,进行调用其构造函数, 对于析构函数, 可以直接p1->~A()调用.

    int main()
    {
    	//A* p1 = new A;
    
    	//不想通过new进行申请,使用operator new需要手动调用构造函数
    	A* p1 = (A*)operator new(sizeof(A));
    	//p1->A(); // 编译器不支持这样显示调用构造
    	//new(p1)A;  // 对已有空间,显示调用构造
    	new(p1)A(10);  // 对已有空间,显示调用构造
    
    	//后面回学到的内存池调用
    	//A* p1 = pool.alloc(sizeof(A));
    	//new(p1)A(10);  // 对已有空间,显示调用构造
    	
    	// delete p1
    	p1->~A();//析构编译器支持
    	operator delete(p1);
    	
    	// new []
    	A* p2 = (A*)operator new[](sizeof(A)*10);
    	//new(p2)A[10]{1,2,3,4};  // 对已有空间,显示调用构造
    	for (int i = 0; i < 10; ++i)
    		new(p2 + i)A(i);
    
    	// delete[]
    	for (int i = 0; i < 10; i++)
    	{
    		(p2 + i)->~A();
    	}
    	operator delete[](p2);
    
    	return 0;
    }
    

    七. 常见面试题

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

    总结

    本文深入探讨了C/C++内存管理机制,从内存分布、动态内存管理方式、new和delete的实现原理到定位new表达式,以及malloc/free和new/delete的区别,全面解析了C/C++内存管理的方方面面。

    理解C/C++内存管理机制对于编写高效、安全的代码至关重要。掌握本文所述内容,能够帮助你更好地理解程序运行时的内存分配和释放过程,避免内存泄漏、越界访问等问题,进而提升代码质量。

    希望本文能够对你有所帮助,如果你有任何疑问,欢迎在评论区留言讨论。


  • 相关阅读:
    显示支付结果_前端轮询_解决方案1
    华为挑战良率难题,台积电老将曝1惊人内幕 | 百能云芯
    GBase 8c V3.0.0数据类型——序列号生成函数
    Linux内核分析与应用2-内存寻址
    Spring中@Lazy注解的使用
    LeetCode(cai鸟之路)139. 单词拆分
    HP电脑主机清除BIOS密码
    【预处理】最大加号标志
    freeswitch拨打分机号
    初识Kafka
  • 原文地址:https://blog.csdn.net/2201_75644377/article/details/140448807