• 常见的动态内存错误总结


    目录

    一、对NULL指针的解引用操作

    二、对动态开辟空间的越界访问 

    三、对非动态开辟内存使用free释放

    四、使用free释放一块动态开辟内存的一部分

    五、对同一块动态内存多次释放

    六、动态开辟的内存未释放(内存泄漏)

    七、返回栈空间地址的问题与野指针陷阱

            结论


    一、对NULL指针的解引用操作

    代码一  动态内存申请后未进行空指针检查

    1. //err
    2. void test()
    3. {
    4. int *p = (int *)malloc(INT_MAX/4);
    5. *p = 20; //未进行空指针判断,若p的值是NULL,就会有问题
    6. free(p);
    7. }
    8. //correct
    9. void test()
    10. {
    11. int *p = (int *)malloc(INT_MAX/4);
    12. if(p == NULL) //进行返回值NULL指针检查,更为严谨
    13. {
    14. exit(-1);
    15. }
    16. else
    17. {
    18. *p = 20;
    19. }
    20. free(p);
    21. }
    • 在使用动态内存函数(malloc、calloc、reallo)申请空间后,需要进行空指针判断。因为动态内存函数申请空间有可能会失败,若开辟失败,则函数会返回一个NULL指针。因此对动态内存函数的返回值,一定要做检查。
    • 注意,free函数是允许传入NULL指针的。当free的参数为NULL指针时,函数什么事情也不做,不会出现动态内存错误。

    代码二  指针变量的传值调用

    *运行Test函数,结果如何?会打印出 hello world 吗?

    1. //运行Test函数,结果如何?
    2. void GetMemory(char *p) {
    3. p = (char *)malloc(100);
    4. }
    5. void Test(void) {
    6. char *str = NULL;
    7. GetMemory(str);
    8. strcpy(str, "hello world");
    9. printf(str);
    10. }

    答案是并不会。该程序无法正常输出,原因在于其中也有“NULL指针陷阱”。

    如下图所示:

    1. 在Test函数中创建指针变量 *str,并赋值为NULL。
    2. 在Test函数中调用GetMemory函数。GetMemory函数的形参为char *p,形参是实参的临时拷贝,形参的改变不影响实参。*p与*str是两个不同的变量,存储在栈上两块完全不同的内存空间。
    3. GetMemory函数中,变量p指向了一块新开辟的内存空间,但p的改变并不影响实参*str。
    4. 由于局部变量建立在栈上,函数调用结束后,生命周期就结束,因此在GetMemory函数结束后,该函数的栈帧销毁,变量*p也随之销毁。而此时*str的值并未改变,仍然是NULL。
    5. 因此最后在Test函数中strcpy试图将字符串"hello world"拷贝到NULL指针str中,strcpy函数出错。该代码无法正常运行。

    以上代码可以理解为指针的传值调用。我们知道,要在调用的函数内“远程”改变函数外的变量值,需要传入变量的地址。因此,要让上面的代码能正常打印"hello world",只需在函数中传入指针变量的地址——二级指针即可。(当然,通过返回值的方式,在函数调用后接收返回值,也是可行的。)

    1. //correct
    2. void GetMemory(char* *p) //传入二级指针
    3. {
    4. *p = (char *)malloc(100); //对二级指针解引用,获得指针变量的地址,从而改变指针变量的值
    5. }
    6. void Test(void) {
    7. char *str = NULL;
    8. GetMemory(str);
    9. strcpy(str, "hello world");
    10. printf(str);
    11. free(str);
    12. }

    二、对动态开辟空间的越界访问 

    1. void test()
    2. {
    3. int i = 0;
    4. int *p = (int *)malloc(10*sizeof(int));
    5. if(NULL == p)
    6. {
    7. exit(EXIT_FAILURE);
    8. }
    9. for(i=0; i<=10; i++)
    10. {
    11. *(p+i) = i;//当i是10的时候越界访问
    12. }
    13. free(p);
    14. }

    该程序中,指针p指向一片开辟了40个字节(即10个整型)的存储空间。若要访问该空间的内存值,则0到9才是合法的下标数值。然而在遍历赋值时,i越界到了10.这时,由于访问了无权限访问的内存空间,程序就会崩溃,无法正常运行。 


    三、对非动态开辟内存使用free释放

    1. void test()
    2. {
    3. int a = 10;
    4. int *p = &a;
    5. free(p);
    6. }

    以上程序中,a变量并不是动态内存开辟的。令指针p指向a的内存空间并free(p),编译器会运行出错(Debug Assertion failed)。注意:free只能释放动态开辟的内存空间。


    四、使用free释放一块动态开辟内存的一部分

    1. void test()
    2. {
    3. int *p = (int *)malloc(100);
    4. p++;
    5. free(p); //p不再指向动态内存的起始位置
    6. }

    以上程序中,原本指向动态内存开辟空间的p后来被移动,最终所指向的位置不再是开辟空间的起始位置。这时再将p释放,释放的只是部分动态开辟内存而不是全部,这样程序仍然会在运行时崩溃。

    因此要注意:在动态开辟空间之后,如果要遍历开辟的内存空间,不要用开辟时接收的指针来遍历。应当再定义一个别的指针如指针q,进行运算操作。将p指针不动留到最后,用于最终的释放操作。


     

    五、对同一块动态内存多次释放

    1. void test()
    2. {
    3. int *p = (int *)malloc(100);
    4. free(p);
    5. free(p); //重复释放
    6. }

    该程序仍然会出现运行错误。在第一次free(p)后,p所指向的空间就已经被释放。此时的p相当于一个野指针,指向已经被释放了的无权限访问的内存空间。

    而之后再进行一次free(p),p所指的内容已经不是动态内存开辟的空间了。因此程序会出错而中止。 

    注意,若在第一次free(p)后,将p = NULL,然后再进行第二次free(p),此时不会发生任何错误。因此,在释放完动态开辟的内存后,要记得将指针赋值为NULL,使程序更加严谨,避免一时疏忽从而让程序出错。


    六、动态开辟的内存未释放(内存泄漏

    代码一 

    1. void test()
    2. {
    3. int *p = (int *)malloc(100);
    4. if(NULL != p)
    5. {
    6. *p = 20;
    7. }
    8. }
    9. int main()
    10. {
    11. test();
    12. while(1);
    13. }

    如上代码中,在test函数中开辟了动态内存,然而却并没有在其中释放。整个程序中都没有释放动态内存,显然造成了内存泄漏。


    代码二

    1. void GetMemory(char **p, int num)
    2. {
    3. *p = (char *)malloc(num);
    4. }
    5. void Test(void)
    6. {
    7. char *str = NULL;
    8. GetMemory(&str, 100);
    9. strcpy(str, "hello");
    10. printf(str);
    11. }

    该代码同样没有进行内存释放。应当在程序结束之前,加上free(str);    str = NULL;语句,完成“善后”工作。


    七、返回栈空间地址的问题与野指针陷阱

    代码一

    *运行test函数,会有什么结果?能正确输字符串"hello"吗?

    1. //运行test函数,会产生什么结果?
    2. char *Get()
    3. {
    4. char p[] = "hello";
    5. return p;
    6. }
    7. void test()
    8. {
    9. char* str = NULL;
    10. str = Get();
    11. printf(str);
    12. }

    答案是不能,该代码会运行出错,因为该程序中的str指针,实际上是一个野指针。

    在Get()函数中,开辟了一个字符数组p[ ]。char p[ ] 是一个临时的数组空间,里面存放了hello\0这6个字符。它是开辟在栈上的,出了函数就被销毁。

    因此,虽然str接收了返回值字符数组的首元素地址p,但函数调用完毕后,由于函数栈空间销毁,数组空间已不存在。

    str就成了指向无访问权限空间的野指针,在printf访问时出错。

     


    结论

    从函数返回一个指向临时在栈空间开辟的指针,就会有野指针访问的问题。因为在函数栈帧中创建的变量,出了函数,栈帧空间就销毁。空间销毁,而空间的地址还在使用,就会出错。

    而若将char p[ ] 用static修饰,将p定义为静态变量,程序就能正常运行:

    1. //ok
    2. char *Get()
    3. {
    4. static char p[] = "hello"; //加了static
    5. return p;
    6. }
    7. void test()
    8. {
    9. char* str = NULL;
    10. str = Get();
    11. printf(str);
    12. }

    因为在函数局部变量前用static修饰,变量就成了静态变量。

    加了static的变量是在静态区开辟的,不是在栈里开辟的,因此p数组的的生命周期延长,在函数调用结束后,空间也未销毁。因此,str没有成为野指针,str中保存的指针p仍然有效。此时能够正常访问,并打印出字符串"hello"。  


    代码二

    *运行test函数,会有什么结果?能正确输字符串"hello"吗?

    1. //运行test函数,结果如何?
    2. char *Get()
    3. {
    4. char *p = "hello";
    5. return p;
    6. }
    7. void test()
    8. {
    9. char* str = NULL;
    10. str = Get();
    11. printf(str);
    12. }

     答案是可以。该代码能够正常运行并输出"hello"。

    该代码与代码一的区别在于,char* p 指向的是一个常量字符串"hello"。常量字符串是存放在静态区而不是栈区的,因此出了Get函数内存空间也不会销毁。在test函数中仍然可以正常访问。

    (注意,由于常量字符串不能被改变,因此它的首元素地址的数据类型是const char* 。p在接收时,最好也把数据类型写成const char*,否则编译器会报警告。)


    代码三

    *该代码正确吗?

    1. int* test2()
    2. {
    3. int a = 10;
    4. return &a;
    5. }
    6. int main()
    7. {
    8. int*p = test2();
    9. printf("%d\n", *p);
    10. return 0;
    11. }

    该代码自然是不正确。和上面的代码一一样,函数返回了局部变量的地址,造成了p的野指针问题。


    代码四

    *该代码正确吗?

    1. int test2()
    2. {
    3. int a = 10;
    4. return a;
    5. }
    6. int main()
    7. {
    8. int p = test2();
    9. printf("%d\n", p);
    10. return 0;
    11. }

    该代码正确。因为返回的并不是变量的地址,而是变量的值。通过寄存器,test2函数能将局部变量的返回值带入main函数的变量p中。


    代码五

    1. //运行Test函数,会有什么结果?
    2. void Test(void)
    3. {
    4. char *str = (char *) malloc(100);
    5. strcpy(str, "hello");
    6. free(str); //释放str
    7. if(str != NULL)
    8. {
    9. strcpy(str, "world");
    10. printf(str);
    11. }
    12. }

    在释放了str后,str成为野指针。str并未置NULL,进入 if 语句后,strcpy野指针非法访问,运行出错。

    注意,free后必须先将指针置NULL。否则使用指针极易出现空指针访问错误!

  • 相关阅读:
    JSON flag table for Link not found for toolset v143 v143
    多模态论文阅读--V*指导视觉搜索成为多模态大语言模型的核心机制
    bryntum gantt 5.0.6
    【前端精进之路】JS篇:第11期 深拷贝与浅拷贝
    A34 STM32_HAL库函数 之 PCCARD通用驱动 所有函数的介绍及使用
    Python数据分析与机器学习47-维基百科词条EDA
    VSCode:用户自定义模版(通用)
    深度学习进度显示神器:tqdm详解
    vue el-upload 以formData传递二进制文件
    4.1提出问题&4.2拉格朗日插值
  • 原文地址:https://blog.csdn.net/wyd_333/article/details/127113598