C语言学习笔记—内存管理

前言

  •  在嵌入式系统中,内存资源是非常有限的。我们所设计的程序是在内存中运行的,而硬件资源的限制使得在程序设计中首要考虑的问题是如何有效地管理内存资源。

作用域

  • 何为作用域:

  1. 作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是代码块作用域、函数作用域、函数原型作用域或文件作用域。
  2. 代码块作用域:位于一堆花括号之间的所有语句是代码块,在代码块的开始位置声明的标识符的作用域就是代码块作用域,从声明开始,到右大画括号结束。
  3. 文件作用域:任何在代码块之外声明的标识符的作用域是文件作用域。从声明开始,到文件结尾结束。也称为全局变量。
  4. 函数原型作用域:在函数原型中声明的标识符的作用域是原型作用域。从声明开始,到有小括号结束。(只是函数原型声明时,定义时的形参属于代码块作用域)。
  5. 函数作用域:它只适用于语句标签,语句标签用于go语句。一个函数中的所有语句标签必须唯一。
  6. 同一作用域不能有同名变量,但不同作用域变量名称可以相同。
    #include <stdio.h>
    
    int a = 0;               // 变量a具有文件作用域,此处为文件作用域开始
    
    void fun_demo(int *b);    // 变量b具有函数原型作用域,从变量定义处一直到变量声明的结尾,也就是局限于小括号内 
    
    int main()
    {
        // 代码块作用域开始
        int q = 0;           // q函数作用域开始
        ...
        return 0;
        //代码块作用域结束,q函数作用域结束
    }
    
    void fun_demo(int *p)
    {
        // 形参变量p函数作用域开始,也是代码块作用域开始
        *p = 2; 
        // 形参变量p函数作用域结束,也是代码块作用域结束
    }
    //全局变量a的文件作用域结束
    
  • auto自动变量:

  1. 一般情况下代码块内部({ }里面的变量)定义的变量都是自动变量。当然也可以显示的使用auto关键字,所有自动变量的声明周期就是变量所属的大括号。
  • register寄存器变量:

  1. 通常变量在内存中,如果把变量放到CPU的寄存器里面,代码执行效率会更高。
    register int a;
  • 代码块作用域的静态变量:

  1. 静态变量是指内存位置在程序执行期间一直不改变的变量,一个代码块内部的静态变量只能被这个代码块内部访问。
    static int a = 0;
  2. 静态变量在程序刚加载到内存的时候就出现,和定义静态变量的大括号无关,一直到程序结束的时候才从内存消失,同时静态变量的值只初始化一次。
    /**** 输出结果:a = 1, b = 1
                    a = 1, b = 2
                    a = 1, b = 3
                    a = 1, b = 4
                    a = 1, b = 5
                    a = 1, b = 6
                    a = 1, b = 7
                    a = 1, b = 8
                    a = 1, b = 9
                    a = 1, b = 10 ****/
    
    #include <stdio.h>
    
    int main()
    {
        int i;
        for(i = 0; i < 10; i++)
        {
            int a = 0;
            static int b = 0;
            a++;
            b++;
            printf("a = %d, b = %d\n",a, b);
        }
        return 0;
    } 
  • 代码块作用域外的静态变量:

  1. 代码块之外的静态变量在程序执行期间一直存在,但只能被定义这个变量的文件访问,代码块之外的静态变量只能在定义这个变量的文件使用,在其他文件中不能访问。
  2. static的全局变量在不同文件中的名字可以相同,因为static定义的变量只能在本文件中使用。
  • 全局变量:

  1. 全局变量的存储方式和静态变量相同,但可以被多个文件访问。
  2. 全局变量即使不在同一个文件里面,也不能重名。
  • 外部变量与extern关键字:

  1. 如下:声明一个变量a,这个变量在别的文件中已经定义了,这里只是使用,而不是定义。
    extern int a;
  • 全局函数和静态函数:

  1. 在C语言中,函数默认都是全局的,使用关键字static可以将函数声明为静态,这时函数只能在ding定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数也没用。

