文章目录
- 为什么存在动态内存的分配
- 动态内存函数的介绍
- 介绍malloc函数的使用
- 介绍calloc函数的使用
- 介绍realloc函数的使用
- 常见的动态内存错误
- 对NULL指针的解引用操作
- 对动态开辟空间的越界访问
- 对非动态开辟内存使用free释放
- 使用free释放一块动态开辟内存的一部分
- 对同一块动态内存多次释放
- 动态开辟内存忘记释放(内存泄露)
- 练习题讲解
- 题目一:
- 题目二:
- 题目三:
- 题目四:
- c/c++程序的内存开辟
- 柔性数组
- 柔性数组的特点
- 柔性数组的优势
为什么存在动态内存的分配
我们已经学习过了对变量和数组的内存开辟方法,那就是定义变量和定义数组。但是它们是在栈区上开辟空间。函数运行完成后直接会销毁栈空间上的变量。
看下面的代码:
int a = 0; //在栈空间上开辟四个字节的内存空间
char arr[10] = {0}; //在栈空间上开辟十个字节的内存空间
但是上述的开辟空间的方式有两个特点:
1、开辟的空间的大小式不可变的。
2、数组在声明的时候必须指定数组的大小,也就是方括号里的值,它所需要的内存空间在编译时分配。
但是对于上述的情况,我们事先知道要分配多大的内存空间,有的时候我们需要在空间大小在程序运行的时候才能知道,这时候我们就可以引入动态内存开辟了。
动态内存函数的介绍
动态内存管理函数有四个分别是:malloc、calloc、realloc、free函数。
介绍malloc函数的使用
malloc函数是在栈区进行动态开辟一块连续的内存空间的函数,如果没有开辟成功它会返回一个空指针,开辟成功会返回这块地址的起始地址,也就是指向这块地址的指针。因为它会返回一个空指针,所以使用malloc函数一定要进行空指针的检查。返回值是void*指针,malloc函数不知道开辟空间的类型,具体情况按照程序员的决定,可以去把malloc的返回值强制类型转换为自己想要的类型的指针。如果参数size_t size 为0,是标准未定义的,是否开辟内存空间还是报错或者其他情况,这取决于编译器。我们来用一个代码来演示malloc函数的使用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{int* a = (int*)malloc(20);//进行访问内存空间,按照数组的形式可以访问到这块内存空间的五个元素//(20个字节,int类型是4个字节,所以为五个元素)//对a指针变量进行判空,排除malloc返回空指针,访问内存空间失败的情况//那就是使用野指针的情况。if (a == NULL){printf("访存失败,原因是:%s", strerror(errno));//strerror函数已经在前面介绍过了exit(-1);}int i = 0;int* p = a;//赋值for (i = 0; i < 5; i++){*(p + i) = i;}//打印for (i = 0; i < 5; i++){//利用数组的方式进行访问存储空间,因为//p[i] == *(p+i)printf("%d\n", p[i]);}
}
使用free函数可以对堆区上动态开辟的内存空间进行释放,不能释放栈区上的内存空间。
如果参数free参数指向的空间不是动态开辟的,free函数的行为是未定义的。参数为NULL指针时,free函数什么也不会做。我们来看下面的代码
int main()
{int* a = (int*)malloc(20);//进行访问内存空间,按照数组的形式可以访问到这块内存空间的五个元素//(20个字节,int类型是4个字节,所以为五个元素)//对a指针变量进行判空,排除malloc返回空指针,访问内存空间失败的情况//那就是使用野指针的情况。if (a == NULL){printf("访存失败,原因是:%s", strerror(errno));//strerror函数已经在前面介绍过了exit(-1);}int i = 0;int* p = a;//赋值for (i = 0; i < 5; i++){*(p + i) = i;}//打印for (i = 0; i < 5; i++){//利用数组的方式进行访问存储空间,因为//p[i] == *(p+i)printf("%d\n", p[i]);}//释放堆区上动态开辟的内存空间。free(a);//注意这里,释放a指向的空间后,a就不知道指向哪块内存空间了//所以要把它置为空,避免野指针。a = NULL;
}
介绍calloc函数的使用
calloc函数也是用来在堆区上进行内存的动态开辟。它相较于malloc函数会把开辟的内存初始化。它的原型是这样的。
void* calloc (size_t num, size_t size);
返回void*类型的指针,指定开辟num个大小为size的元素的空间,并且把每个空间初始化为0。
calloc函数和malloc函数的对比:
1、参数不同。
2、都是在堆区上申请空间,但是malloc不初始化,calloc会初始化为0,如果要初始化,就是用calloc,不需要初始化,使用malloc函数。
int main()
{int* a = (int*)calloc(5, 4);if (a == NULL){printf("%s\n", strerror(errno));exit(-1);}//查看开辟的内存空间是否被初始化for (int i = 0; i < 5; i++){printf("%d ", *(a + i));}free(a);a = NULL;
}
运行时内存状态
介绍realloc函数的使用
realloc的出现让动态内存的分配更加灵活,有的时候我们觉得内存空间分配少了,有的时候又觉得大了,这个时候就需要realloc函数出马了,realloc函数可以做到动态内存大小的调整。函数原型如下:
void* realloc (void* ptr, size_t size);
返回void* 的指针,调整ptr指向的内存空间的大小,调整为几个字节,这里需要注意size要包含之前的内存空间的大小。还需要注意,如果ptr所指向的空间后面没有空间了,realloc函数会另寻找一块空间进行分配。并且把原有的空间进行拷贝,返回新空间的地址,然后free掉之前的空间。这里可能会返回空指针。
realloc函数的两种情况:
情况一:原有空间之后有足够大的空间。
情况二:原有空间之后没有足够大的空间。
情况一:当是情况一的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况二:当时情况二的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
我们来看下面的代码:
int main()
{int* ptr = (int*)malloc(20);//需要判空if (ptr == NULL){printf("空间分配失败,原因是:%s\n", strerror(errno));return;}else{//业务处理}int* temp = (int*)realloc(ptr, 40);//需要进行判空。if (temp == NULL){printf("空间分配失败,原因是:%s\n", strerror(errno));return;}else{ptr = temp;//业务处理}//free掉内存空间free(ptr);ptr = NULL;return 0;
}
执行时内存空间状态
还需注意一点:如果realloc函数ptr接收到的是一个空指针,那么它的功能就相当于malloc函数了。
常见的动态内存错误
对NULL指针的解引用操作
#define MAX 20
void test()
{int* p = (int*)malloc(MAX);//解决办法,判空if (p == NULL) {return;}else{*p = 20;}//如果p是空指针,那么会发生对空指针的//解引用操作。free(p);
}
对动态开辟空间的越界访问
void test()
{int i = 0;int* p = (int*)malloc(10 * sizeof(int));if (NULL == p){exit(-1);}for (i = 0; i <= 10; i++){*(p + i) = i; //当i是10的时候越界访问}free(p);
}
对非动态开辟内存使用free释放
void test()
{int a = 10;int* p = &a;free(p); //这是对栈区上的内存空间free,会发生错误
}
使用free释放一块动态开辟内存的一部分
void test()
{int* p = (int*)malloc(100);p++;//p不在指向内存空间的起始位置。free(p);
}
对同一块动态内存多次释放
void test()
{int* p = (int*)malloc(100);free(p);free(p); //对p空间重复释放
}
动态开辟内存忘记释放(内存泄露)
void test()
{int* p = (int*)malloc(100);if (p != NULL){*p = 20;}
}
int main()
{while (1){test();}//如果这个程序不间断的跑下去,会把内存空间全部泄露,别人就不能用了。
}
忘记释放不在使用的动态开辟的空间会造成内存泄漏。所以说动态开辟的空间一定要用free释放,并且正确释放。
练习题讲解
题目一:
void GetMemory(char* p)
{p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}
题目二:
char* GetMemory(void)
{char p[] = "hello world";return p;
}
void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}
题目三:
void GetMemory(char **p, int num)
{*p = (char *)malloc(num);
}
void Test(void)
{char *str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);
}
题目四:
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);//对已释放的堆区的内存空间进行访问。//程序会崩溃,写出了内存上的错误。if (str != NULL){strcpy(str, "world");printf(str);}
}
c/c++程序的内存开辟
c/c++ 程序内存分配的几个区域
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存空间容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
3、数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码。
观察上面的图,我们就可以很好的理解static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就被销毁了。但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,知道程序结束才销毁。
所以声明周期会延长。
柔性数组
在C99标准中,结构体中的最后一个元素允许是未知大小的数组,这就叫做柔性数组的成员。
struct st_type
{int i;int a[0] //柔性数组成员
};
有些编译器报错无法编译可以写成
struct st_type_1
{int i;int a[];
};
柔性数组的特点
结构中的柔性数组成员前面前面至少有一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
我们来用代码说话:
包含柔性数组成员的结构用malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
我们来用代码说话:
int main()
{int i = 0;struct st_type* st = (struct st_type*)malloc(sizeof(struct st_type)+100*sizeof(int));st->i = 100;//判空if (st == NULL){return;}for (i = 0; i < 100; i++){st->a[i] = 0;}for (i = 0; i < 100; i++){printf("%d ", st->a[i]);}//调整内存大小struct st_type* temp = (struct st_type*)realloc(st, sizeof(struct st_type*) + 200 * sizeof(int));if (temp == NULL){return;}else{st = temp;}free(st);return 0;
}
柔性数组的优势
实现柔性数组功能的两种方案。
//方案一:
int main()
{int i = 0;struct st_type* st = (struct st_type*)malloc(sizeof(struct st_type)+100*sizeof(int));st->i = 100;//判空if (st == NULL){return;}for (i = 0; i < 100; i++){st->a[i] = 0;}for (i = 0; i < 100; i++){printf("%d ", st->a[i]);}//调整内存大小struct st_type* temp = (struct st_type*)realloc(st, sizeof(struct st_type*) + 200 * sizeof(int));if (temp == NULL){return;}else{st = temp;}free(st);return 0;
}//方案二:
typedef struct st_type
{int i;int* a //柔性数组成员
}type_a;
int main()
{type_a* st = (type_a*)malloc(sizeof(type_a));if (st == NULL){return;}int* ptr = (int*)malloc(100 * sizeof(int));if (ptr == NULL){return;}else{st->i = 100;st->a = ptr;}//使用//调整内存大小int* pt = (int*)realloc(st->a, 200 * sizeof(int));if (pt == NULL){return;}else{st->a = pt;st->i = 200;}//使用//释放free(st->a);free(st);st = NULL;
}
上述代码都可以实现同样的功能,但是方案一有两个好处:
第一个好处是:方便内存的释放
如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其他成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。