经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C 语言 » 查看文章
C语言内存管理
来源:cnblogs  作者:与鹿逐秋  时间:2021/4/6 10:15:31  对本文有异议

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

(一) 栈区与堆区

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

1. 栈内存

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

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

  1. #include<stdio.h>
  2. void showA() //定义函数 showA
  3. {
  4. int a;
  5. printf("&a=%p\n",&a); //输出变量 a 的地址
  6. }
  7. void showB() //定义函数 showB
  8. {
  9. int b;
  10. printf("&b=%p\n",&b); //输出变量 b 的地址
  11. }
  12. int main(void)
  13. {
  14. showA(); //调用 showA 函数
  15. showB(); //调用 showB 函数
  16. getchar();
  17. return 0;
  18. }

运行结果如图所示:

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

2. 栈内存注意事项

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

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

  1. #include<stdio.h>
  2. int* showA()
  3. {
  4. int a=1;
  5. return &a; //返回变量 a 的地址
  6. }
  7. void showB()
  8. {
  9. int b=200;
  10. }
  11. int main(void)
  12. {
  13. int* p_a=showA();
  14. printf("%d ",*p_a); //输出 p_a 指向的变量的值
  15. showB(); //调用 showB 函数
  16. printf("%d ",*p_a); //输出 p_a 指向的变量的值
  17. getchar();
  18. return 0;
  19. }
  20. // 运行结果
  21. // 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 函数进行手动释放,否则这块堆内存将无法再次被使用。

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

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

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

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main(void)
  4. {
  5. int *p_int=(int*)malloc(sizeof(int));
  6. *p_int=200;
  7. printf("%p %d",p_int,*p_int);
  8. free(p_int);
  9. getchar();
  10. return 0;
  11. }

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

4. 堆内存注意事项

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main(void)
  4. {
  5. int*p_int = (int*)malloc(sizeof(int));
  6. *p_int = 10;
  7. printf("%p %d\n",p_int,*p_int);
  8. free(p_int);
  9. printf("%p %d\n",p_int,*p_int);
  10. getchar();
  11. return 0;
  12. }

运行结果如图所示:

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

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

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

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

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

  1. #include<stdio.h>
  2. int main(void)
  3. {
  4. int a[9900000];
  5. getchar();
  6. return 0;
  7. }

运行结果如图所示:

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

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

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main(void)
  4. {
  5. int *p_int=(int*)malloc(100000000);
  6. *p_int=100;
  7. printf("%d\n",*p_int);
  8. free(p_int);
  9. getchar();
  10. return 0;
  11. }

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

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

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int* getMemory()
  4. {
  5. int*p_int=(int*)malloc(sizeof(int));//被调函数分配内存
  6. *p_int=100;
  7. return p_int;
  8. }
  9. int main(void)
  10. {
  11. int* p=getMemory();
  12. printf("%d\n",*p);
  13. free(p); //主调函数释放内存
  14. getchar();
  15. return 0;
  16. }

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

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

  1. #include<stdio.h>
  2. int* getMemory()
  3. {
  4. static int a=100;
  5. return &a;
  6. }
  7. int main(void)
  8. {
  9. int* p=getMemory();
  10. printf("%d ",*p);
  11. getchar();
  12. return 0;
  13. }

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

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. void fun(int *p_int)
  4. {
  5. *p_int=100;
  6. }
  7. int main(void)
  8. {
  9. int* p=(int*)malloc(sizeof(int)); //主调函数分配堆内存
  10. fun(p);
  11. printf("%d",*p);
  12. free(p); //主调函数释放堆内存
  13. getchar();
  14. return 0;
  15. }

这是推荐的做法!

7. 初始化内存

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main(void)
  4. {
  5. int* p_int=(int*)malloc(sizeof(int));
  6. printf("%d",*p_int);
  7. getchar();
  8. return 0;
  9. }

运行结果如图所示:

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. int main(void)
  4. {
  5. int* p_int=(int*)malloc(sizeof(int));
  6. *p_int=0;
  7. printf("%d",*p_int);
  8. getchar();
  9. return 0;
  10. }

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

8. memset

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

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

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<string.h>
  4. int main(void)
  5. {
  6. int* p_int=(int*)malloc(sizeof(int)*10);//注意不要写成 malloc(10)
  7. int i=0;
  8. memset(p_int,0,sizeof(int)*10); //初始化堆内存
  9. for (i=0;i<10;i++)
  10. {
  11. printf("%d ",p_int[i]);
  12. }
  13. free(p_int);
  14. getchar();
  15. return 0;
  16. }

(二) 结构体

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

1. 结构体概述

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

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

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

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

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

  1. struct 结构体名
  2. {
  3. 成员列表
  4. };

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

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

  1. struct Employee
  2. {
  3. char name[8];
  4. int age;
  5. int id;
  6. int salary;
  7. };

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

