动态内存管理
动态内存管理的几个函数
- malloc -- 申请动态内存空间
- free -- 释放动态内存空间
- calloc -- 申请并初始化一系列内存空间
- realloc -- 重新分配内存空间
malloc
-
函数原型:
void *malloc(size_t size);
-
malloc
函数向系统申请分配size
个字节的内存空间,并返回一个指向这块空间的指针(void
类型)。 -
如果函数调用成功,返回一个指向申请的内存空间的指针,由于返回类型是
void
指针(void *
),所以它可以被转换成任何类型的数据;如果函数调用失败,返回值是NULL
。另外,如果size
参数设置为0, 返回值也可能是NULL
,但这并不意味着函数调用失败。#include<stdio.h> #include<stdlib.h> int main() {int *ptr;ptr = (int *)malloc(sizeof(int)); //因为不是所有的操作系统的int都是4个字节,所以参数使用sizeof()// (int *)将返回的void指针转化为int指针if (ptr == NULL){printf("分配内存失败了! \n");exit(1); //退出程序,需导入头文件<stdlib.h>}printf("请输入一个整数 : ");scanf("%d", ptr);printf("你输入的整数是 : %d\n", *ptr);return 0; }
运行结果
请输入一个整数 : 100 你输入的整数是 : 100
-
注意:由于
malloc
申请的空间位于内存的堆上,如果你不主动释放堆上的内存资源,那么它将永远存在,直到程序被关闭。所以当我们不再使用这块内存时,要自己动手释放,避免造成内存泄漏。 -
malloc
不仅可以申请存储基本数据类型的空间,事实上它还可以申请一块任意尺寸的内存空间。对于后者,由于申请得到的空间是连续的,所以我们经常用数组来进行索引即可:#include <stdio.h> #include <stdlib.h> int main(void) {int *ptr = NULL;int num, i;printf("请输入待录入整数的个数:");scanf("%d", &num);ptr = (int *)malloc(num * sizeof(int));for (i = 0; i < num; i++){printf("请录入第%d个整数:", i+1);scanf("%d", &ptr[i]);}printf("你录入的整数是:");for (i = 0; i < num; i++){printf("%d ", ptr[i]);}putchar('\n');free(ptr);return 0; }
free
-
函数原型:
void free(void *ptr);
-
free
函数释放ptr
参数指向的内存空间。该内存空间必须是由malloc
、calloc
或realloc
函数申请的。否则,该函数将导致未定义行为。如果ptr
参数是NULL
,不执行任何操作。注意:该函数并不会修改
ptr
参数的值,所以调用后它仍然指向原来的地方(变为非法空间)。#include<stdio.h> #include<stdlib.h> int main() {int *ptr;ptr = (int *)malloc(sizeof(int)); //因为不是所有的操作系统的int都是4个字节,所以参数使用sizeof()// (int *)将返回的void指针转化为int指针if (ptr == NULL){printf("分配内存失败了! \n");exit(1); //退出程序,需导入头文件<stdlib.h>}printf("请输入一个整数 : ");scanf("%d", ptr);printf("你输入的整数是 : %d\n", *ptr);free(ptr); //释放内存空间printf("你输入的整数是 : %d\n", *ptr); //再次打印会出错return 0; }
内存泄漏
导致内存泄漏主要有两种情况:
-
隐式内存泄漏(即用完内存块没有及时使用free函数释放)
注: 下面这段程序可能会造成系统假死,运行需谨慎
#include<stdio.h> #include<stdlib.h> int main() {while (1) // 死循环{malloc(1024); //不断地申请内存空间}return 0; }
-
丢失内存块地址
#include<stdio.h> #include<stdlib.h> int main() {int *ptr;int num = 123;ptr = (int *)malloc(sizeof(int)); //因为不是所有的操作系统的int都是4个字节,所以参数使用sizeof()// (int *)将返回的void指针转化为int指针if (ptr == NULL){printf("分配内存失败了! \n");exit(1); //退出程序,需导入头文件<stdlib.h>}printf("请输入一个整数 : ");scanf("%d", ptr);printf("你输入的整数是 : %d\n", *ptr);ptr = # //ptr指向局部变量printf("你输入的整数是 : %d\n", *ptr);free(ptr); //此时ptr指向的不是malloc申请的内存空间,使用free函数释放会出错return 0; }
初始化内存空间
由于malloc
并不会帮你初始化申请的内存空间,所以需要自己进行初始化。
memset
-
以men开头的函数被编入字符串标准库,函数的声明包含在
string.h
这个头文件中:memset
—— 使用一个常量字节填充内存空间memcpy
—— 拷贝内存空间memmove
—— 拷贝内存空间memcmp
—— 比较内存空间memchr
—— 在内存空间中搜索一个字符
-
memset
函数可以用来初始化内存空间#include <stdio.h> #include <stdlib.h> #include <string.h> #define N 10 int main(void) {int *ptr = NULL;int i;ptr = (int *)malloc(N*sizeof(int));if (ptr == NULL){exit(1);}memset(ptr, 0, N*sizeof(int));for (i = 0;i < N; i++){printf("%d ",ptr[i]);}putchar('\n');free(ptr); }
calloc
-
函数原型:
void *calloc(size_t nmemb, size_t size);
-
calloc
函数在内存中动态地申请nmemb
个长度为size
的连续内存空间(即申请的总空间尺寸为nmemb * size
),这些内存空间全部被初始化为0. -
calloc
函数与malloc
函数的一个重要区别是:calloc
函数在申请完内存后,自动初始化该内存空间为零malloc
函数不进行初始化操作,里边数据是随机的
-
下面两种写法是等价的:
// calloc() 分配内存空间并初始化 int *ptr = (int *)calloc(8, sizeof(int)); //两个参数
// malloc() 分配内存空间并用 memset() 初始化 int *ptr = (int *)malloc(8 * sizeof(int)); //一个参数 memset(ptr, 0, 8 * sizeof(int));
拓展内存空间
memcpy
有时候我们可能会需要对原来的分配的内存空间进行拓展,这时就需要再调用一次malloc
申请一个更大的内存空间,再使用memcpy
将原来的数据搬进去
-
函数原型:
void *memcpy(void *dest,const void *src, size_t n);
-
memcpy
函数从 src 指向的内存空间拷贝 n 个字节到dest指向的内存空间。src和dest指向的内存区域不能出现重叠,否则应该使用memmove
函数。 -
memcpy
函数并不关心被复制的数据类型,只是逐字节地进行复制,这给函数的使用带来了很大的灵活性,可以面向任何数据类型进行复制。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{int *ptr1 = NULL;int *ptr2 = NULL;//第一次申请的内存空间ptr1 = (int *)malloc(10 * sizeof(int));//进行若干操作之后发现ptr1申请的内存空间不够用//第二次申请的内存空间ptr2 = (int *)malloc(20 * sizeof(int));//将ptr1的数据拷贝到ptr2中memcpy(ptr2, ptr1, 10);free(ptr1);//对ptr2申请的内存空间进行若干操作...free(ptr2); //释放对ptr2申请的内存空间return 0;
}
realloc
-
函数原型:
void *realloc(void *ptr, size_t size);
-
以下几点是需要注意的:
realloc
函数修改ptr
指向的内存空间大小为size字节- 如果新分配的内存空间比原来的大,则旧内存块的数据不会发生改变;如果新的内存空间的大小小于旧的内存空间,可能会导致数据丢失,慎用!
- 该函数将移动内存空间的数据并返回新的指针
- 如果ptr参数为NULL,那么调用该函数就相当于调用
malloc(size)
- 如果size参数为 0,并且ptr参数不为NULL,那么调用该函数就相当于调用
free(ptr)
- 除非ptr参数为NULL,否则ptr的值必须由先前调用
malloc
、calloc
或realloc
函数返回
-
设计一个程序,要求:
- 不使用
malloc
函数,使用realloc
函数申请内存空间 - 让用户一直输入一个整数,用户每输入一个整数就新增加一个整形空间存放。直到用户输入-1表示输入结束,再把输入的所有整数打印出来
#include<stdio.h> #include<stdlib.h>int main(void) {int i, num;int count = 0;int *ptr = NULL; //注意,这里必须初始化为NULLdo{printf("请输入一个整数(输入-1表示结束) : ");scanf("%d",&num);count++;ptr = (int *)realloc(ptr, count * sizeof(int));if (ptr == NULL){exit(1);}ptr[count-1] = num;}while (num != -1);printf("输入的整数分别是 : ");for (i=0;i<count;i++){printf("%d ",ptr[i]);}putchar('\n');free(ptr);return 0; }
运行结果
请输入一个整数(输入-1表示结束) : 1 请输入一个整数(输入-1表示结束) : 2 请输入一个整数(输入-1表示结束) : 3 请输入一个整数(输入-1表示结束) : -1 输入的整数分别是 : 1 2 3 -1 //如果不想打印-1,把for循环条件表达式改为"i<count-1"即可
- 不使用
C语言的内存布局
#include<stdio.h>
#include<stdlib.h>
int global_uninit_var;
int global_init_var1 = 520;
int global_init_var2 = 880;
void func(void);
void func(void)
{;
}
int main(void)
{int local_var1;int local_var2;static int static_uninit_var;static int static_init_var = 456;char *str1 = "I love FishC.com!";char *str2 = "You are right!";int *malloc_var = (int *)malloc(sizeof(int));printf("addr of func -> %p\n",func);printf("addr of str1 -> %p\n",str1);printf("addr of str2 -> %p\n",str2);printf("addr of global_init_var1 -> %p\n",&global_init_var1);printf("addr of global_init_var2 -> %p\n",&global_init_var2);printf("addr of static_init_var -> %p\n",&static_init_var);printf("addr of static_uninit_var -> %p\n",&static_uninit_var);printf("addr of global_uninit_var -> %p\n",&global_uninit_var);printf("addr of malloc_var -> %p\n",malloc_var);printf("addr of local_var1 -> %p\n",&local_var1);printf("addr of local_var2 -> %p\n",&local_var2);return 0;
}
运行结果:
addr of func -> 00007ff754751591
addr of str1 -> 00007ff754759000
addr of str2 -> 00007ff754759012
addr of global_init_var1 -> 00007ff754758010
addr of global_init_var2 -> 00007ff754758014
addr of static_init_var -> 00007ff754758018
addr of static_uninit_var -> 00007ff75475c044
addr of global_uninit_var -> 00007ff75475c040
addr of malloc_var -> 0000021f4af616a0
addr of local_var1 -> 000000d8845ff674
addr of local_var2 -> 000000d8845ff670
//注意: 每个人的结果不一定相同
规律:
C语言内存空间划分
根据内存地址从低到高分别划分为:
- 代码段(Text segment)
- 数据段(Initialized data segment)
- BSS段(Bss segment/Uninitialized data segment)
- 栈(Stack)
- 堆(Heap)
代码段
- 代码段通常是指用来存放程序执行代码的一块内存区域。
- 这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。
- 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
数据段
- 数据段通常用来存放已经初始化的全局变量和局部静态变量。
BSS段
- BSS 段通常是指用来存放程序中未初始化的全局变量的一块内存区域。
- BSS 是英文 Block Started by Symbol 的简称,这个区段中的数据在程序运行前将被自动初始化为数字 0。
堆
- 前边我们学习了动态内存管理函数,使用它们申请的内存空间就是分配在这个堆里边。
- 所以,堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。
- 当进程调用
malloc
等函数分配内存时,新分配的内存就被动态添加到堆上;当利用free
等函数释放内存时,被释放的内存从堆中被剔除。
栈
- 栈是函数执行的内存区域,通常和堆共享同一片区域。
栈和堆的区别
堆和栈则是 C 语言运行时最重要的元素,下面我们将两者进行对比。
-
申请方式:
- 堆由程序员手动申请
- 栈由系统自动分配
-
释放方式:
- 堆由程序员手动释放
- 栈由系统自动释放
-
生存周期:
-
堆的生存周期由动态申请到程序员主动释放为止,不同函数之间均可自由访问
-
栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问
#include<stdio.h> #include<stdlib.h> int *func(void) {int *ptr = NULL; ptr = (int *)malloc(sizeof(int));//在堆上申请一个内存空间if (ptr == NULL){exit(1);}*ptr = 520;return ptr; //返回ptr的地址 } int main(void) {int *ptr = NULL;ptr = func(); printf("%d\n",*ptr);//在main函数中访问func函数中申请的内存空间的值free(ptr); }
注意:这段代码中,在
func
函数中申请内存空间,在main
函数中访问并释放。但实战中不建议这样做,建议在哪个函数中申请了内存空间就在哪个函数中释放。
-
-
发展方向:
-
堆和其它区段一样,都是从低地址向高地址发展
-
栈则相反,是由高地址向低地址发展
#include<stdio.h> #include<stdlib.h> int main(void) {int *ptr1 = NULL; //指针变量存放在栈中int *ptr2 = NULL;ptr1 = (int *)malloc(sizeof(int)); //内存空间存放在堆中ptr2 = (int *)malloc(sizeof(int));printf("stack: %p -> %p\n",&ptr1, &ptr2);//栈 -> 指针变量的地址printf("heap: %p -> %p\n",ptr1,ptr2);//堆 -> 指针变量指向的地址return 0; }
运行结果
stack: 00000060fadffb28 -> 00000060fadffb20 //栈 高 -> 低 heap: 000001a4c5fe16a0 -> 000001a4c5fe16e0 //堆 低 -> 高
-