C的实用笔记35——为字符串开辟动态内存

时间:2021-02-03 01:21:17

1.啥是动态内存?存在哪里?

1、动态内存的引入:

  1. 问题(静态分配内存):不论是整型数组、字符型数组,数组中括号[ ]内是一个常量表达式,所以数组的每一维度的长度是在定义时就定死了的,也就是说所以我们不允许通过变量给数组每一维度长度赋值。这种数组,会在程序编译或运行的过程中,按照事先规定大小分配内存空间的分配方式,我们称为静态分配内存。细一点说(其中的概念在疑难杂症一章中细讲):
    1. 如果数组定义在函数内部,也就是说它是一个普通局部变量(in栈区),那么静态分配内存发生在程序运行阶段(函数调用阶段)。
    2. 如果数组定义在函数内部并且前面加了static,也就是说他是一个静态局部变量(in静态全局区),那么静态分配内存发生在程序运行阶段(函数调用阶段)。
    3. 如果数组定义在函数外部,也就是说它是一个全局变量(in静态全局区),那么静态分配内存发生在程序编译阶段。
  2. 需求(动态分配内存):在程序运行过程中,根据需要大小*分配所需空间,一般使用特定的函数进行分配,使用完毕后又将这些内存给释放掉。

2、动态内存在堆区:这在疑难杂症一章节中细讲。

3、知识点:

  1. size_t:类似名,它是被关键字typedef定义出来的,相当于unsigned int。
  2. void *:通用指针,任何类型的指针都可以给它赋值,如果用它当作函数的形参,那么就不用考虑指针类型不同带来的麻烦;它也可以给任何类型的指针赋值,如果用它当作函数的返回值并赋值给某个指针,那么就需要进行指针类型的强制转换
  3. NULL:它不是一个指针类型,它是一个符号常量,是宏定义出来的,实际上NULL就等于(void *)0。它的作用有两点:①NULL用于给指针变量初始化;②NULL作为指针函数的返回值,代表函数执行失败。
  4. 堆区:堆区是虚拟内存的一个分区,它专门用来存放动态内存。堆区变量的生长方向从低地址到高地址。堆区变量的生命周期从申请开始计算,如果人为地释放掉动态内存,那么堆区变量的生命周期就从申请到释放,如果不释放动态内存,那么堆区变量的生命周期就从申请到程序结束。
  5. 悬挂指针:意思是我们定义了一个指针,并让这个指针指向我们申请的动态内存的首地址,然后,①这个指针在指向别的内存地址之前没有把之前的动态内存释放掉,或者说②这个指针被当做普通局部变量在函数调用完毕后被释放掉之前没有把之前的动态内存释放掉。由于堆区变量的生命周期很长,那么我们就再也无法知道这个动态内存的首地址了,我们将由于人为失误不释放动态内存而导致的无法查询的动态内存的首地址称为悬挂指针,它是野指针的一种。
  6. 内存泄漏:悬挂指针累积,容易把内存空间消耗殆尽。

4、一些变量名含义:

  1. dest:目的地址
  2. src:来源地址
  3. ptr:指针,原来的地址
  4. size:字节长度
  5. newsize:新的字节长度
  6. nmemb:块数

2.动态内存分配常用的4个API,和使用时的必要的语句

1、知识点:库函数介绍,从函数三要素出发

  1. malloc函数
    1. 函数原型:void*   malloc(size_t   size);
    2. 操作数:①size是希望开辟的字节数
    3. 函数功能:开辟所需的动态内存空间,并返回一个指向动态内存首地址的通用指针。
      int len;
      scanf("%d", &len);			//自定义开辟长度
      
      int *p;		
      p = (int *)malloc(len*4);	//开辟的字节数是4的倍数
      
      char *str;
      str = (char *)malloc(len);	//开辟了len字节长度

  2. memset函数
    1. 函数原型:void*   memset(void   *ptr,     int   value,     size_t   num);
    2. 操作数:①通用指针ptr指针指向的内容要求是可写的,但它不一定是堆区的动态内存,可以是数组;②value是设置的值,如果操作的对象是整型数组,那么value就是数字,如果操作的对象是字符数组(字符串),那么value就是该数字对应的ASCII码;③num是希望设置的字节数。
    3. 函数功能:函数名是设置内存的意思,将ptr所指向的内存中的前num个字节全部用value代替,并返回ptr的值。常用来清理新开辟的动态内存空间。
      memset(p, 0, len*4);		//给整型指针p指向的动态内存内容全部设置成0
      memset(str, '\0', len);		//给字符型指针str指向的动态内存内容全部设置成空字符

  3. free函数
    1. 函数原型:void   free(void   *ptr);
    2. 操作数:①通用指针ptr指向的内存必须是malloc、calloc、realloc开辟的动态内存。绝对不能把静态分配的数组的地址传到free函数中
    3. 函数功能:释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。有两个实际作用:防止悬挂指针,防止内存泄漏。
      free(p);
      free(str);

  4. realloc函数
    1. 函数原型:void*   realloc(void    *ptr,    size_t    newsize)
    2. 操作数:①通用指针ptr指向的内存必须是malloc、calloc开辟的动态内存;②newsize是指用来覆盖原先动态内存空间的新字节长度。
    3. 函数功能:在ptr指向的位置重新分配动态内存的空间大小,newsize可以比原先的小,也可以比原先的大,但是我们一般将realloc函数用于扩容。
    4. 内部决策
      1. 新动态内存长度 < 原动态内存长度:只从原内存中复制长度等于新内存空间的内容,后面的丢失。返回值是原动态内存地址。
      2. 新动态内存长度 > 原动态内存长度,并且,后面的闲置内存长度足够:将原动态内存中的内容全部复制来,返回值是原动态内存地址。
      3. 新动态内存长度 > 原动态内存长度,并且,后面的闲置内存长度不够:使用堆区中的另第一块能够满足这一要求的内存块,将原动态内存的内容全部复制过来,并将原动态内存释放掉,返回值是新动态内存地址。

