【C语言】动态内存经典笔试题(上卷)

前言

本系列将详细讲解4道有关动态内存的经典笔试题,以助于加深对动态内存的理解。这些题目都非常经典,你可能随时会遇到它们,所以非常重要

本文讲解其中的前两题。

第一题

这个程序运行的结果是什么?

void GetMemory(char *p)
 {
     p = (char *)malloc(100);
 }

 void Test(void)
 {
     char *str = NULL;
     GetMemory(str);
     strcpy(str, "hello world");
     printf(str);
 }

int main()
{
    Test();
    return 0;
}
分析

代码的执行顺序从主函数开始,主函数中调用了Test(),所以我们去看Test()。创建了空指针str传给函数GetMemory(),开辟了100字节的地址,首地址存到p中。再回到Test(),往下执行。

要注意的是,实参传给形参时,形参是实参的一份临时拷贝。所以相当于我们在GetMemory里创建了一个指针变量p,里面存的是NULL,因为p得到的是str的值;在堆上申请了100个字节的空间,假设地址是0x0012ff40,然后这个地址就放入p中,p有能力能找到这块空间。

但此时并没有将这个地址给str,str依然是NULL,GetMemory函数结束后,str还是NULL,没有指向有效的空间,据我们对strcpy的了解,已经对空指针解引用操作了,程序会崩溃。这是这段代码的第一个问题。

第二个问题,我们没有free我们在GetMemory里开辟的这块空间,回到Test之后没人记得这块空间在哪,就形成了内存泄漏的问题。

补充这个printf(str);的语法是没有问题的。虽然我们一般会使用printf("%s\n",str);的方式,但其实printf("haha\n");我们也是直接把字符串给printf了,本质上我们给的是首字符的地址。就像char*p = "haha";我们也是把字符首地址赋给p而不是把字符赋给p。  也就是printf有首字符地址就可以打印。

怎么改正这个代码?

我们可以去思考这个代码的原意是什么,从函数名GetMemory可以看出,是想申请(get)一块内存(memory),将这块内存用于存放拷贝内容。所以我们可以从原意出发去修改。

改法1:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void GetMemory(char** p)//二级指针接收
{
    *p = (char*)malloc(100);//*p相当于str,str=(char*)malloc(100);开辟的内存首地址确实给了str
}

void Test(void)
{
    char* str = NULL;
    GetMemory(&str);//改为传str的地址
    strcpy(str, "hello world");
    printf(str);
    free(str);//避免内存泄漏
    str=NULL;
}

int main()
{
    Test();
    return 0;
}

为了理解这么改的含义,我们可以简化一下这个问题,如果我们写这样的代码:

 这就类似我们上面修改前的代码,p只是a的一份临时拷贝,里面放着0,当它改为10后,对a并没有实质影响。那么如果我们想通过改变p来改变a的值,就会把代码改成这样:

可以看到,此时我们的a打印出来,已经被改为10了。

同样的,str虽然是指针,但指针变量也是变量。我们现在是想将str里的内容(NULL)改为我们开辟的内存的起始地址,所以我们直接传str是不能达到效果的,应该传&str,就像&a,但是不同的在于str本身就是个指针,而指针的地址应该是个二级指针,所以我们要用二级指针来接收。

最后,再记得释放str和置为空指针,避免内存泄漏就行了。

改法2:

我们把p返回并用str接收就行了。但是这样写有点多此一举,改成这样更简单、合适:

第二题

下面这个程序运行结果是什么?

char *GetMemory(void)
{
     char p[] = "hello world";
     return p;
}

void Test(void)
{
     char *str = NULL;
     str = GetMemory();
     printf(str);
}

int main()
{
    Test();
    return 0;
}
分析:

打印结果是这个:

