zoukankan      html  css  js  c++  java
  • C语言内存管理

    在 C 语言中,当一个程序被加载到内存中运行,系统会为该程序分配一块独立的内存空间,并且这块内存空间又可以再被细分为很多区域,比如:栈区、堆区、静态区、全局区......等。这里只介绍常用的内存区域:栈区、堆区。

    (一) 栈区与堆区

    栈区:保存局部变量。存储在栈区的变量,在函数执行结束后,会被系统自动释放。
    堆区:由 malloc、calloc、realloc……等函数分配内存。其生命周期由 free 函数控制,在没有被释放之前一直存在,直到程序运行结束。

    1. 栈内存

    定义在函数内部的局部变量,都保存在栈区。栈区的特点是:函数执行结束后,由系统“自动回收”局部变量所对应的内存空间。所谓的“自动回收”其实是操作系统将这块栈内存又分配给其他函数中的局部变量使用。

    打个比方:将栈区比作餐厅,局部变量比作客人,局部变量对应的栈内存比作餐具。客人吃饭时使用的餐具,在客人离开后由餐厅负责回收、清洗,然后再给其他客人使用。同理,局部变量与栈内存的关系也是如此,当定义局部变量时,系统会在栈区为其分配一块内存空间,当函数执行结束后系统负责回收这块内存,又分配给其他局部变量使用。

    #include<stdio.h>
    void showA() //定义函数 showA
    {
        int a;
        printf("&a=%p
    ",&a); //输出变量 a 的地址
    }
    void showB() //定义函数 showB
    {
        int b;
        printf("&b=%p
    ",&b); //输出变量 b 的地址
    }
    int main(void)
    {
        showA(); //调用 showA 函数
        showB(); //调用 showB 函数
        getchar();
        return 0;
    }
    

    运行结果如图所示:

    可以验证局部变量对应的内存在函数执行结束后,会被系统回收分配给其他函数中的局部变量使用。

    2. 栈内存注意事项

    由于局部变量在函数执行结束后,会被系统“自动回收”并分配给其他函数中的局部变量使用。因此,在 C 程序中,不能将局部变量地址作为函数返回值,否则会出现一些意想不到的效果。

    下面通过例子来深入了解一下。

    #include<stdio.h>
    int* showA()
    {
        int a=1;
        return &a; //返回变量 a 的地址
    }
    
    void showB()
    {
        int b=200;
    }
    int main(void)
    {
        int* p_a=showA();
        printf("%d ",*p_a); //输出 p_a 指向的变量的值
        showB(); //调用 showB 函数
        printf("%d ",*p_a); //输出 p_a 指向的变量的值
        getchar();
        return 0;
    }
    
    // 运行结果
    // 1 200
    

    之所以会出现这种情况,是由于 showA 函数执行结束后,局部变量 a 对应的栈内存被系统回收后分配给 showB 函数中的局部变量 b 使用。因此,变量 a 和变量 b 对应同一块栈内存,而指针变量 p_a 始终指向这块栈内存。可以认为开始时 p_a 指向变量 a,调用 showB函数后,p_a 指向变量 b,所以第 16 行*p_a 的值为 200。

    3. 堆内存

    使用 malloc 系列函数分配的内存都属于堆区,使用完后调用 free 函数进行释放,否则可能会造成内存泄漏。

    打个比方:堆区相当于自己家,malloc 分配的堆内存相当于盘子。在家里吃饭时使用的盘子,吃完后必须手动进行清洗,否则盘子将不能再使用。同理,堆内存也是如此,使用完毕后,需要调用 free 函数进行手动释放,否则这块堆内存将无法再次被使用。

    malloc 函数
    函数原型:
    void *malloc(int size);
    头文件:
    #include 
    参数列表:
    size:分配多少个字节。
    功能:
    申请指定大小的堆内存。
    返回值:
    如果分配成功则返回指向被分配内存的指针,否则返回空指针 NULL。
    

    【说明】
    void*表示“不确定指向类型”的指针,使用前必须进行强制类型转化,将 void*转化为“确定指向类型”的指针。
    一定要注意 malloc 的参数是“字节”,因为 malloc 不知道你申请的内存要放什么类型的数据,所以统一的“汇率”就是“字节”。

    free 函数
    函数原型:
    void free(void* ptr);
    头文件:
    #include <stdlib.h>
    参数列表:
    ptr:指向要被释放的堆内存。
    功能:
    释放 ptr 指向的内存空间。
    

    下面通过例子来了解如何在堆区分配内存。

    #include<stdio.h>
    #include<stdlib.h>
    int main(void)
    {
        int *p_int=(int*)malloc(sizeof(int));
        *p_int=200;
        printf("%p %d",p_int,*p_int);
        free(p_int);
        getchar();
        return 0;
    }
    

    如果内存申请但是忘了释放(free),那么就会导致“内存泄露”(memory leak)
    运行结果如图所示:

    4. 堆内存注意事项

    在 C 程序中,被 free 之后的堆内存,将会被操作系统回收分配给,不建议继续使用,否则输出的结果将难以预料。
    下面通过例子来了解使用被 free 的堆内存。

    #include<stdio.h>
    #include<stdlib.h>
    int main(void)
    {
        int*p_int = (int*)malloc(sizeof(int));
        *p_int = 10;
        printf("%p %d
    ",p_int,*p_int);
        free(p_int);
        printf("%p %d
    ",p_int,*p_int);
        getchar();
        return 0;
    }
    

    运行结果如图所示:

    可以看到 p_int 指向的堆内存地址没有改变,但是该内存空间中的数据被修改了。这是因为被 free 的堆内存会被系统回收,分配给其他地方使用,修改了这块堆内存中的数据。

    不再使用的内存一定要及时的 free,否则会造成内存泄漏;还有用的内存也不能提前free。

    5. 栈内存与堆内存分配限制

    栈内存:
    (1) 由系统自动分配、释放。如:函数形参、局部变量。
    (2) 栈内存比较小,在 VS2012 中,栈内存默认最大为 1M,如果局部变量占用的栈内存过大,会发生栈溢出。

    下面通过例子来了解一下栈溢出。

    #include<stdio.h>
    int main(void)
    {
        int a[9900000];
        getchar();
        return 0;
    }
    

    运行结果如图所示:

    定义 int 类型数组 a 长度为 100 0000 已经超过了 1M,发生栈溢出错误。其中“Stackoverflow”中文意思就是栈溢出。

    堆内存:
    (1)由程序员自己申请、释放。如果没有释放,可能会发生内存泄露,直到程序结束后由系统释放。
    (2)堆内存比较大,可以分配超过 1G 的内存空间。

    下面使用 malloc 分配大内存空间。

    #include<stdio.h>
    #include<stdlib.h>
    int main(void)
    {
        int *p_int=(int*)malloc(100000000);
        *p_int=100;
        printf("%d
    ",*p_int);
        free(p_int);
        getchar();
        return 0;
    }
    

    运行后发现 malloc(100000000)分配成功,没有报错。

    6. 函数内部返回数据的三种方式

    方式一
    在被调函数中使用 malloc 分配内存,在主调函数中 free 释放内存。
    例如:

    #include<stdio.h>
    #include<stdlib.h>
    int* getMemory()
    {
        int*p_int=(int*)malloc(sizeof(int));//被调函数分配内存
        *p_int=100;
        return p_int;
    }
    int main(void)
    {
        int* p=getMemory();
        printf("%d
    ",*p);
        free(p); //主调函数释放内存
        getchar();
        return 0;
    }
    

    方式 1 分配内存与释放内存是分开的,容易导致程序员忘记在主调函数中释放内存,从而导致内存泄漏,因此方式 1 不推荐使用。

    方式二
    使用 static 修饰的局部变量,例如:

    #include<stdio.h>
    int* getMemory()
    {
        static int a=100;
        return &a;
    }
    int main(void)
    {
        int* p=getMemory();
        printf("%d ",*p);
        getchar();
        return 0;
    }
    

    方式 2 不适用于多线程等复杂的环境,因此也不推荐使用。

    方式三
    在主调函数中分配堆内存,在被调函数中使用堆内存,最后又在主调函数中释放堆内存。
    例如:

    #include<stdio.h>
    #include<stdlib.h>
    void fun(int *p_int)
    {
        *p_int=100;
    }
    int main(void)
    {
        int* p=(int*)malloc(sizeof(int)); //主调函数分配堆内存
        fun(p);
        printf("%d",*p);
        free(p); //主调函数释放堆内存
        getchar();
        return 0;
    }
    

    这是推荐的做法!

    7. 初始化内存

    使用 malloc 函数分配的堆内存,系统不会初始化内存,内存中残留的还是旧数据。因此,引用未初始化的堆内存,输出的数据也将是未知的。例如:

    #include<stdio.h>
    #include<stdlib.h>
    int main(void)
    {
        int* p_int=(int*)malloc(sizeof(int));
        printf("%d",*p_int);
        getchar();
        return 0;
    }
    

    运行结果如图所示:

    输出 p_int 指向堆内存中数据,由于这块内存未初始化,因此输出结果将是难以预料的。为了避免引用堆内存中的未知数据,一般使用 malloc 在堆区分配内存后,需要将这块堆内存初始化为 0,例如:

    #include<stdio.h>
    #include<stdlib.h>
    int main(void)
    {
        int* p_int=(int*)malloc(sizeof(int));
        *p_int=0;
        printf("%d",*p_int);
        getchar();
        return 0;
    }
    

    上述这种初始化堆内存的方式,只适合于单个基本类型大小的堆内存。如果分配多个基本类型大小的堆内存时,这种赋值方式就不合适了。
    例如:
    int* p_int=(int*)malloc(sizeof(int)*10);
    上述程序,分配了 10 个 int 类型字节大小的堆内存,如果仍采用赋值表达式进行初始化,就需要 for 循环初始化 10 次,太麻烦,所以 C 语言中提供了 memset 函数方便对内存进行初始化。

    8. memset

    函数原型:
    void* memset(void* dest,int value,int size);
    头文件:
    #include<string.h>
    参数列表:
    dest:被初始化的目标内存区域。
    value:初始值。
    size:初始化 size 个字节。
    功能:
    将 dest 指向的内存空间前 size 个字节初始化为 value。
    返回值:
    返回 dest 指向的内存地址。
    

    由于是把 value 按照字节来进行填充的,而 value 是 int 类型(4 个字节),一般 value不要填充除了 0 之外的值,除非你很了解内存结构和二进制。

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    int main(void)
    {
        int* p_int=(int*)malloc(sizeof(int)*10);//注意不要写成 malloc(10)
        int i=0;
        memset(p_int,0,sizeof(int)*10); //初始化堆内存
        for (i=0;i<10;i++)
        {
            printf("%d ",p_int[i]);
        }
        free(p_int);
        getchar();
        return 0;
    }
    

    (二) 结构体

    在 C 语言中,char、int、float……等属于系统内置的基本数据类型,往往只能解决简单的问题。当遇到比较复杂的问题时,只使用基本数据类型是难以满足实际开发需求的。因此,C 语言允许用户根据实际项目需求,自定义一些数据类型,并且用它们来定义变量。

    1. 结构体概述

    在前面介绍了 C 语言中的多种数据类型,例如:整型、字符型、浮点型、数组、指针……等等。但是在实际开发中,只有这些数据类型是不够的,难以胜任复杂的程序设计。

    例如:在员工信息管理系统中,员工的信息就是一类复杂的数据。每条记录中都包括员工的姓名、性别、年龄、工号、工资等信息。姓名为字符数组、性别为字符、年龄为整型、工号为整型、工资为整型。

    对于这类数据,显然不能使用数组存储,因为数组各个元素的类型都是相同的。为了解决这个问题,C 语言中提供了一种组合数据类型“结构体”。

    结构体是一种组合数据类型,由用户自己定义。结构体类型中的元素既可以是基本数据类型,也可以结构体类型。

    定义结构体类型的一般格式为:

    struct 结构体名
    {
        成员列表
    };
    

    成员列表由多个成员组成,每个成员都必须作类型声明,成员声明格式为:
    数据类型 成员名;

    下面来看一个具体的例子:

    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    

    这段代码中 struct 是关键字,Employee 结构体名,struct Employee 表示一种结构体类型。
    该结构体中有 4 个成员,分别为 name、age、id、salary,使用这种结构体类型就可以表示员工的基本信息。

    2. 定义结构体变量

    在 C 语言中,定义结构体变量的方式有 3 种:
    第 1 种 先定义结构体类型,再定义结构体变量,一般形式为:

    struct 结构体名
    {
        成员列表
    };
    struct 结构体名 变量名;
    
    例如:
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    struct Employee emp;
    

    这种方式和基本类型的变量定义方式相同,其中 struct Employee 是结构体类型名,emp是结构体变量名。

    第 2 种 在定义结构体类型的同时定义变量,一般形式为:

    struct 结构体名
    {
        成员列表
    }变量名;
    例如:
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    }emp,emp1,emp2;
    

    这种方式将结构体类型定义与变量定义放在一起,可以直接看到结构体的内部结构,比较直观。

    第 3 种 直接定义结构体变量,并且不需指定结构体名,一般形式为:

    struct
    {
        成员列表
    }变量名;
    例如:
    struct
    {
        char name[8];
        int age;
        int id;
        int salary;
    }emp;
    

    这种方式由于没有指定结构体名,显然不能再使用该结构体类型去定义其他变量,在实际开发中很少用到。

    3. 初始化、引用结构体变量

    (1)结构体变量初始化

    在 C 语言中,结构体变量初始化,本质上是对结构体变量中的成员进行初始化,使用花括号{ }在初始化列表中对结构体变量中各个成员进行初始化,例如:

    struct Employee emp={“hello”,20,1,10000}
    或
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    }emp={“hello”,20,1,10000};
    

    编译器会将“hello”、20、1、10000按照顺序依次赋值给结构体变量emp中的成员name、age、id、salary。

    (2)引用结构体变量

    引用结构体变量的本质,是引用结构体变量中的不同类型的成员,引用的一般形式为:结构体变量名.成员名;

    例如:emp.name 表示引用 emp 变量中的 name 成员,emp.id 表示引用 emp 变量中的id 成员。

    其中“.”是成员运算符,它的优先级在所有运算符中是最高的。

    #include<stdio.h>
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    int main(void)
    {
        struct Employee emp={"hello",20,1,10000};
        printf("%s
    ",emp.name);
        printf("%d
    ",emp.age);
        printf("%d
    ",emp.id);
        printf("%d
    ",emp.salary);
        getchar();
        return 0;
    }
    

    除了采用初始化列表,还可以使用赋值运算符,对成员进行初始化,例如:

    #include<stdio.h>
    #include<string.h>
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    int main(void)
    {
        struct Employee emp;
        strcpy(emp.name,"hello");
        emp.age=20;
        emp.id=1;
        emp.salary=10000;
        printf("%s
    ",emp.name);
        printf("%d
    ",emp.age);
        printf("%d
    ",emp.id);
        printf("%d
    ",emp.salary);
        getchar();
        return 0;
    }
    

    【说明】

    使用成员列表的方式初始化时,编译器会自动将字符串“hello”复制到字符数组 name中。而使用成员赋值方式初始化时,需要调用 strcpy 函数,将字符串“hello”复制到字符数组 name 中。

    4. 结构体指针

    指向结构体变量的指针就是结构体指针,如果指针变量中保存一个结构体变量的地址,则这个指针变量指向该结构体变量,需要注意的是指针变量的类型必须和结构体变量的类型相同。

    定义结构体指针变量的一般形式为:
    struct 结构体名 *指针变量名

    例如:

    struct Employee emp;
    struct Employee * p_emp=&emp;
    

    其中 emp 为结构体变量,p_emp 为结构体指针,将 emp 取地址赋给指针变量 p_emp表示 p_emp 指向 emp。
    在 C 语言中,通过结构体指针 p 也可以引用结构体中的成员,有以下两种方式:
    (1)(*p).成员名;
    (2)p->成员名;

    例如:struct Employee * p_emp=&emp;
    (*p_emp)表示指向的结构体变量 emp,(*p_emp).age 表示指向的结构体变量 emp 中的成员 age。注意,“.”运算符优先级是最高的,(*p_emp)两侧的括号不能省略。

    为了简化操作,C 语言允许将(*p).成员名用 p->成员名替换,(*p_emp).age 等价于p_emp->age,“->”称为指向运算符。

    方式 1:

    #include<stdio.h>
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    int main(void)
    {
        struct Employee emp={"hello",20,1,10000};
        struct Employee *p_emp=&emp;
        printf("%s
    ", (*p_emp).name);
        printf("%d
    ", (*p_emp).age);
        printf("%d
    ", (*p_emp).id);
        printf("%d
    ", (*p_emp).salary);
        getchar();
        return 0;
    }
    

    以“->”方式访问结构体成员(常用)

    #include<stdio.h>
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    int main(void)
    {
        struct Employee emp={"hello",20,1,10000};
        struct Employee *p_emp=&emp;
        printf("%s
    ", p_emp->name);
        printf("%d
    ", p_emp->age);
        printf("%d
    ", p_emp->id);
        printf("%d
    ", p_emp->salary);
        getchar();
        return 0;
    }
    

    到底用“.”还是“->”初学者容易迷糊,记住一点:结构体变量用“.”,结构体指针变量用“->”

    5. typedef 类型别名

    在 C 语言中,除了使用 C 语言提供的标准类型名:char、int、double……以及自定义的结构体类型。还可以使用 typedef 关键字指定一个新的类型名来代替已有的类型名,相当于给已有类型起别名。类似于现实生活中,给一个人起外号一样,其实都是一个人。

    typedef 的一般使用形式为:
    typedef 原类型名 新类型名

    例如:
    typedef int integer
    其中 integer 是 int 类型的别名,在程序中可以使用 integer 代替 int 来定义整型变量。

    例如:
    integer a,b;
    等价于
    int a,b;

    下面通过例子来了解 typedef 的应用。

    #include<stdio.h>
    typedef int integer;
    int main(void)
    {
        integer a=10;
        printf("%d",a);
        getchar();
        return 0;
    }
    

    typedef 不仅可以为基本类型起别名,还可以为自定义数据类型起别名,例如:

    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    typedef struct Employee t_Employee;
    

    其中 struct Employee 为自定义结构体类型名,t_Employee 为 struct Employee 的别名。在程序中可以使用 t_Employee 替换 struct Employee。

    下面通过例子来了解 typedef 在结构体中的应用。

    #include<stdio.h>
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    typedef struct Employee t_Employee; //定义别名
    int main(void)
    {
        t_Employee emp={"hello",20,1,10000};
        printf("%s
    ",emp.name);
        printf("%d
    ",emp.age);
        printf("%d
    ",emp.id);
        printf("%d
    ",emp.salary);
        getchar();
        return 0;
    }
    

    6. 结构体复制

    在 C 语言中,允许相同类型的结构体变量之间相互赋值。
    例如:
    t_Employee emp={"hello",20,1,10000};
    t_Employee emp2=emp;

    执行 emp2=emp,会将结构体变量 emp 各个成员的值原样复制一份到变量 emp2 各个成员中。和基本类型变量的赋值规则相同,emp2 是 emp 的一个拷贝。这种赋值方式,被称为“结构体复制”。

    下面通过例子来了解结构体复制。

    #include<stdio.h>
    struct Employee
    {
        char name[8];
        int age;
        int id;
        int salary;
    };
    typedef struct Employee t_Employee;
    int main(void)
    {
        t_Employee emp={"hello",20,1,10000};
        t_Employee emp2;
        emp2=emp;
        printf("%s
    ",emp2.name);
        printf("%d
    ",emp2.age);
        printf("%d
    ",emp2.id);
        printf("%d
    ",emp2.salary);
        getchar();
        return 0;
    }
    
  • 相关阅读:
    sscanf 与 ssprintf 用法 (转载--https://www.cnblogs.com/Anker/p/3351168.html)
    PYTHON网络爬虫与信息提取[信息的组织与提取](单元五)
    PYTHON网络爬虫与信息提取[BeautifulSoup](单元四)
    PYTHON网络爬虫与信息提取[网络爬虫协议](单元二)
    Python网络爬虫与信息提取[request库的应用](单元一)
    scikit-learn实现ebay数据分析 的随笔
    machine leanring 笔记 vectorization
    machine learning 笔记 normal equation
    machine leanring 笔记 octave命令笔记
    UVa 1354 天平难题 Mobile Computing
  • 原文地址:https://www.cnblogs.com/wangyueping/p/14540812.html
Copyright © 2011-2022 走看看