【C语言】深度剖析动态内存管理2

简介: 【C语言】深度剖析动态内存管理

3. 常见的动态内存错误


动态内存虽然好用,但是使用不当就会让人十分苦恼,下面列出几个常见的错误。


3.1 对NULL指针进行解引用操作

int main()
{
  int* p = (int*)malloc(INT_MAX);
  if (p == NULL)//判断
  {
    perror("malloc");
    return 1;
  }
  else
  {
    *p = 5;
  }
  free(p);
  p = NULL;
  return 0;
}


分析:


如果开辟空间过大,malloc开辟空间失败,返回NULL空指针,这时对指针解引用操作,程序就会奔溃。


最好的方法是对p是否为空指针进行判断,如果为空指针则打印错误信息,并退出函数。


3.2 动态开辟空间的越界访问

int main()
{
  int* p = (int*)malloc(20);
  if (p == NULL)
  {
    return 1;
  }
  //使用
  int i = 0;
  for (i = 0; i < 20; i++)//把20当做元素个数了
  {
    *(p + i) = i;//严重越界
  }
  //释放
  free(p);
  p = NULL;
  return 0;
}

分析:


malloc开辟了20个字节的空间,但是我误以为20为元素个数,造成了严重的越界访问,导致程序奔溃。


一定要搞懂函数的意思,在对指针进行操作时候看清楚!!!



3.3 对非动态开辟的内存使用free释放

int main()
{
  int num = 10;
  int* p = &num;
  //...
  free(p);
  p = NULL;
  return 0;
}


分析:


平时创建的局部变量在栈空间上开辟,当作用域结束,变量会自动销毁。而free只作用于在堆区上开辟的空间,如果将平常开辟的内存进行释放,程序会奔溃。编译器会很凌乱,表示这届程序员真难带!!!


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

int main()
{
  int* p = (int*)malloc(40);
  if (p == NULL)
  {
    return 1;
  }
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    *p = i;
    p++;//p改变了
  }
  //释放
  free(p);
  p = NULL;
  return 0;
}


分析:

p在使用过程中,进行了调整,p不再指向原来动态内存开辟的空间的起始位置。这块空间可能是动态开辟内存的一部分,也可能完全不适于开辟的空间。这时运行程序,程序依然会奔溃。


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

int main()
{
  int* p = malloc(40);
  if (p == NULL)
  {
    return 1;
  }
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    *(p + i) = i;
  }
  free(p);//已经释放过了
    //p = NULL//加上这个就不会奔溃
  //...继续写代码
  free(p);//忘记已经释放过了
  return 0;
}


分析:


当我们对一块动态内存进行释放后,接着写代码,然后忘记自己已经对这块空间进行释放。于是我们继续释放,当程序运行起来时,程序会奔溃。


要牢记一个malloc/calloc对应一个free。


如果我们在这里把p = NULL,就不会有问题了。因为free对空指针时不会操作的。



3.6 动态开辟内存忘记释放(内存泄漏)

//函数会返回动态开辟空间的地址,记得在使用之后释放
int* get_memory()
{
  int* p = (int*)malloc(40);
  //...
  return p;
}
int main()
{
  int* ptr = getmemory();
  //使用
  //没释放
  return 0;
}


分析:


函数返回了动态内存开辟的空间,我们可以对其进行使用。但是一定要释放,否则就会出现内存泄漏,也就是"吃内存"的情况。


在我们设计这个函数时就应该写好相应注释,提醒使用者。使用者也应该养成良好的习惯,对动态内存开辟的空间进行释放。



4. 几个经典的笔试题


4.1 题目1


下列程序运行结果是什么?


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;
}

运行结果:

fed0d584980a680f44bf2a13feeb1b5c.png



分析:


程序奔溃了。这里有两个问题。


第一个问题:


GetMemory函数传参传str本身,p是str的一份临时拷贝。在函数中使用p开辟一块100个字节的动态内存的空间。也就相当于p改变了,但是str本身并没有改变。


