这一节又是C语言重点,主要讲的是动态内存的分配,那就不罗嗦了直接开始。

内存空间的动态分配

在我们之前学习中,我们知道如果需要定义批量的数据,我们就会使用数组,但是大家应该都发现了它的一个缺点,那就是数组定义时候的长度必须的是常量,不能改变,这样我们使用的时候难免会遇到空间不足或者空间浪费的情况。
如果我们使用堆空间,就能够解决这个问题,并且可以随时释放。

动态内存空间分配的步骤:

  • 申请一个指针变量
  • 申请一块内存空间,并将其首地址赋给指针变量,此时便可以通过指针变量访问这片内存
  • 当我们使用完毕之后,需要释放这片内存空间

关于申请内存空间和释放内存空间主要涉及两个函数:
malloc--申请空间
free--释放空间

示例:

int *p = NULL;
//malloc这个函数的参数是我们需要申请空间的大小(字节)
p = (int *)malloc(sizeof(int));
*p = 10;
printf("%d",*p);
//释放申请的空间
free(p);
return 0;

注:
malloc 返回一个指向已分配空间的 void 指针,如果内存不足,则返回 NULL。
我们以后应该使用if判断堆空间申请是否成功。

使用堆和使用数组的差别

数组长度只能是常量,堆大小可以是变量

示例:

int main()
{
    //我想申请能够存储10个整数的空间
    //然后遍历输入
    //遍历输出
    int *p = NULL;
    int  num = 0;
    
    printf("请输入需要申请int的个数:\n")
    scanf_s("%d",&num);
    
    
    p = (int *)malloc(num * sizeof(int));

    //我们使用指针和数组是非常类似的
    //输入
    for (int i = 0; i < num; i++)
    {
        printf("请输入一个数:");
        scanf_s("%d", &p[i]);
    }
    //输出
    for (int i = 0; i < num; i++)
    {
        printf("%d ",p[i]);
    }

    free(p);
    p = NULL;

    return 0;
}                    

堆空间的释放

我们使用的数组会自动释放内存空间,但是我们使用堆空间的时候,需要自己释放内存口昂见,如果忘记释放,就会造成内存空间泄漏。

数组示例:

int main()
{
    int n = 0;
    int *p = NULL;
    printf("你需要存储多少个整数:");
    scanf_s("%d", &n);
    //int Arr[n];//数组长度必须是常量
    while(true)
    {
        int Arr[1024];
    }
    return 0;
}

我们使用数组申请的空间会自动释放。

堆示例:

int main()
{
    int n = 0;
    int *p = NULL;
    printf("你需要存储多少个整数:");
    scanf_s("%d", &n);
    //int Arr[n];//数组长度必须是常量
    while(true)
    {
        p = (int *)malloc(n * sizeof(int));
    }
    return 0;
}

我们申请的堆空间不会自动释放,他会占满你的内存。

关于悬空指针与野指针

悬空指针

当我们释放了内存之后,我们指针也应该指向NULL,因为那片内存空间已经不再属于这个指针,可能已经被别的代码或数数据占用,但是如果我们继续操作就会修改其不属于我们修改内容的内存。
这些释放之后没有赋值为NULL的指针被称为悬空指针。

示例:

int main()
{
    int *p = (int *)malloc(sizeof(int));
    
    free(p);
    
    *p = 10;    //继续使用了释放空间后的指针
    return 0;
}

野指针

指针被定义之后,但是没有初始化,这种指针被称为野指针。

示例:

int main()
{
    int *p;
    *p = 10;    //再使用野指针
        //这个指针没有指向任何一个地址,但是却对其指针所指向的内容赋值
    return 0;
}

总结:
指针变量,要么指向一个有意义地址(变量,数组,堆),要么指向NULL(0)
使用悬空指针或者野指针都会造成程序崩溃

一个程序的内存划分

通常来说,一个运行的程序,内存会划分为5个区域:
代码区、常量区、栈区、堆区、全局数据区

  • 代码区

    • 存放函数体的二进制代码
  • 常量区

    • 存放常量值,如常量字符串等,不允许修改,程序结束后由操作系统回收
  • 栈区

    • 由编译器自动分配和释放,用来存放函数的参数、局部变量等。其操作方式类似于数据结构中的栈
  • 堆区

    • 一般由程序员分配和释放(通过malloc/free、new/delete),若程序员没有释放,则程序结束时由操作系统回收。它与数据结构中的堆是两回事,分配方式类似于链表
  • 全局数据区

    • 全局变量和静态变量的存储是放在一块的,初始化的全局变量和初始化的静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由操作系统回收

示例:

int g_Num = 0;
void Fun(int nParam)
{
    printf("参数地址:%p\n", &nParam);
    static int nNum = 0;
    printf("静态局部:%p\n", &nNum);
}

int main()
{
    int nTest1 = 0;
    int nTest2 = 0;
    printf("局部变量:%p\n", &nTest1);
    printf("局部变量:%p\n", &nTest2);
    Fun(10);
    printf("全局变量:%p\n", &g_Num);

    int *p1 = (int *)malloc(10);
    int *p2 = (int *)malloc(10);
    printf("堆:%p\n", p1);
    printf("堆:%p\n", p2);

    printf("常量区:%p\n", "hello world");
    printf("常量区:%p\n", "hello 15pb");

    return 0;
}

其他内存操作函数

内存初始化函数memset,其原型为:
void *memset(起始地址,要设置的值,要设置多大区域);
一般用于给刚申请出来的内存设置一个初始值

内存拷贝函数memcpy,其原型为:
void *memcpy(目标地址,源数据地址,要拷贝多大区域);
一般用于把内存中的数据拷贝到另一块内存中

综合应用

在我们以后使用堆的时候,基本上都会大量使用指针,在我们才接触没多久的情况下,使用指针出错的概率相对较大,不过我们记住以下几个原则,能让我们少踩些坑:

  • 刚申请的动态内存的初始值是补确定的
  • 不能对同一指针(地址)连续两次free操作
  • 不能对指向静态内存区(全局变量)或栈区(局部变量)的指针应用free(但是可以对空指针NULL应用free),对一个指针应用free之后,它的值不会变,但他指向了一个无效的内存区,这也就是我们说的悬空指针。
  • 如果没有即使释放某块动态内存,并且将指针指向了别的地方,就会造成内存泄漏,执行malloc和free有一定的代价,所以数据量小的不应该放在动态内存中,并且应该避免频繁的申请和释放动态内存。

我们在进行内存区域的申请时,主要需要注意避免以下错误:

  • 没有成功分配内存但是我们却使用了它
  • 内存分配成功但是没有对他进行初始化就引用了它
  • 内存分配成功并且已经初始化,但操作越过了内存边界
  • 忘记释放内存,造成内存泄漏
  • 释放了内存却继续使用

综合示例:

int *p = NULL;
p = (int *)malloc(5 * sizeof(int));
meset(p,0,5*sizeof(int));
int num = 0;

for(int i = 0;i<5;i++)
{
    p[i] = i;
}
p = (int *)malloc(10 * sizeof(int));

free(p);

p[0] = 1;    //此时使用了悬空指针

//p = &Num

//free(p);//不能释放除了堆空间之外的空间
//free(p);//错误对一块内存空间进行释放两次
//p = NULL;//即使置为空,配合下面的判断,避免使用空指针

if(p!=NULL)
{
    //一般我们在使用指针之前,检测一下是不是空指针是一个好习惯
}
最后修改:2020 年 08 月 26 日
如果觉得我的文章对你有用,请随意赞赏