为什么存在动态内存分配

  在之前的学习中,我们所掌握的在内存中开辟空间的方式有:

    ①在栈上开辟sizeof(数据类型)大小的空间;

    ②在栈上开辟sizeof(数据类型)*个数大小的连续的空间;

  上述所说的这些方法开辟的空间有两个特点:

    ①开辟的空间大小是固定的;

    ②数组在定义时,必须表明申请的空间大小,也就是数组的长度,不能使用变量代替,他所需要的内存空间在编译时分配;

  那么问题来了,假设此时我们需要一块空间,而这块空间是在程序运行时才能知道大小,拿该怎么办呢?这时候就需要用到动态内存分配,顾名思义,就是在你程序运行之后,你需要一块内存空间,此时系统会动态的分配一块空间给你,后续还可以动态的修改这块内存空间,十分的灵活。

动态内存分配函数

  malloc和free

    在动态内存分配中,最重要的两个函数就是malloc和free函数,这两个是成双成对出现的。

    动态内存开辟函数:malloc()

void* malloc(size_t size);

 

    这个函数向内存申请一块连续可用的空间,并返回指向这块内存空间的指针,需要注意以下几点:

      ①size的单位是字节,所以申请的空间是size个字节;

      ②如果开辟成功,则返回一个指向开辟好空间的指针;

      ③如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做判空检查,如果未进行,那么后续就会对野指针进行一些列的操作,会出现未定义行为;

      ④返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定,我们可以对申请到的空间进行强制类型转换,就可以得到我们想要的数据类型的空间;

      ⑤如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器;

    空间是由我们自己开辟的,所以系统不会随随便便的帮我们释放掉,在程序进程未结束时,我们就不需要这块内存了该怎么办?需要用到动态内存释放和回收函数:free()

void free (void* ptr);

    prt为动态申请内存成功后返回的指针,free函数就用来释放掉动态开辟的内存,需要注意以下几点:

      ①如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的;

      ②如果参数 ptr 是NULL指针,则函数什么事都不做;

    函数介绍完了,下面举个例子演示一下如何申请释放内存;不过在使用之前我么需要包含他所对应的头文件<stdlib.h>,这个头文件大家应该不陌生吧,在前面我的代码中经常出现,一直没有用武之地,其实是在这里准备着呢;

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
    int n = 0;
    printf("请输入要分配的数组的内存空间的大小:");
    scanf("%d", &n);
    int* array = (int*)malloc(sizeof(int) * n);
    if (array != NULL) {
        printf("分配成功!\n");
        printf("请给数组赋值:\n");
        for (int i = 0; i < n; i++)
        {
            scanf("%d", &array[i]);
        }
        for (int i = 0; i < n; i++)
        {
            printf("%d\t", array[i]);
        }
        printf("\n");
    }
    free(array);
    array = NULL;
    printf("数组已经释放!\n");
    return 0;
}

  calloc

    在C语言中还提供了一个动态内存分配的函数calloc,他与free也是搭配使用的,申请一块空间,然后自己手动释放;

void* calloc(size_t num,size_t,size);

 

    这个函数的功能是为 num 个大小为 size(单位为字节) 的元素开辟一块空间,并且把空间的每个字节初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,所以malloc函数需要注意的点,他也需要注意。举个例子:

#include <stdio.h>  
#include <stdlib.h>  
int main()  
{  
  int n = 0;  
  printf("请输入数组长度:");  
  scanf("%d", &n);  
  int* array = (int*)calloc(n, sizeof(int));
  if(array!=NULL){
  for(int i = 0; i < n; i++)  {  
    printf("%d\n", array[i]);  
  }  
  }
  free(array);
  array=NULL;
  return 0;
}                                           

    其实这个函数的作用比较鸡肋,因为如果申请的内存空间很大时,初始化全为0这一步将会浪费一些时间,而且在实际开发中并不一定要将内存全部初始化为0,就算需要初始化全为0,那么我们可以使用之前我介绍到的memset()函数即可。

  realloc

    在写代码的过程中,如果我们申请到的内存空间太大或者太小,需要调整该怎么办?是重新申请一块合适大小的内存空间,然后将原来的数据搬运过去吗?可能是,也可能不是,具体是怎么样的,在下面为大家回答。

    在C语言中为我们提供了一个扩容函数realloc,通过这个函数我们就可以对申请到的内存大小进行调整;

void* realloc(void* ptr, size_t size);

    ptr是要调整的内存地址,size为调整之后新大小,返回值为调整之后的内存起始位置。realloc在调整内存空间的是存在两种情况:

      1、如果目标内存空间后面有足够大小的空间则直接将后面的空间选定所需的大小归入目标空间中即可。(不需要搬运数据)
      2、如果目标空间后面没有足够大小的空间则在内存中重新寻找一片足够大小的空间进行开辟并且将原来的数据放入到新的空间中,然后将原内存空间释放掉。(需要将数据搬运到新开辟的空间中)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main(){
    int n = 0;
    printf("请输入数组的长度:");
    scanf("%d", &n);
    int* array = (int*)malloc(sizeof(int) * n);
    if (array != NULL) {
        //正常业务执行
    }
    else {
        exit(-1);
    }
    printf("请输入目标扩容的大小:");
    scanf("%d", &n);
    array = (int*)realloc(array, n * sizeof(int));
    if (array != NULL) {
        printf("扩容成功!\n");
        printf("请给数组赋值:");
        for (int i = 0; i < n; i++){
            scanf("%d", &array[i]);
        }
        printf("打印数组:");
        for (int i = 0; i < n; i++){
            printf("%d\n", array[i]);
        }
    }
    else {
        printf("申请失败!\n");
    }
    free(array);
    array = NULL;
    return 0;
}

