本章主要内容如下:
??为什要有动态内存管理?
??动态内存函数学习
??常见的动态内存错误
??动态内存相关题目
??柔型数组介绍
为什么要有动态内存分配?
举例子: 比如上一章节我们学习的通讯录,我们设置的是可以存储1000个联系人的信息,如果我们没有那么多朋友,用不了那么多空间,造成浪费;如果我们朋友很多,1000个不够用,又存不下?该怎么办呢?
所以这种开辟内存的方式有如下缺点:
- 空间开辟大小是固定的。
- 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组在编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。
C语言提供动态内存分配,空间大小可以自己调整。
我们之前学习过内存划分:
栈区:局部变量、函数形参
堆区:动态内存分配(malloc、calloc、realloc、free)
静态区:静态变量(static修饰的)、全局变量(静态区的内容在程序的生命周期内都存在,由编译器在编译时候分配)
在这里我们指的是用户空间,对于内核空间不做讲解,以后学习操作系统再详细学习。
malloc和free
malloc函数
void *malloc( size_t size );
头文件:<stdlib.h> or <malloc.h>
函数功能:向内存申请一块连续可用的空间(内存块),并返回指向这块空间的指针。
参数:size是申请的字节个数 返回值:已分配的内存块的起始地址,返回类型是void*(不知道申请的空间存放什么类型的数据)
-
如果开辟成功,则返回一个指向开辟好的空间的指针。 -
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查,即使申请的内存很小。 -
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,若要返回指向非void类型的指针,请对返回值强制类型转换。 -
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
free函数
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
函数功能:释放动态开辟的内存空间
头文件:<stdlib.h> or <malloc.h>
参数:ptr是动态开辟内存的起始地址
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。(所以不要这样做!!!)
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
注意:free之后,指针指向的空间释放,还给操作系统,这块空间就不能在使用,但是ptr仍然指向这块空间,p就是野指针,如果使用p那么就会造成非法访问,所以我们一般释放后,将指针置为NULL,让ptr指针再也找不到这块空间。
举个例子:
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int n = 0;
scanf("%d",&n);
int* p =(int*)malloc(n*sizeof(int));
if (p == NULL)
{
perror("malloc");
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
calloc函数
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下
void *calloc( size_t num, size_t size );
头文件:<stdlib.h> or <malloc.h> 函数功能:开辟一块连续可用的空间,并且把空间的每个字节初始化为0。
参数: num为元素个数 size 为每个元素的大小
所以num*size是开辟的总字节数。
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int* p = (int*)calloc(10,sizeof(int));
if (p == NULL)
{
printf("calloc:%s\n",strerror(errno));
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
malloc和calloc的区别
malloc函数只负责在堆区申请空间,并且返回起始地址,不初始化内存空间; calloc函数在堆区申请空间并且在返回起始地址之前,对空间进行初始化,将所有字节初始化为0;
所以如果,我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
realloc
realloc函数是调整动态内存空间大小,realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。
那 realloc 函数就可以做到对动态开辟内存大小的调整。函数原型如下:
void *realloc( void *memblock, size_t size );
头文件:<stdlib.h> or <malloc.h> 函数功能:重新分配内存块 参数:memblock是原来动态开辟的内存空间的起始地址 size是新内存块的字节个数 返回值:调整之后的内存空间的起始地址
注意:这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
-
realloc返回一个指向重新分配(可能是移动的)内存块的空指针,若要获得指向非void类型的指针,要对返回值进行强制类型转换。 -
如果参数size为0且memblock参数不是NULL(第一种),或者如果没有足够的可用内存将内存块扩展到给定的大小(第二种),则返回值为NULL。 -
返回值为NULL时,在第一种情况下,会释放原来的内存块。在第二种情况下,原始内存块没有变化,不会释放。 -
对于realloc函数的参数memblock,如果memblock是NULL,那么realloc函数行为和malloc函数一样,如果memblock不为NULL, 那么它应该是一个动态开辟的内存块的起始地址(malloc、calloc、realloc的返回值)。
realloc调整内存空间的两种情况
情况1:原有空间之后有足够大的空间
返回原空间的起始地址。
情况2:原有空间后面没有足够大的空间
在内存中重新找一块连续可用空间,把原来空间中的数据拷贝到新的内存空间,并释放掉原来开辟的空间,返回新空间的起始地址。
如果realloc开辟空间失败,会返回一个NULL。 因为realloc开辟空间可能失败也可能成功,所以我们使用一个临时指针变量来接收realloc的返回值,如果返回值为NULL,那么不影响原来空间,如果realloc开辟空间成功,再把临时指针变量赋值给原指针。
看下面的代码,只有新的空间开辟成功时,才把ptr赋值给p,保证如果新的空间开辟失败,p不会变成空指针,程序依然能进行,这里realloc扩容失败,程序结束了,开辟的所有内存自动释放,我们没有手动释放,但是在以后的程序中,如果程序要一直运行,在realloc扩容失败的情况下,我们需要对原空间进行释放,否则会造成内存泄漏。
#include <stdlib.h>
int main()
{
int* p = calloc(10,sizeof(int));
if (p == NULL)
{
printf("calloc:%s\n",strerror(errno));
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p+i) = i;
}
int* ptr = realloc(p,20*sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
else
{
return -1;
}
for (i = 10; i < 20; i++)
{
*(p + i) = i;
}
for (i = 0; i < 20; i++)
{
printf("%d ",*(p+i));
}
free(p);
p = NULL;
return 0;
}
动态内存常见错误
int main()
{
int* p = (int*)malloc(20);
*p = 0;
return 0;
}
所以我们应该养成习惯,对动态开辟的内存要进行有效性判断,避免对NULL解引用操作。 正确代码如下:
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
return -1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
int main()
{
int* p = (int*)malloc(200);
if (p == NULL)
{
return -1;
}
int i = 0;
for (i = 0; i < 60; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
return -1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
return 0;
}
上述代码报错,改变了p的值,所以free§并不能达到释放动态开辟的内存空间的效果,free的参数必须是动态开辟内存的起始地址。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return -1;
}
free(p);
free(p);
p = NULL;
return 0;
}
上述代码报错,不能对同一块动态内存多次释放。
因为free释放NULL,什么都不做,所以我们每次释放空间之后,置为NULL,这样即使再次释放,释放的也是NULL.
下面代码也是错误
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(4*sizeof(int));
if (p == NULL)
{
perror("maoolc");
return -1;
}
int i = 0;
for (i = 0; i < 4; i++)
{
p[i] = i;
}
int* ptr = p;
free(p);
free(ptr);
p = NULL;
return 0;
}
因为ptr和p都只指向同一块动态开辟的内存空间,所以通过p指针已经释放了这块内存空间,如果再对ptr指针进行释放,就会报错。
在堆上开辟的空间有两种回收的方式: 1.主动使用free释放 2.当程序退出的时候,申请的空间操作系统自动回收
对于需要一直运行的程序,比如服务器端程序,动态开辟的空间如果不释放,一直被占用,别人就一直不能使用这块空间,如果一直开辟空间,全都不释放,内存最终会被消耗完,造成内存泄漏,程序卡死。
注意:忘记释放不再使用的动态开辟的空间会造成内存泄漏。 切记: 动态开辟的空间一定要释放,并且正确释放。
总结:我们动态开辟内存使用步骤可以简单总结如下: 1.开辟空间 2.指针有效性判断 3.使用 4.释放 5.指针置为NULL(防止变成野指针,NULL不是野指针)
动态内存分配和指针的经典笔试题
(1)
#include <stdio.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
上面程序的结果是什么呢?我们运行代码发现程序崩溃了。
下面我们分析以下这段代码 ??首先,在Test函数中,定义了一个char*类型的指针str,初始化为NULL,然后调用GetMemory函数,通过值传递把str的值传递给p,p是str的一份临时拷贝,此时p就是NULL,改变p并不会影响str,str仍为NULL;
??其次,在GetMemory函数内部,malloc动态内存开辟100个字节空间,并把这块空间的起始地址赋值给p,p来维护这100个字节的空间,不会影响str,此时str仍然为NULL,此时strcpy试图把“hello world”拷贝到str时,程序就崩溃了,因为NULL指向的空间是不允许我们访问的!(不能对NULL进行解引用操作)
??在GetMemory函数内部,malloc开辟的空间,我们并没有进行释放,造成内存泄漏,而且一旦该函数调用结束后,我们就不可能再进行释放了,因为p是局部变量,函数调用结束后,p就销毁了,我们就找不到这个变量了,所以想释放都释放不了了!
下面我们来修改这段代码,使其能够正常运行,上面这段代码的本意是想把函数内动态开辟的内存空间起始地址给函数外面的str指针,那么有几种方式可以修改呢?
代码1:
使用址传递,在函数内部通过指针来改变函数外面需要修改的变量
#include <stdlib.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
这里p是局部变量,函数调用结束p销毁,但是p销毁并不影响通过str来释放动态开辟的内存,因为str已经指向的这块内存。
栈空间在函数调用结束就自动销毁了,但是堆空间要我们自己通过free来进行释放。
代码2:
#include <stdlib.h>
#include <string.h>
char* GetMemory(char* p)
{
p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
将代码2进行优化:
#include <stdlib.h>
#include <string.h>
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
补充一点,关于printf函数
int main()
{
char* p = "hello world";
printf("hello world");
printf(p);
}
只有字符串可以这样打印。
(2) 如下代码的结果是什么?
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
程序运行打印的是随机值,why?
我们来分析一下,在Test函数内部,定义指针str,调用GetMemory函数,在GetMemory函数内部,用常量字符串"hello world"初始化字符数组p,p是局部变量,在栈上开辟空间,然后将p返回给str,此时,虽然str的值是p,但是p是局部变量,函数调用结束,p这块空间就释放归还给操作系统了,此时str就是野指针了,那么此时打印这块空间的内容就是不确定的了,如果我们释放这块空间后,没有其他人使用这块空间,那么可能还保留了我们放进去的值,如果别人使用了,就可能改变了这块空间的内容,打印出来的结果就是不确定的。
我们来简单画一下函数栈帧的创建及销毁
函数GetMemory栈帧销毁后,此时str指向的空间已经释放啦
为了帮助理解,我们来举个例子,比如小明去住宾馆,住在302房间(开辟一块空间,字符数组p),此时302这个房间的使用权就归小明,等到第二天,小明退了房间,那么小明就没有了302房间的使用权限了(变量销毁,空间使用权限归还给操作系统,字符数组p的空间销毁),但是302房间仍然在那里,这时,就算小明记住了302房间的地址(释放掉的那块空间的地址,也就是指针p),找到了302房间,却无法使用302房间了(对这块空间没有使用权限),并且他也不知道此时302房间里面的东西是否和他离开时一样(空间的内容是否改变),如果没有新的客人住进来,那么302房间里面还和小明离开时一样,如果有新的客人住进来,那么里面的东西可能就改变了,如果小明强行进入房间是非法的(非法访问内存)。
非法访问内存编译器不一定会报错,但是并不代表这样做是合法的!
对于上面这类问题是:返回栈空间地址的问题!此时返回的指针是野指针!
如下代码:
int* test()
{
int n = 10;
return &n;
}
int main()
{
int* p = test();
printf("%d\n",*p);
return 0;
}
有可能打印出来10,如果这块空间被使用过了,那么结果可能就不是10 了
**注意:**空间销毁的意思是:空间还给操作系统,空间的内容可能不改变,可能被别人使用改变了内容!内存空间还存在,只是我们没用使用权限!
我们可以使用static来修饰局部变量,局部变量存储在静态区,那么函数调用结束,空间依然存在,我们可以通过指针来访问来访问这块空间:
#include <stdio.h>
#include <string.h>
char* test()
{
static char p[] = "hello world";
return p;
}
int main()
{
char* str = test();
size_t i = 0;
for (i = 0; i < strlen(str); i++)
{
printf("%c",str[i]);
}
return 0;
}
再看如下代码:
int* test()
{
static int a = 0;
a++;
printf("a = %d\n",a);
return &a;
}
int main()
{
int* p = test();
*p = 10;
test();
return 0;
}
结果为
(3)
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
这段代码的问题是存在内存泄漏,没有将动态分配的内存进行释放! 并且没有进行指针有效性检查。
更改这段代码:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
(4)
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
注意,free仅仅是释放指针指向的内存空间,并不会改变指针的值,这里free释放str之后,由于str指向的空间被释放掉了,所以str变成野指针,虽然str仍然指向释放掉的那块内存空间其实地址,但是并不能访问,strcpy(str, “world”);时,strcpy内部会对str进行解引用操作,非法访问内存(释放过的空间,我们是没有使用权限的),程序崩溃。所以释放空间后,我们要把指针置为NULL。
修改为如下:
void Test(void)
{
char* str = (char*)malloc(100);
if(str == NULL)
{
return ;
}
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
C/C++程序内存分配的区域划分
内核空间是给操作系统使用的,用户无法使用的,我们现在只讨论如下几个区域。
-
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,若开辟空间过大,可能会造成栈溢出。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。 -
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。 -
数据段(静态区) :存放全局变量、静态数据(static)。程序结束后由系统释放。 -
代码段:存放函数体(类成员函数和全局函数)的二进制代码、常量数据。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。
普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。
柔性数组(flexible array)
在C99标准中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
关于柔性数组的写法,有如下两种
写法1
typedef struct st
{
int i;
int a[0];
}st;
对于第一种写法,有些编译器会报错,那么可以使用第二种写法 写法2
typedef struct st
{
int i;
int a[];
}st;
柔性数组的特点
看如下代码:
typedef struct st
{
int i;
int a[];
}st;
printf("%d\n", sizeof(st));
因为结构成员数组a是柔性数组,sizeof(st)得到的大小是不包括柔性数组的大小的,所以结果是4.
柔性数组的使用
动态开辟空间的大小 = 结构体其他成员的大小 + 需要的柔性数组的大小
#include <stdlib.h>
#include <string.h>
#include <errno.h>
typedef struct st
{
int i;
int a[];
}st;
int main()
{
st* p = (st*)malloc(sizeof(st) + 100 * sizeof(int));
if(p == NULL)
{
printf("%s\n",strerror(errno));
return -1;
}
int i = 0;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
p = NULL;
return 0;
}
这样柔性数组成员a,相当于获得了100个整型元素的连续空间.
如果柔性数组的空间不够用了,那么我们需要使用realloc()函数来重新开辟空间
#include <stdlib.h>
#include <string.h>
#include <errno.h>
typedef struct st
{
int i;
int a[];
}st;
int main()
{
st* p = (st*)malloc(sizeof(st) + 100 * sizeof(int));
if (p == NULL)
{
printf("%s\n",strerror(errno));
return -1;
}
int i = 0;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
st* ps = (st*)realloc(p, sizeof(st) + 200 * sizeof(int));
if(ps == NULL)
{
perror("realloc");
}
else
{
p = ps;
}
free(p);
p = NULL;
return 0;
}
柔性数组的优势
把上面的代码看成第一种写法,那我们还有另外一种写法,有的人可能会说,第一种写法在结构体中放一个柔性数组这么麻烦,可不可以在结构体中放一个数组指针,这个指针指向一个动态开辟的数组?答案是可以的,我们来实现一下
方法2
#include <stdlib.h>
#include <string.h>
#include <errno.h>
typedef struct st_type
{
int i;
int* p_a;
}type_a;
int main()
{
type_a* p = malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int*)malloc(p->i * sizeof(int));
int i = 0;
for (i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
int* ptr = realloc(p->p_a,200*sizeof(int));
if (ptr == NULL)
{
printf("扩容失败!\n");
return -1;
}
p->p_a = ptr;
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
这里我们做了两次动态内次分配,以及两次空间的释放。
必须先释放p_a指向的内存空间,再释放p指向的内存空间。
这种写法和第一种写法相比,哪一种更好呢? 既然一个指针能解决的事情,我们为什么要弄出来一个柔性数组呢?因为我们想给一个结构体内的数据分配一个连续的内存!
方法1 的实现有两个好处:
第一个好处是:方便内存释放。 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片(malloc次数越多,内存碎片越多)。
内存池开辟使用malloc,但是内存池中内存的使用不是使用malloc。 内存池是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
内存池优点: 比malloc/free进行内存申请/释放的方式快 不会产生或很少产生堆碎片 可避免内存泄漏
关于“C语言结构体里的数组和指针”更详细的讲解:C语言结构体里的数组和指针
本章完。
|