C++动态内存分配、内存溢出/泄露、野指针/悬垂指针及代码实例

近期博主就C++的动态内存分配进行了学习。学习内容包括:为什么需要进行动态内存分配、如何进行动态内存分配、C++内存机制(堆与栈的区别)、内存溢出/泄露、野指针/悬垂指针、结合代码实例分析等。

同时也借鉴了不少优秀的文章,其中一部分来源已不可考,如有侵权,通知删除。


一、动态内存分配

1.为什么需要动态内存分配?

有的时候,一个程序到底需要多少内存,是事先无法预测,而在程序运行的时候才知道的。这种情况下,就不能事先定义所有变量了,而是需要在程序运行的时候,再人为地、动态地进行内存的分配。

2.如何进行动态内存分配?

new/delete运算符。

(1)简单变量的动态内存分配

#include <iostream>
#include <windows.h>
using namespace std;

int main ()
{
    
    
    int *p1 = new int(1);
    float *p2 = new float(2.0f);
    char *p3 = new char('c');
    /*
    或者上述代码也可以写成:
	int *p1 = new int;
	*p1 = 1;
	*/

    cout<<*p1<<endl;
    cout<<*p2<<endl;
    cout<<*p3<<endl;
    cout<<p1<<endl;
    cout<<p2<<endl;
	cout<<p3<<endl;
	delete p1;
	delete p2;
	delete p3;

    cout<<"\n"<<endl;
    system("pause"); 

   return 0;
}

运行结果:
在这里插入图片描述
但是,计算机内的空间是有限的,假如空间已被用完,无法分配内存,该如何处理呢?可以用下面这段代码来检查内存是否分配成功:

double* pvalue  = NULL;
if( !(pvalue  = new double ))
{
    
    
   cout << "Error: out of memory." <<endl;
   exit(1);

}

(2)数组的动态内存分配

#include <iostream>
#include <windows.h>
using namespace std;

int main ()
{
    
    
    int* array = new int[5];  //分配内存
    for(int i = 0 ; i < 5 ; i++){
    
    
        array[i] = i;
        cout<<*(array+i)<<endl;  //这里也可以写成array[i]
    }
    delete [] array;  //删除内存
    
    cout<<"\n"<<endl;
    system("pause"); 
    return 0;
}

运行结果:
在这里插入图片描述

(3)对象的动态内存分配(略)


二、C++内存机制

1.栈区

主要存储:函数参数、局部变量。
分配内存时,在栈区从高地址到低地址查找空的内存进行分配。
函数内部声明的所有变量都将用栈内存。调用函数时,分配内存,函数结束时,内存自动释放。每调用一个函数就会给它分配一段连续的栈空间。

2.堆区

主要存储:程序员使用new、malloc出来的内存。
分配内存时,在内存的堆区中从低地址到高地址寻找空的内存区域进行分配。
使用delete、free来释放内存。

3.全局区/静态区

主要存储:程序中的全局变量、static静态变量。
分配内存时在全局区从低地址到高地址查找空闲的内存区域进行分配。

扫描二维码关注公众号,回复: 15399008 查看本文章

4.常量区

存储字符串常量、整形常量。

5.代码区

存放程序执行的cpu指令。

在这里插入图片描述

补充1:深入理解栈区和堆区的使用特点

1.	#include <stdio.h>
2.	#include <stdlib.h>
3.	 
4.	int main() {
    
    
5.	  int a;
6.	  int *p;
7.	  p = (int *)malloc(sizeof(int));
8.	  free(p);
9.	 
10.	  return 0;
11.	}

上述代码很简单,有两个变量a和p,类型分别为int和int *,其中,a和p存储在栈上,p的值为在堆上的某块地址(在上述代码中,p的值为0x1c66010),上述代码布局如下图所示:

在这里插入图片描述

补充2:堆与栈的区别

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

三、内存泄露、内存溢出、野指针、悬垂指针

内存泄露

内存泄露:某部分内存本该释放掉,但由于各种原因没有释放,导致了内存的浪费。

内存泄露的案例:
错误原因:malloc、new申请的内存没有主动释放。

#include <iostream>
#include <windows.h>
using namespace std;

int main ()
{
    
    
    while(true){
    
    
        int *s = (int*)malloc(50); 
    }
    cout<<"\n"<<endl;
    system("pause"); 

   return 0;
}

