C 语言内存管理、动态分配内存、野指针

文章目录

  • C 语言内存管理、动态分配内存、野指针
  • 前言:
  • 1.内存分区
    • 1.1 代码区
    • 1.2.1 全局初始化数据区(静态数据区data段)
    • 1.2.2 未初始化数据区(静态数据区bss段)
    • 1.3 栈区
    • 1.4 堆区
      • 1.4.1 malloc与free
      • 1.4.2 calloc和realloc
  • 2 动态分配内存
  • 3 重新调整内存的大小和释放内存
  • 4 野指针及其原因
  • 5 实例分析
  • 6 内存管理的目标:
  • 6.1 分页内存
  • 微信扫码进交流群

前言:

程序员们编写内存管理程序时,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的,除非你转型写JAVA等自动内存管理的语言。而内存管理一定要正确使用指针,不会正确使用指针,肯定算不上是合格的程序员,建议养成使用“调试器逐步跟踪程序”的习惯,这样会让你真进步。

1.内存分区

C源代码经过预处理、编译、汇编和链接4步生成一个可执行程序。

程序在没有运行之前,即没有被加载到内存前,可执行程序内部已经分好3段信息,分别是代码区(text)、数据区(data)和未初始化数据区(bss)三个部分。(也有人把data和bss合起来叫做静态区或全局区)。

运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区、数据区和未初始化数据区(静态区)之外,还额外增加了栈区和堆区。

计算机中的内存是分区来管理的,程序和程序之间的内存是独立的,不能互相访问,比如QQ和浏览器分别所占的内存区域是不能相互访问的。而每个程序的内存也是分区管理的,一个应用程序所占的内存可以分为很多个区域,我们需要了解的主要有四个区域,通常叫内存四区:

1.1 代码区

程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令、常量字符串等)都加载到代码区,这块内存在程序运行期间是不变的。

代码区是平行的,里面装的就是一堆指令,放CPU执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,为了防止程序意外地修改了它的指令。

函数也是代码的一部分,故函数都被放在代码区,包括main函数。

注意:"int a = 0;"语句可拆分成"int a;“和"a = 0”,定义变量a的"int a;"语句并不是代码,它在程序编译时就执行了,并没有放到代码区,放到代码区的只有"a = 0"这句。

1.2.1 全局初始化数据区(静态数据区data段)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

1.2.2 未初始化数据区(静态数据区bss段)

存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空(NULL)

1.3 栈区

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

栈(stack)是一种先进后出的内存结构,所有的自动变量、函数形参都存储在栈中,这个动作由编译器自动完成,我们写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。当一个自动变量超出其作用域时,自动从栈中弹出。

  • 每个线程都有自己专属的栈;
  • 栈的最大尺寸固定,超出则引起栈溢出;
  • 变量离开作用域后栈上的内存会自动释放。
//实验一:观察代码区、静态区、栈区的内存地址#include "stdafx.h"
int n = 0;
void test(int a, int b)
{printf("形式参数a的地址是:%d\n形式参数b的地址是:%d\n",&a, &b);
}
int _tmain(int argc, _TCHAR* argv[])
{static int m = 0;
int a = 0;
int b = 0;
printf("自动变量a的地址是:%d\n自动变量b的地址是:%d\n", &a, &b);
printf("全局变量n的地址是:%d\n静态变量m的地址是:%d\n", &n, &m);
test(a, b);
printf("_tmain函数的地址是:%d", &_tmain);
getchar();
}

结果分析:自动变量a和b依次被定义和赋值,都在栈区存放,内存地址只相差12,需要注意的是a的地址比b要大,这是因为栈是一种先进后出的数据存储结构,先存放的a,后存放的b,形象化表示如上图(注意地址编号顺序)。一旦超出作用域,那么变量b将先于变量a被销毁。这很像往箱子里放衣服,最先放的最后才能被拿出,最后放的最先被拿出。

//实验二:栈变量与作用域
#include "stdafx.h"
//函数的返回值是一个指针,尽管这样可以运行程序,但这样做是不合法的,因为
//非要这样做需在x变量前加static关键字修饰,即static int a = 0;
int *getx()
{int x = 10;return &x;
}int _tmain(int argc, _TCHAR* argv[])
{int *p = getx();*p = 20;printf("%d", *p);getchar();
}