动态内存的常见问题

  动态内存分配正因为它的灵活性,所以很容易出现问题,下面列举出一些问题,希望大家在以后写代码时,不会犯这样的错误:

1、对NULL指针的解引用操作。
void test()
{
//INT_MAX表示在这个编译器上,int型数据所能表示的最大值,所以这样子申请一定会失败,返回一个NULL
 int *p = (int *)malloc(INT_MAX/4);
//这里没有对p进行判空操作,所以会对NULL进行解引用操作,因此我们在申请一块内存之后,一定要进行判空操作
 *p = 20;
 free(p);
}

2、对动态开辟空间越界访问。
void test()
{
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
 exit(EXIT_FAILURE);
 }
 for(i=0; i<=10; i++)
 {
//当i是10的时候越界访问,在动态申请到的连续内存相当于是一个数组,所以一定要注意下标的大小,不要越界了
 *(p+i) = i;
 }
 free(p);
}

3、对非动态内存使用free释放。
void test()
{
 int a = 10;
 int *p = &a;
//free函数只能对动态申请的内存进行释放操作,非动态申请的内存不能进行操作
 free(p);
}

4、释放一块动态开辟内存的一部分。
void test()
{
 int *p = (int *)malloc(100);
 p++;
//p不再指向动态内存的起始位置,此时再释放就不正确了,会出现未定义行为
 free(p);
}

5、对同一块内存多次释放。
void test()
{
 int *p = (int *)malloc(100);
 free(p);
//重复释放,所以我们在释放掉一块内存之后就将这个指针赋值为NULL,free对NULL进行操作不会有任何事情发生,不会出错
 free(p);
}

6、动态开辟内存忘记释放。
void test()
{
 int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
}
int main()
{
 test();
//当我们一直申请新的内存空间而不释放的话,就会出现内存空间满了,无法再进行内存的申请,就会出现内存泄漏问题,这是非常严重的bug,所以我们在使用过申请的内存之后,一定要记得释放
 while(1);
}

  下面举出几个经典的笔试题,看看大家能不能找出问题所在:

题目一:
void
GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); }
//错误:没有free,内存泄漏;malloc返回值未进行判空操作;
//GetMemory函数的参数也未进行判空;调用GetMemory函数是进行值传递,修改不了str中的内容,后续的拷贝操作就会触发内存访问越界; 题目二:
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); }
//错误:p是在GetMemory函数中定义的,是一个局部变量,当函数运行结束之后,他也就随之被释放了,所以str操作的是一个野指针,对str操作会出现未定义行为
题目三:
void GetMemory2(char **p, int num)//注意,这里传进来的是指针的地址,用二级指针接受,所以和第一题不同,这里str成功的传了进去 { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); }
//错误:没有进行free操作;malloc没有判空;函数的参数也未进行合法性检验
题目四:
void Test(void) { char *str = (char *) malloc(100); strcpy(str, “hello”); free(str); if(str != NULL) { strcpy(str, “world”); printf(str); } }
//错误:malloc未进行判空操作;free操作不会把指针中的地址变为NULL,所以if语句可以进去,这样就对一块释放过的内存进行操作,会出现未定义行为

 

c/c++程序的内存开辟

  下面画一幅图来为大家演示一下:

  c/c++程序内存分配的几个区域:

    1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些 存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

    2. 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。

    3. 数据段(静态区)(static):存放全局变量、静态数据(static修饰)。程序结束后由系统释放。

    4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

  栈和堆的区别:

    ①栈的空间比较小,堆的空间超级大;

    ②栈上申请释放内存速度极快(本质是一条机器指令),堆上申请释放内存速度很慢(涉及到操作系统内核上的一些工作);

  用栈还是用堆:(实际开发中)

    内存小、频繁申请释放——栈

    内存大、不需要频繁申请释放——堆

    内存小、不需要频繁申请释放——都行

    内存大、也需要频繁申请释放——堆(原理是内存池,你问我为什么?我也不知道,大佬说的!)

柔性数组

  这是c99中定义的新语法:结构体最后一个元素允许是未知大小的数组,这就是柔性数组成员。

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;
//上面的数组中大小设为0;有些编译器上可能不通过,可以写成下面形式
typedef struct st_type
{
 int i;
 int a[];//柔性数组成员
}type_a;

 

  柔性数组的特点:

    ①结构中的柔性数组成员前面必须至少一个其他成员;     ②sizeof返回的这种结构大小不包括柔性数组的内存;

    ③包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小;

  柔性数组的使用

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++)
{
 p->a[i] = i;
}
free(p);

  柔性数组的优点:方便内存释放,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存给释放掉。

  柔性数组的缺点:限制条件太死,必须是在结构体中,而且是结构体的最后一个元素,所以基本上不会用到,只在某些特定的地方才会用到。