2、必要语句:

  1. 包含头文件
    1. memset函数:函数声明在头文件<string.h>中。
    2. free函数/malloc函数/realloc函数:函数声明在头文件<stdlib.h>中。
  2. 开辟动态内存时,指针类型强制转换
    int len;
    scanf("%d", &len);
    char *str;
    str = (char *)malloc(len);		//强制转换(char *)
  3. 判断动态内存开辟是否成功:使用malloc函数时,如果开辟失败,将返回NULL。由于单片机开发中没有特别大的空间,所以要留下个心眼判断动态内存是否开辟成功。而在我们使用realloc函数时,如果开辟失败,将返回NULL,但原来的指针仍然有效,所以必要的话也可以额外定义一个指针来判断动态内存重新分配是否成功。
    int len;
    scanf("%d", &len);
    char *str;
    str = (char *)malloc(len);
    if(str == NULL){				//如果开辟失败,就提前退出程序
        printf("malloc error");
        exit(-1);
    }
    memset(str, '\0', len);

  4. free以后给指针赋值NULL:释放掉动态内存以后,由于没有给p赋值,所以指针p还是指向原先动态申请的内存,但是这个内存已经不能再被使用了,这时p就变成野指针。因此给野指针赋值为NULL。
    int len;
    scanf("%d", &len);
    char *str;
    str = (char *)malloc(len);
    if(str == NULL){			
        printf("malloc error");
        exit(-1);
    }
    memset(str, '\0', len);
    free(str);
    str = NULL;						//释放动态内存后,给野指针str赋值NULL

3.实战——为字符串开辟动态内存

1、知识点:

  1. 堆区里,为字符串开辟的动态内存的存储单元是连续的。但如果使用malloc函数进行多次申请,那么第1次和第2次申请的两个内存地址不一定是连续的。
  2. 如果发现想要存放的字符串太长,这时候就要用realloc函数进行动态内存的重新分配,来保障字符串存储在连续的区域。

2、基本步骤:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    char *str;
    str = (char *)malloc(12);                   //1.malloc使用
    if(str == NULL){                            //1.1 必要语句
        printf("malloc error\n");
        exit(-1);
    }
    memset(str, '\0', 12);                      //2.memset使用
    strcpy(str, "chenlichen");                  
    printf("%s\n", str);
    printf("扩容前地址:%x\n", str);
    char s[] = "chenlichen2354623423589135";
    int newLen = strlen(s) + 1;   
    realloc(str, newLen);                       //3.realloc使用
    strcpy(str, s);
    printf("%s\n", str);
    printf("扩容后地址:%x\n", str);
    free(str);                                  //4.free使用
    str = NULL;                                 //4.1 必要语句
    puts("end");
    return 0;
}

4.长度自定义时初始化字符串的两种方法

1、知识点:

  1. 堆区的字符串可以修改字符串里的字符。
  2. 堆区的字符串不能通过指针偏移来给字符串遍历赋值。

2、两种方式:

  1. 通过scanf函数初始化:
    #include <stdio.h>
    #include <stdlib.h>
    int main(int argc, char const *argv[])
    {
        int len;
        char *str;
        printf("请输入字符串长度\n");
        scanf("%d", &len);
        str = (char *)malloc(len);
        puts("请输入字符串");
        scanf("%s", str);
        printf("%s", str);
        return 0;
    }

  2. 通过strcpy函数初始化:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    int main(int argc, char const *argv[])
    {
        int len;
        char *str;
        printf("请输入字符串长度\n");
        scanf("%d", &len);
        str = (char *)malloc(len);
        strcpy(str, "chenlichen");
        printf("%s", str);
        return 0;
    }