C语言内存管理

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

(一) 栈区与堆区

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

1. 栈内存

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

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

#include<stdio.h>
void showA() //定义函数 showA
{
    int a;
    printf("&a=%p\n",&a); //输出变量 a 的地址
}
void showB() //定义函数 showB
{
    int b;
    printf("&b=%p\n",&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\n",p_int,*p_int);
    free(p_int);
    printf("%p %d\n",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\n",*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\n",*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\n",emp.name);
    printf("%d\n",emp.age);
    printf("%d\n",emp.id);
    printf("%d\n",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\n",emp.name);
    printf("%d\n",emp.age);
    printf("%d\n",emp.id);
    printf("%d\n",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\n", (*p_emp).name);
    printf("%d\n", (*p_emp).age);
    printf("%d\n", (*p_emp).id);
    printf("%d\n", (*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\n", p_emp->name);
    printf("%d\n", p_emp->age);
    printf("%d\n", p_emp->id);
    printf("%d\n", 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\n",emp.name);
    printf("%d\n",emp.age);
    printf("%d\n",emp.id);
    printf("%d\n",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\n",emp2.name);
    printf("%d\n",emp2.age);
    printf("%d\n",emp2.id);
    printf("%d\n",emp2.salary);
    getchar();
    return 0;
}
posted @ 2021-04-06 09:30  与鹿逐秋  阅读(590)  评论(0编辑  收藏  举报