内存布局(内存四区)

  • 代码区:

  1.  程序被操作系统加载到内存的时候,所有的可执行代码(程序代码指令、常量字符串等)都加载到代码区,这块内存在程序运行期间是不变的。代码区所有的内容在程序加载到内存的时候就确定了,运行期间不可以修改,只可以执行。
  2. 注意:"int a = 0;"语句可拆分成"int a;"和"a = 0",定义变量a的"int a;"语句并不是代码,它在程序编译时就执行了,并没有放到代码区,放到代码区的只有"a = 0"这句。
  • 静态区:

  1. 静态区存放程序中所有的全局变量和静态变量。
  2. 静态区是程序加载到内存的时候就确定了,程序退出时从内存消失。所有全局变量和静态变量在程序运行期间都占用内存。
  • 栈区:

  1. 栈(stack)是一种先进后出的内存结构,所有的自动变量、函数形参,函数的返回值都存储在栈中,这个动作由编译器自动完成,写程序时不需要考虑。栈区在程序运行期间是可以随时修改的。
  2. 当一个自动变量超出其作用域时,自动从栈中弹出(也就是变量离开作用域后栈上的内存会自动释放
  3. 栈的最大尺寸固定,超出则引起栈溢出。
  4. C语言的形参是从右到左入栈的,如:
    int test_demo(int a, int b)  // b先入栈,a后入栈
    {
        ...
    }
  5. 不同系统栈的大小不一样,即使相同的系统,栈的大小也是不一样,window程序在编译时可以指定栈的大小,linux栈的大小可以通过环境变量设置。
  6. 例子:全局变量a和静态变量b,c存放在静态区,地址相连;自动变量d和e都在栈区存放,地址只相差4个字节,需要注意:a的地址比b要大,这是因为栈是一种先进后出的数据存储结构,先存放的a,后存放的b,一旦超出作用域,那么变量b将先于变量a被销毁。
    /**** 输出结果:&a = 00405008, &b = 00402000, &c = 00402004, &d = 0060FF0C, &e = 0060FF08 ****/
    #include <stdio.h>
    
    int a = 0;
    static int b = 1;
    
    int main()
    {
        static int c = 2;
        int d = 3;
        int e = 4;
        printf("&a = %p, &b = %p, &c = %p, &d = %p, &e = %p\n",&a, &b, &c, &d, &e);
        return 0;
    }
    
  • 堆区:

  1. 堆(heap)和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。
  2. 堆是一个大容器,可以解决栈造成的内存溢出问题,但在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。
  3. 注意:一个程序的栈大小是有限的,如果一个数组特别大,会导致栈溢出。如果使用一个特别大的数组,需要把数组放入堆中,而不是栈。如果一个数组定义的时候大小不能确定,那么适合用堆。如下程序:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        int i;
        scanf("%d", &i);
        int *p = malloc(i * sizeof(int));  // 可以根据用户的输入在堆中分配大小不同的数组
        int a;
        for(a = 0; a < i; a++)
           printf("%d\n", p[a]);
        free(p);
        return 0;
    }