这段代码没有任何语法错误,也能得到预期的结果:20。但是这么写是有问题的:因为int p = getx()中变量x的作用域为getx()函数体内部,这里得到一个临时栈变量x的地址,getx()函数调用结束后这个地址就无效了,但是后面的p = 20仍然在对其进行访问并修改,结果可能对也可能错,实际工作中应避免这种做法,不然怎么死的都不知道。不能将一个栈变量的地址通过函数的返回值返回,切记!

另外,栈不会很大,一般都是以K为单位。如果在程序中直接将较大的数组保存在函数内的栈变量中,很可能会内存溢出,导致程序崩溃(如下实验三),严格来说应该叫栈溢出(当栈空间以满,但还往栈内存压变量,这个就叫栈溢出)

//实验三:看看什么是栈溢出
int _tmain(int argc, _TCHAR* argv[])
{char array_char[1024*1024*1024] = {0};array_char[0] = 'a';printf("%s", array_char);getchar();
}

1.4 堆区

堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。更重要的是堆是一个大容器,它的容量要远远大于栈,这可以解决上面实验三造成的内存溢出困难。

一般比较复杂的数据类型都是放在堆中。但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。对于一个32位操作系统,最大管理管理4G内存,其中1G是给操作系统自己用的,剩下的3G都是给用户程序,一个用户程序理论上可以使用3G的内存空间。堆上的内存必须手动释放(C/C++),除非语言执行环境支持GC(如C#在.NET上运行就有垃圾回收机制)。那堆内存如何使用?

接下来看堆内存的分配和释放:

1.4.1 malloc与free

void *malloc(int num)

在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。单位为字节(Byte),函数返回void *指针;

void free(void *address)

该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。

free负责在堆中释放malloc分配的内存。malloc与free一定成对使用。看下面的例子:

//实验四:解决栈溢出的问题
#include "stdafx.h"
#include "stdlib.h"
#include "string.h"void print_array(char *p, char n)
{int i = 0;for (i = 0; i < n; i++){printf("p[%d] = %d\n", i, p[i]);}
}int _tmain(int argc, _TCHAR* argv[])
{char *p = (char *)malloc(1024*1024*1024);//在堆中申请了内存memset(p, 'a', sizeof(int) * 10);//初始化内存int i = 0;for (i = 0; i < 10; i++){p[i] = i + 65;}print_array(p, 10);free(p);//释放申请的堆内存getchar();
}

程序可以正常运行,这样就解决了刚才实验三的栈溢出问题。堆的容量有多大?理论上讲,它可以使用除了系统占用内存空间之外的所有空间。实际上比这要小些,比如我们平时会打开诸如QQ、浏览器之类的软件,但这在一般情况下足够用了。实验二中说到,不能将一个栈变量的地址通过函数的返回值返回,如果我们需要返回一个函数内定义的变量的地址该怎么办?可以这样做:

//实验五:
#include "stdafx.h"
#include "stdlib.h"int *getx()
{int *p = (int *)malloc(sizeof(int));//申请了一个堆空间return p;
}int _tmain(int argc, _TCHAR* argv[])
{int *pp = getx();*pp = 10;free(pp);
}

这样写是没有问题的,可以通过函数返回一个堆地址,但记得一定用通过free函数释放申请的堆内存空间。"int *p = (int *)malloc(sizeof(int));"换成"static int a = 0"也是合法的。因为静态区的内存在程序运行的整个期间都有效,但是后面的free函数就不能用了!

1.4.2 calloc和realloc

void *calloc(int num, int size)

在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。

void *realloc(void *address, int newsize)

该函数重新分配内存,把内存扩展到 newsize

calloc和realloc,也用来在堆中申请内存空间的函数还有calloc和realloc,用法与malloc类似。

2 动态分配内存

编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:

char name[100];

如果预先不知道需要存储的文本长度,例如存储有关一个主题的详细描述。在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main()
{char name[100];char *description;strcpy(name, "Zara Ali");/* 动态分配内存 */description = (char *)malloc( 200 * sizeof(char) );if( description == NULL ){fprintf(stderr, "Error - unable to allocate required memory\n");}else{strcpy( description, "Zara ali a DPS student in class 10th");}printf("Name = %s\n", name );printf("Description: %s\n", description );
}

当上面的代码被编译和执行时,它会产生下列结果:

Name = Zara Ali
Description: Zara ali a DPS student in class 10th

上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示:

calloc(200, sizeof(char));

上面有个fprintf函数,实例演示下 fprintf() 函数的用法。

#include <stdio.h>
#include <stdlib.h>int main()
{FILE * fp;fp = fopen ("file.txt", "w+");fprintf(fp, "%s %s %s %d", "We", "are", "in", 2014);fclose(fp);return(0);
}编译并运行上面的程序,它将创建文件 file.txt,内容如下:We are in 2014

当动态分配内存时,我们对内存有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。

3 重新调整内存的大小和释放内存

当程序退出时,操作系统会自动释放所有分配给程序的内存,建议不需要内存时,都应该调用函数 free() 来释放内存。

或者,您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。让我们使用 realloc() 和 free() 函数,再次查看上面的实例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main()
{char name[100];char *description;strcpy(name, "Zara Ali");/* 动态分配内存 */description = (char *)malloc( 30 * sizeof(char) );if( description == NULL ){fprintf(stderr, "Error - unable to allocate required memory\n");}else{strcpy( description, "Zara ali a DPS student.");}/* 假设您想要存储更大的描述信息 */description = (char *) realloc( description, 100 * sizeof(char) );if( description == NULL ){fprintf(stderr, "Error - unable to allocate required memory\n");}else{strcat( description, "She is in class 10th");}printf("Name = %s\n", name );printf("Description: %s\n", description );/* 使用 free() 函数释放内存 */free(description);
}

当上面的代码被编译和执行时,它会产生下列结果:

Name = Zara Ali
Description: Zara ali a DPS student.She is in class 10th

您可以尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。

4 野指针及其原因

野指针指的是指向“垃圾”内存的指针,不是NULL指针。

前面第2节动态分配内存里提了使用指针分配动态内存,什么时候会出现“野指针”呢?主要有以下原因:

(1)指针变量没有被初始化。指针变量和其它的变量一样,若没有初始化,值是不确定的。也就是说,没有初始化的指针,指向的是垃圾内存,非常危险。

#include <stdio.h>
int main(int argc, const char * argv[]) {int *p;printf("%d\n", *p);*p = 10;printf("%d\n", *p);return 0;
}

(2)指针p被free之后,没有置为NULL。free函数是把指针所指向的内存释放掉,使内存成为了自由内存。但是,该函数并没有把指针本身的内容清楚。指针仍指向已经释放的动态内存,这是很危险。

程序员稍有疏忽,会误以为是个合法的指针。就有可能再通过指针去访问动态内存。实际上,这时的内存已经是垃圾内存了。
关于野指针会造成什么样的后果,这是很难估计的。若内存仍然是空闲的,可能程序暂时正常运行;若内存被再次分配,又通过野指针对内存进行了写操作,则原有的合法数据,会被覆盖,这时,野指针造成的影响将是无法估计的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {int n = 5, *p, i;if ((p = (int *)malloc(n * sizeof(int))) == NULL){printf("malloc error\n");return 0;}memset(p, 0, n * sizeof(int));for (i = 0; i < n; i++){p[i] = i+1;printf("%d ", p[i]);}printf("\n");printf("p=%p *p=%d\n", p, *p);free(p);printf("after free:p=%p *p=%d\n", p, *p);*p = 100;printf("p=%p *p=%d\n", p, *p);return 0;
}
说明:该程序中,故意在执行了“free(p)”之后,通过野指针p对动态内存进行了读写,程序正常执行,也在预料之中。前面已经分析过,内存释放后,若继续访问甚至修改,后果是不可预料的。

(3)指针操作超越了变量的作用范围。指针操作时,由于逻辑上的错误,导致指针访问了非法内存,这种情况让人防不胜防,只能依靠程序员好的编码风格,已及扎实的基本功。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {int a[5] = {1, 9, 6, 2, 10}, *p, i, n;n = sizeof(a) / sizeof(n);p = a;for (i = 0; i <= n; i++){printf("%d ", *p);p++;}printf("\n");*p = 100;printf("*p=%d\n", *p);return 0;
}
说明:该程序故意出了两个错误,一是for循环的条件“i <= n”,p指针指向了数组以外的空间。二是“*p = 100”,对非法内存进行了写操作。

(4)不要返回指向栈内存的指针。

在函数中,详细介绍了指针函数,指针函数会返回一个指针。在主调函数中,往往会通过返回的指针,继续访问指向的内存。因此,指针函数不能返回栈内存的起始地址,因为栈内存在函数结束时会被释放。

5 实例分析

const int num[3]={1,2,3};
void main()
{char *b =NULL;int c =128;b = (char*)malloc(1024*sizeof(char));UpdateCounter(b,c,num[1]);free(b);printf("c=%d,num[1]=%d\n",c,num[1]);}static int UpdateCounter(char *b,int c,int num1)
{int d =0;const int e[4]={11,22,33,44};if (b[c++]>e[num1++]){d++;}return d;
}

部分分析如下:

main函数和UpdateCounter为代码的一部分,故存放在代码区

数组a默认为全局变量,故存放在静态区

main函数中的"char *b = NULL"定义了自动变量b(variable),故其存放在栈区

接着"b = (char )malloc(1024sizeof(char));"向堆申请了部分内存空间,故这段空间在堆区

6 内存管理的目标:

  • 地址保护:一个程序不能访问另一个程序地址空间。
  • 地址独立:程序发出的地址应与物理主存地址无关。

6.1 分页内存

一般操作系统管理内存时,最小单位不是字节,而是内存页(32位操作系统的内存页一般是4K)。

比如,初次申请1K内存,操作系统会分配1个内存页,也就是4K内存。4K是一个折中的选择,因为:内存页越大,内存浪费越多,但操作系统内存调度效率高,不用频繁分配和释放内存;内存页越小,内存浪费越少,但操作系统内存调度效率低,需要频繁分配和释放内存。嵌入式系统的内存内存资源很稀缺,其内存页会更小,因此在嵌入式开发当中需要特别注意。

这要进一步研究,因为这就是要扩展到具体操作系统上去了,推荐一本书吧,感兴趣好好啃。《操作系统之设计哲学》

微信扫码进交流群

敲重点!菜鸡Ai微信交流群成立

关注公众号,后台回复,进群。

交流方向已涵盖:PyTorch、TensorFlow、MXNET、Unity3D、虚幻、VR、AR、XR、论文投稿&交流、实习Offer、面试面经、算法刷题、NLP、XLNET、ERNIE2.0、BERT系列、目标检测、图像分割、目标跟踪、人脸检测&识别、OCR、姿态估计、SLAM、医疗影像、Re-ID、GAN、自动驾驶、强化学习、模型剪枝&压缩、遥感图像、行为识别、视频理解、图像融合、图像检索等。

一定要备注:研究方向+地点+学校/公司+昵称(如行为识别+北京+北大+小丁),根据格式备注,才能通过且邀请进群

【C 语言必知必会】内存管理、动态分配内存、野指针相关推荐

  1. 【SQL必知必会】002-基础篇:了解SQL:一门半衰期很长的语言

    [SQL必知必会]002-基础篇:了解SQL:一门半衰期很长的语言 文章目录 [SQL必知必会]002-基础篇:了解SQL:一门半衰期很长的语言 一.概述 二.半衰期很长的 SQL 三.入门 SQL ...

  2. HBR推荐|迎战未来必知的10大管理创新趋势

    疫情加速改变了企业!疫后新常态下企业管理亟需与时俱进.大胆创新!因为原有许多管理方法和工具更已不再适用.我们更要站在巨人的肩膀上远眺未来,值此HBR百年纪念之际,洞察迎战未来必知的10大管理创新趋势, ...

  3. mysql必知必会_MySQL必知必会

    MySQL必知必会 联结的使用, 子查询, 正则表达式和基于全文本的搜索, 存储过程, 游标, 触发器, 表约束. 了解SQL 数据库基础 电子邮件地址薄里查找名字时, 因特网搜索站点上进行搜索, 验 ...

  4. mysql必学十大必会_MYSQL 学习(一)--启蒙篇《MYSQL必知必会》

    MYSQL必知必会 一. DDL 数据定义语言 Data Definition Language 是指CREATE,ALTER和DROP语句. DDL允许添加/修改/删除包含数据的逻辑结构,或允许用户 ...

  5. SQL Server必知必会

    SQL Server必知必会 2009-10-27-17:57:57 Structure     Query     Language:SQL 结构化       查询      语言 数据库产品: ...

  6. 致Emacs初学者+Emacs初学者必知必会

    原文链接:http://emacser.com/to-emacs-beginner.htm 需要专门花时间去学的软件为数不多, Emacs正是其中之一. 我周围的好多人在我的"鼓吹" ...

  7. SQL必知必会(一)SQL基础篇

    SQL基础 1.SQL语言 2.SQL开发规范 3.DB.DBS 和 DBMS 的区别是什么 4.SQL执行顺序 1.oracle中执行顺序 2.MYSQL执行顺序 3.sql关键字执行顺序 5. I ...

  8. 【系统分析师之路】系统分析师必知必会(需求分析篇)

    [系统分析师之路]系统分析师必知必会(需求分析篇) 系统分析师必知必会 需求分析篇 [系统分析师之路]系统分析师必知必会(需求分析篇) 1.什么是软件需求 2. 需求分类 2.1)业务需求 2.2)用 ...

  9. MySQL必知必会笔记(一)基础知识和基本操作

    第一章  了解MySQL     数据库       保存有组织的数据的容器.(通常是一个文件或一组文件) 人们经常使用数据库这个术语代替他们使用的软件.这是不正确的,确切的说,数据库软件应称为DBM ...