回到Test函数中,str依然是NULL空指针。这时对str进行字符串拷贝。会对空指针进行解引用操作。程序奔溃。


值得一提的是这里的printf(str)并没有问题。可以通过一个简单的例子来证明:我们平时可以通过printf("hello")把hello打印出来,同样的我们也可以把字符串的首元素地址放入指针中,通过指针打印出字符串。因为printf("hello")是把h的地址传给了printf,这样打印没问题,那么我把其他部分省略,我的意思也是把地址传给printf,然后直接打印字符串。


第二个问题:


malloc开辟的空间没有释放。但是如果我们想释放也无法释放,因为在GerMemory函数中存放开辟空间地址的指针由于退出函数被释放了。返回Test函数后没人知道这块空间在哪里,也没法释放。

所以这个函数实际上是存在着很严重的问题的,所以我们接下来就将其改对。


正确写法:


   传址调用,直接改变str

void GetMemory(char** p)
{
    *p = (char*)malloc(100);
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str);
    strcpy(str, "hello world");
    printf(str);
    //释放
    free(str);
    str = NULL;
}
int main()
{
    Test();
    return 0;
}


分析:

我们要改变str的值,那么就把str的地址传入。str是一级指针,那么&str就需要用二级指针接收。通过解引用,找到str,将动态开辟空间的首地址放入p中。在使用完之后对空间进行释放。

  • 参数无意义,返回值改变str
char* GetMemory(char* p)
{
    p = (char*)malloc(100);
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
    //释放
    free(str);
    str = NULL;
}
int main()
{
    Test();
    return 0;
}


分析:


这种写法也行。但是这里的参数其实没有实际的意义,我完全可以省略参数,在函数体内创建变量,开辟动态空间,然后返回起始地址。这种方法也行,但是我不是很推荐。

以上两种方法运行结果:

26ef26a595be95119db434deff66f39a.png


4.2 题目2

下列程序运行结果是什么?

char* GetMemory(void)
{
    char p[] = "hello world";
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}
int main()
{
    Test();
    return 0;
}


运行结果:

4fb7cd604ebfd0d496bd51e3b5ddcfa1.png


分析:


str里存放的是空指针,然后调用GetMemory函数,在函数中在栈空间上开辟一个数组,返回p(指向数组首元素位置)。但是出了函数,在函数中开辟的数组就会被销毁。当str接收返回值时就会接收被销毁空间的地址,当我对str进行打印时,这块空间已经还给操作系统了,这块空间可能被更改,也可能没更改。当前我们对其进行打印是乱码。


这就是典型的返回栈空间地址的问题!!!


一个小细节:


刚刚说返回函数栈空间的地址不对,那么这个函数对不对?


int test()
{
  int a = 10;
  return a;
}
int main()
{
  int ret = test();
  printf("%d\n", ret);
  return 0;
}


分析:


这个函数是完全正确的。当局部变量a返回时,会把a放到寄存器中,假设我们这个寄存器为eax,然后a销毁。再通过eax把返回值带回。


那么这个呢?

int* test()
{
  int a = 10;
  return &a;
}
int main()
{
  int* p = test();
  printf("%d\n", *p);
  return 0;
}


分析:


这就是典型的返回函数栈空间的地址。

我主函数中的p指向的空间a已经被释放,属于野指针,如果通过指针去访问,就是非法访问。

但是大家可能会有疑惑,那我这个运行结果怎么解释:

c19a787e8471a38c107a7dece54c41a0.png


这个其实是巧合。a所在的空间恰好没有被修改,如果我们坚信这个是对的,以后肯定是会翻车的!!!

如果我稍加改变,在打印*p之前打印一句话,例如这样:



int main()
{
  int* p = test();
    printf("hello\n");
  printf("%d\n", *p);
  return 0;
}