堆的分配和释放

  • malloc:

  1. 分配所需的内存空间,需要标准库:stdlib.h
  2. malloc() 函数的声明如下:其中,size :内存块的大小,以字节为单位。函数返回void *指针。
    void *malloc(size_t size);
  3. malloc分配空间的值是随机的,不会自动清零。
  4. 例子:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
        /* 这个指针变量s任然在栈里面,只不过它指向了一个堆地址空间 */
        char *s = malloc(10);     // 在堆中分配了10个字节空间
        strcpy(s, "abcd");
        printf("%s\n", s);
        return 0;
    }
  • free:

  1. 释放之前分配的内存空间,需要标准库:stdlib.h
  2.  free() 函数的声明如下:ptr :指针指向一个要释放内存的内存块。该函数不返回任何值。
    void free(void *ptr);
  3. 用malloc在堆中分配的空间不会自动释放,需要用free来手动释放。如果没有free,则会造成内存泄漏。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
        /* 这个指针变量s任然在栈里面,只不过它指向了一个堆地址空间 */
        char *s = malloc(10);     // 在堆中分配了10个字节空间
        strcpy(s, "abcd");
        printf("%s\n", s);
        free(s);                  // 释放堆中的内存
        s = malloc(20);           // 因为s是自动变量,可以重新使用,这时又重新指向一个新的堆空间
        free(s);                  // free(s)并不是把变量s释放了,s任然在栈中,而是释放s指向的那块内存空间
        return 0;
    }
  4. 例子:函数返回一个指针时,用mallco可以保留内存空间。
    #include <stdio.h>
    #include <stdlib.h>
    
    int *test_demo()
    {
        int a = 10;        // 自动变量,出了这个作用域就自动释放掉
        return &a;
    }
    
    int *test_demo1()
    {   
        int *p1 = malloc(1*sizeof(int));   // 自动指针变量p1指向一个堆空间,不会自动释放
        *p1 = 10;
        return p1;
    }
    
    int main()
    {
        /* 编译时这行出现warning,因为test_demo内部的变量a已经不在内存了,所以p指向一个无效的空间 */
        int *p  = test_demo();
       
        int *p1 = test_demo1();
        printf("*p1 = %d\n", *p1);
        free(p1); 
        return 0;
    }
  5. 例子:合并2个可变长度的字符串。
    /**** 输出结果:abcdefhijklmn ****/
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
        char a[] = "abcdef";
        char b[] = "hijklmn";
        char *p = malloc(strlen(a) + strlen(b) + 1);
        strcpy(p, a);
        strcat(p, b);
        printf("%s\n", p);
        free(p);
        return 0;
    }
  • calloc:

  1. 分配所需的内存空间,需要标准库:stdlib.h
  2.  calloc() 函数的声明如下:nmemb:要被分配的元素个数;size:一个元素的大小。
    void *calloc(size_t nmemb, size_t size);
  3. malloc 和 calloc 之间的不同点是,malloc 不会设置内存为零,需要memset清空,而 calloc 会自动设置分配的内存为零。但都需要free释放。
    用malloc分配10个int:
    int *p = malloc(10 * sizeof(int));
    用calloc分配10个int:
    int *p = calloc(10, sizeof(int));
  • realloc:

  1. 重新调整之前调用 malloc 或 calloc 所分配的内存空间,需要标准库:stdlib.h
  2.  realloc() 函数的声明如下:ptr指针指向一个要重新分配内存的内存块; 内存块的新的大小,以字节为单位。
    void *realloc(void *ptr, size_t size);
  3. 例子:
    /**** 输出结果:123456789abcdefg ****/
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
        char *s1 = calloc(10, sizeof(char));
        char *s2 = calloc(10, sizeof(char));
        strcpy(s1, "123456789");
        strcpy(s2, "abcdefg");
        s1 = realloc(s1, strlen(s1) + strlen(s2) + 1);
        strcat(s1, s2);
        printf("%s\n", s1);
        free(s1);
        free(s2);
        return 0;
    }
  4. 当想通过函数内部给指针形参分配堆内存的时候,形参一定是一个二级指针。如下2个程序:
    /**** 程序编译会出错 ****/
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    void test_demo(char *s)
    {
        s = calloc(10,sizeof(char));
        strcpy(s, "hello");
    }
    
    int main()
    {
        char *p = NULL;
        test_demo(p);
        printf("%s\n", p);      // 调用完test_demo后,p的值任然为空,空的值不能输出
        free(p);                // 无法指向有效内存,出现内存泄漏,s在执行完test_demo后不存在了
        return 0;
    }
    
    /**** 输出结果:hello ****/
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    void test_demo(char **s)
    {
        *s = calloc(10,sizeof(char));
        strcpy(*s, "hello");
    }
    
    int main()
    {
        char *p = NULL;
        test_demo(&p);
        printf("%s\n", p);
        free(p);
        return 0;
    }

猜你喜欢

转载自blog.csdn.net/qq_34935373/article/details/88773574