这题的突破口在于,p可是个数组,进入函数GetMemory创建,出了函数就会销毁。而在销毁前,我们先把p返回了。p是数组名,即起始地址。这个地址返回后我们放到str中,str能找到这块空间。内存里确实有这块空间,但是能否用这块空间取决于我们有没有使用权限。出了函数GetMemory,其实这块空间的使用权限就没有了,但str仍能找到它(str就是个野指针)。打印出来“烫烫烫…”说明这块空间使用权限不属于当前程序时,空间中内容可能被改掉了,我们非要打印就不再是hello world了。

注意,这个现象发生根本原因在于我们的p是个数组,如果改为melloc开辟的空间,就不会有这个问题。因为如果我们不手动释放,在程序彻底结束前,p就一直指向这块空间且有使用权限。数组(也相当于局部变量)开辟的空间是放在上的。所以这个问题是一个典型的返回栈空间(临时空间)地址的问题。

我们可以返回一个变量,但不能返回一个变量的地址。因为这个变量(比如数组)出了函数就销毁了,记住地址也没用。

比如,下面这个代码就是返回一个变量:

实际上,编译器会怎么处理这个代码呢?

a会销毁,编译器会把10放到一个寄存器中。寄存器是CPU里的一个存储空间,是不会销毁的。a销毁了没关系,会将寄存器里的10再给n。就像一个托管。

而如果返回地址,就不行:

 

此时p得到返回的地址,但是指向的空间已经归还操作系统,所以p是个野指针。虽然我们打印出来发现还是原来的值,这只是巧合。如果改为这样:

可以看到,在前面打印了一次其他内容后,就无法再打印出2077。 为什么呢?这说明我们在打印*p时,这个空间还给操作系统了,里面已经不再是2077了。我们在printf("See\n");的时候可能已经把这块空间申请走了,把原本内容覆盖了。

 

可以看到,在test函数结束后,函数栈帧销毁了,而printf("See\n");在申请空间时可能把原来test的空间覆盖了,所以可能会把p指向的空间的内容修改。

再次重申,这是返回栈空间地址的问题。

一旦有人接收了这个地方,就注定是个野指针。

附加题

我们再看看这两个题:

一、以下程序有什么问题?

int* f1(void) {
	int x = 10;
	return (&x);
}

分析:这个也是返回栈空间地址的问题。

二、以下程序有什么问题?

int* f2(void) {
	int* ptr;
	*ptr = 10;
	return ptr;
}

分析:ptr没有初始化就进行了解引用,ptr是野指针。

到此,上卷结束,祝阅读愉快^_^

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-09 00:58:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-09 00:58:01       101 阅读
  3. 在Django里面运行非项目文件

    2024-06-09 00:58:01       82 阅读
  4. Python语言-面向对象

    2024-06-09 00:58:01       91 阅读

热门阅读

  1. advices about writing promotion ppt

    2024-06-09 00:58:01       50 阅读
  2. KMeans聚类分析星

    2024-06-09 00:58:01       32 阅读
  3. 中介子方程七

    2024-06-09 00:58:01       34 阅读
  4. C++的封装(十二):外部构造函数

    2024-06-09 00:58:01       29 阅读
  5. 服务器硬件基础知识有哪些?

    2024-06-09 00:58:01       23 阅读
  6. Linux的命令补全脚本

    2024-06-09 00:58:01       20 阅读
  7. Android Camera APP预览画面镜像及旋转处理

    2024-06-09 00:58:01       26 阅读
  8. 请求分页存储管理方式

    2024-06-09 00:58:01       24 阅读
  9. Flink 容错

    2024-06-09 00:58:01       22 阅读
  10. 5、js关于数组的常用方法(19种)

    2024-06-09 00:58:01       33 阅读
  11. ubuntu,确认cudnn是否安装成功

    2024-06-09 00:58:01       31 阅读
  12. C# WPF入门学习主线篇(十二)—— Canvas布局容器

    2024-06-09 00:58:01       30 阅读
  13. 富格林:有效杜绝被骗安全做单

    2024-06-09 00:58:01       26 阅读
  14. 【C++PCL】点云处理对称目标函数的ICP

    2024-06-09 00:58:01       30 阅读