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

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和浏览器分别所占的内存区域是不能相互访问的。而每个程序的内存也是分区管理的,一个应用程序所占的内存可以分为很多个区域,我们需要了解的主要有四个区域,通常叫内存四区:

【C 语言必知必会】内存管理、动态分配内存、野指针_第1张图片

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 
#include 
#include 
 
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 
#include 

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 
#include 
#include 
 
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 
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 
#include 
#include 
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 
#include 
#include 
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微信交流群成立

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

【C 语言必知必会】内存管理、动态分配内存、野指针_第2张图片

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

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

你可能感兴趣的:(基础相关,编程语言,堆栈,指针,c语言,内存管理)