2. 定义结构体变量

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

  1. struct 结构体名
  2. {
  3. 成员列表
  4. };
  5. struct 结构体名 变量名;
  6. 例如:
  7. struct Employee
  8. {
  9. char name[8];
  10. int age;
  11. int id;
  12. int salary;
  13. };
  14. struct Employee emp;

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

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

  1. struct 结构体名
  2. {
  3. 成员列表
  4. }变量名;
  5. 例如:
  6. struct Employee
  7. {
  8. char name[8];
  9. int age;
  10. int id;
  11. int salary;
  12. }emp,emp1,emp2;

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

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

  1. struct
  2. {
  3. 成员列表
  4. }变量名;
  5. 例如:
  6. struct
  7. {
  8. char name[8];
  9. int age;
  10. int id;
  11. int salary;
  12. }emp;

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

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

(1)结构体变量初始化

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

  1. struct Employee emp={“hello”,20,1,10000}
  2. struct Employee
  3. {
  4. char name[8];
  5. int age;
  6. int id;
  7. int salary;
  8. }emp={“hello”,20,1,10000};

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

(2)引用结构体变量

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

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

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

  1. #include<stdio.h>
  2. struct Employee
  3. {
  4. char name[8];
  5. int age;
  6. int id;
  7. int salary;
  8. };
  9. int main(void)
  10. {
  11. struct Employee emp={"hello",20,1,10000};
  12. printf("%s\n",emp.name);
  13. printf("%d\n",emp.age);
  14. printf("%d\n",emp.id);
  15. printf("%d\n",emp.salary);
  16. getchar();
  17. return 0;
  18. }

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

  1. #include<stdio.h>
  2. #include<string.h>
  3. struct Employee
  4. {
  5. char name[8];
  6. int age;
  7. int id;
  8. int salary;
  9. };
  10. int main(void)
  11. {
  12. struct Employee emp;
  13. strcpy(emp.name,"hello");
  14. emp.age=20;
  15. emp.id=1;
  16. emp.salary=10000;
  17. printf("%s\n",emp.name);
  18. printf("%d\n",emp.age);
  19. printf("%d\n",emp.id);
  20. printf("%d\n",emp.salary);
  21. getchar();
  22. return 0;
  23. }

【说明】

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

4. 结构体指针

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

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

例如:

  1. struct Employee emp;
  2. 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:

  1. #include<stdio.h>
  2. struct Employee
  3. {
  4. char name[8];
  5. int age;
  6. int id;
  7. int salary;
  8. };
  9. int main(void)
  10. {
  11. struct Employee emp={"hello",20,1,10000};
  12. struct Employee *p_emp=&emp;
  13. printf("%s\n", (*p_emp).name);
  14. printf("%d\n", (*p_emp).age);
  15. printf("%d\n", (*p_emp).id);
  16. printf("%d\n", (*p_emp).salary);
  17. getchar();
  18. return 0;
  19. }

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

  1. #include<stdio.h>
  2. struct Employee
  3. {
  4. char name[8];
  5. int age;
  6. int id;
  7. int salary;
  8. };
  9. int main(void)
  10. {
  11. struct Employee emp={"hello",20,1,10000};
  12. struct Employee *p_emp=&emp;
  13. printf("%s\n", p_emp->name);
  14. printf("%d\n", p_emp->age);
  15. printf("%d\n", p_emp->id);
  16. printf("%d\n", p_emp->salary);
  17. getchar();
  18. return 0;
  19. }

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

5. typedef 类型别名

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

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

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

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

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

  1. #include<stdio.h>
  2. typedef int integer;
  3. int main(void)
  4. {
  5. integer a=10;
  6. printf("%d",a);
  7. getchar();
  8. return 0;
  9. }

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

  1. struct Employee
  2. {
  3. char name[8];
  4. int age;
  5. int id;
  6. int salary;
  7. };
  8. typedef struct Employee t_Employee;

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

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

  1. #include<stdio.h>
  2. struct Employee
  3. {
  4. char name[8];
  5. int age;
  6. int id;
  7. int salary;
  8. };
  9. typedef struct Employee t_Employee; //定义别名
  10. int main(void)
  11. {
  12. t_Employee emp={"hello",20,1,10000};
  13. printf("%s\n",emp.name);
  14. printf("%d\n",emp.age);
  15. printf("%d\n",emp.id);
  16. printf("%d\n",emp.salary);
  17. getchar();
  18. return 0;
  19. }

6. 结构体复制

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

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

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

  1. #include<stdio.h>
  2. struct Employee
  3. {
  4. char name[8];
  5. int age;
  6. int id;
  7. int salary;
  8. };
  9. typedef struct Employee t_Employee;
  10. int main(void)
  11. {
  12. t_Employee emp={"hello",20,1,10000};
  13. t_Employee emp2;
  14. emp2=emp;
  15. printf("%s\n",emp2.name);
  16. printf("%d\n",emp2.age);
  17. printf("%d\n",emp2.id);
  18. printf("%d\n",emp2.salary);
  19. getchar();
  20. return 0;
  21. }

原文链接:http://www.cnblogs.com/wangyueping/p/14540812.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号