最新文章

  1. python不需要缩进的代码顶行编写_python程序快速缩进多行代码方法总结
  2. [LeetCode][JavaScript]Invert Binary Tree 反转二叉树
  3. TESTb需要向至少十几家应用商店提交上线审核,且每个应用商店要求的资料可能都不一样,异常繁琐
  4. 08 Tomcat+Java Web项目的创建和War的生成
  5. mir2disease:miRNA相关疾病数据库
  6. 数字IC四大岗位分析
  7. jeecms9自定义标签以及使用新创建的数据库表
  8. 恭喜宿主获得鸿蒙,我在混沌开学院
  9. C语言 sigaction函数捕捉信号 注册回调函数
  10. NiFi分享第一期-安全认证(证书认证)
  11. 研究生学习初入门之导师大致方向
  12. 核心微生物分析_科学网—微生物组核心OTU鉴定usearch otutab_core - 刘永鑫的博文...
  13. 编译kernel外部模块
  14. 一个完整的程序化交易系统包含了哪些因素?
  15. 请碟仙儿│一个区块链思想实验
  16. Cesium实现——日照分析
  17. Zabbix服务器端运行中 显示为 否 No 的解决方案
  18. POJ 1584 计算几何 凸包
  19. 巨人史玉柱经典创业语录
  20. 龙湖计算机学院,龙湖义务教育阶段学校招生电脑随机摇号圆满完成 15号上午统一公布录取结果...

热门文章

  1. Linux常用命令--文件搜索命令:压缩解压命令
  2. Windows Vista 5342下载?
  3. My SQL下载安装配置检查
  4. 三天开发一个系统,奖金3k【源码开源】
  5. G - 翻翻棋 FZU - 2230
  6. 苹果手机计算机怎么变高级,苹果手机中隐藏的7个高级功能
  7. 3DsMax—投影效果制作
  8. linux安装数据库乱码,Linux上Oracle安装前汉字乱码和安装后创建数据库乱码的解决方法...
  9. 前端学习从入门到高级全程记录之35(jQuery②)
  10. 脚扭了,悲剧----码农的身体多注意