#include <iostream>
using namespace std;
int main()
{
    
     
   int *a = new int(123);
   cout << *a << endl;
   // We should write "delete a;" here
   a = new int(456);
   cout << *a << endl;
   delete a;
   return 0;
}

注意:
(1)函数内部的定义的临时变量指针就像其他变量一样,函数结束则自动被释放,但是new的内存空间不会被释放,通过new分配的内存一定要手动释放
(2)定义一个指针但是未赋值时,指针为NULL。对空指针应用delete是安全的

内存溢出

内存溢出:系统已经不能再分配出你所需要的空间。比如系统现在只有1G的空间,但是你偏偏要2个G空间,这就叫内存溢出。

内存泄露最终就是会导致内存溢出。

野指针

简单说:没初始化的指针
简单讲是指,指向不可用的内存区域的指针。野指针指向的是垃圾内存区域的指针,一旦使用往往会造成不可预测的结果,这种随机不可预测的结果才是最可怕的。

比如:

void func() {
    
    
    int* p;
    cout<<*p<<endl;
}

结果p会输出一个随机的数字。

悬垂指针:

简单说:一个指针对象被删除,就成了悬垂指针
悬垂指针也是野指针常见的一种。当我们显式地从内存中删除一个对象或者返回时通过销毁栈帧,并不会改变相关的指针的值。这个指针实际仍然指向内存中相同位置,甚至该位置仍然可以被读写,只不过这时候该内存区域完全不可控,因为你不能保证这块内存是不是被别的程序或者代码使用过。

比如:

void dangling_point() {
    
    
    char *p = (char *)malloc(10);
    strcpy(p, "abc");
    cout<<p<<endl;
    free(p);
    if (p != NULL) cout<<p<<endl;
    strcpy(p, "def"); 
}

我们执行free语句时,p所指向的内存被释放,此时该内存区域变成不可用内存。但是,p此时指向的地址并没有发生改变,而且也不会为空,所以if(p != NULL)判断为真,会继续执行后面的语句。
但是strcpy(p, “def”); 这一句,虽然代码不会报错崩溃,但此时正在篡改动态内存区,会产生不可预料的结果,此操作相当危险,尤其是debug时候非常不好排查。

解决方法:为了避免悬垂指针的问题,一般做法是在free/delete指针以后,再将p=NULL。


四、一个实际案例

看下面两段代码,想想它们的输出有什么不同。
一:

#include <iostream>
#include <windows.h>
using namespace std;

class People{
    
    
    public:
        string name;
        long birthday;

};

People* getPeople(){
    
    
People px;
px.birthday = 1111;
    return &px;      
    /*
    返回&px程序会出现警告:
    address of local variable 'px' returned [-Wreturn-local-addr]gcc
    原因:函数内部定义的变量在函数结束时会被释放掉,所以返回一个地址是不行的。
    */
}

int main ()
{
    
    
    People *p = getPeople();
    cout<<p->birthday<<endl;
    system("pause"); 

   return 0;
}

二:

#include <iostream>
#include <windows.h>
using namespace std;

class People{
    
    
    public:
        string name;
        long birthday;

};

People getPeople(){
    
    
    People px;
px.birthday = 1111;
return px;
}

int main (){
    
    
    People p = getPeople();
    cout<<p.birthday<<endl;
    system("pause"); 
    return 0;
}

乍一看两者并没有什么差别,但是第一段程序会出现警告:address of local variable ‘px’ returned [-Wreturn-local-addr]gcc
在这里插入图片描述
究其原因:代码一种,返回的是一个地址;代码二中返回的是一个实际的值。

我们知道,自定义函数中的局部变量都存放在栈区,在函数刚开始运行时自动分配内存空间,在函数结束运行时自动释放内存空间

此时若在函数中返回一个内部变量的地址,是不可以的。因为函数运行结束后,这些临时存在的内存空间都释放了,变量也消失了。虽然我已经把地址return过去了,但以后拿着这个地址去找那个已经消失的内存空间,只能找个寂寞。

此时若在函数中返回一个变量的值,是可以的。因为我们return的是一个实实在在的数据,我们把数据传过去,那边就收到这个数值了。

参考资料

1.warning: function returns address of local variable【函数返回首地址出错原因】
2.c++野指针小结
3.C/C++ 内存泄漏-原因、避免以及定位
4.【C++】动态内存分配详解(new/new[]和delete/delete[])

猜你喜欢

转载自blog.csdn.net/rellvera/article/details/129530598