09385b5b28502a91210f1536c9cb80be.png


这里仅仅是增加了一句话就改变了*p的值,这是为什么?


看过我之前函数栈帧博客的,可能好理解些,接下来简单说一下原理:


当我们调用test函数时,在main函数上方需要开辟test函数的函数栈帧。栈空间使用习惯是从高地址向低地址使用。首先在栈帧最下方开辟a变量所需空间,当返回时则将*p放入寄存器中,将值带回,test函数栈帧被销毁。这一时刻很巧,a的值也没有改变。但是如果我们在打印*p前再使用了printf函数来打印一句话。这个printf函数可能就会在原先被释放的test函数栈帧的基础上开辟栈帧空间,这时a空间中的数据可能就会被修改,这就是6的来源。



4.3 题目3


下列程序运行结果是什么?


void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
int main()
{
    Test();
    return 0;
}


运行结果:

8aaf246d56519c3ed12ddad29fd6948d.png


分析:

str起始为NULL,将&str和开辟空间大小传给GetMemory函数,函数在内部通过*p找到str空间,将动态开辟空间的起始地址放入str中,在通过strcpy进行拷贝,拷贝也成功了,最后打印也没问题。

这个过程看似一气呵成,但是缺了释放动态开辟的空间!!!及时释放非常重要!!!


正确写法:

void GetMemory(char** p, int num)
{
    *p = (char*)malloc(num);
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
    free(str);
    str = NULL;
}
int main()
{
    Test();
    return 0;
}


运行结果:

f1c66cecad04668b1835922f3d61ac73.png


4.4 题目4


下列程序运行结果是什么?

void Test(void)
{
  char* str = (char*)malloc(100);
  strcpy(str, "hello");
  free(str);
  if (str != NULL)
  {
    strcpy(str, "world");
    printf(str);
  }
}
int main()
{
  Test();
  return 0;
}


运行结果:

bae7c1f8bc513bbad2187fc2c4d915e3.png


分析:


malloc开辟了一块100个字节的空间,放入str中,strcpy也将"hello"放入了str中,然后我就对str空间进行释放了。但是我没有置为空指针。所以下面的if语句是会执行的,这时我使用了被释放的str,str为野指针,为非法访问。在对str进行strcpy将world放入str中,再进行打印。


虽然跑出了结果,但是它本质上是错的,只能说明编译器大意了,没有闪(doge)。我们还是要发挥主观能动性,自己发现错误,毕竟我们是程序员。


正确写法:


这个代码其实槽点挺多的,首先它释放空间后没有置空。其次它也没有开辟完空间就对str是否为空指针进行判断,所以我们不妨对它进行一个大整改。


我们在释放完空间之后直接将str置为空指针。让下面的if语句起到作用,就达到了我们原本的目的。



void Test(void)
{
  char* str = (char*)malloc(100);
  if (str == NULL)
  {
    return;
  }
  strcpy(str, "hello");
  free(str);
  str = NULL;
  if (str != NULL)
  {
    strcpy(str, "world");
    printf(str);
  }
}
int main()
{
  Test();
  return 0;
}

运行结果:

9c95c6e68b188a652e8c5126fa2e72fc.png


5. 结语


到这里,本篇博客就到此结束了。相信大家对动态内存管理也有了一定的了解。动态内存管理在C语言中是一块非常重要的知识,还是希望大家可以熟练掌握。

相关文章
|
2月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
40 3
|
19天前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
31 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
19天前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
48 6
|
23天前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
40 6
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
101 13
|
24天前
|
大数据 C语言
C 语言动态内存分配 —— 灵活掌控内存资源
C语言动态内存分配使程序在运行时灵活管理内存资源,通过malloc、calloc、realloc和free等函数实现内存的申请与释放,提高内存使用效率,适应不同应用场景需求。
|
1月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
57 11
|
24天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
1月前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
52 11
|
22天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
51 1
下一篇
DataWorks