目前为止,我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
上述开辟空间的方式其实有两个缺点:
而在实际情况中,有时候我们需要的空间大小在程序运行的时候才能知道, 那么数组在编译时开辟空间的方式就不能满足了。
要解决这个问题,就需要使用动态内存函数,这些函数全部包含在头文件
中。
在C语言中,提供了动态内存开辟的函数:
void* malloc (size_t size);
这个函数能够向内存申请一块连续可用的空间,并返回指向这块空间的指针。
关于malloc
函数,需要注意以下几点:
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL
指针,因此malloc
的返回值一定要做检查。
返回值的类型是void*
,所以malloc
函数并不知道开辟空间的类型,具体在使用的时候需要使用者自己来决定。
如果参数size
为0
,即开辟0
个字节,malloc
的行为是标准未定义的,取决于编译器。
malloc
申请到空间后会直接返回这块空间的起始地址,不会初始化空间的内容。
和局部变量、形式参数等存放在内存的栈区不同,动态内存开辟的空间存放在内存的堆区。
例:
#include
#include
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)//如果p为空,则开辟空间失败
{
perror("malloc");
return 1;
}
//开辟成功
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%p\n", p + i);//打印所开辟空间的值
}
return 0;
}
输出结果:
由于malloc
申请到空间后会直接返回这块空间的起始地址,不会初始化空间的内容,所以我们从输出结果可以看到我们打印所开辟空间的值是一串乱码。
由于malloc
申请的空间只有在程序退出时才会还给操作系统,并不会主动释放,所以C语言提供了另外一个函数free
,专门用来释放和回收动态内存,其原型如下:
void free (void* ptr);
关于free
函数,需要注意以下几点:
ptr
指向的空间不是动态开辟的,那么free
函数的行为未定义。ptr
是NULL
指针,则free
函数什么事都不做。free
释放完参数ptr
指向的空间后,一定要把ptr
置为NULL
,否则ptr
就是野指针。例:
#include
#include
int main()
{
int num = 0;
int i = 0;
scanf("%d", &num);//输入空间的大小
int* ptr = NULL;
ptr = (int*)malloc(num * sizeof(int));
if (NULL != ptr)//判断ptr指针是否为空
{
for (i = 0; i < num; i++)
{
*(ptr + i) = 0;
}
}
for (i = 0; i < num; i++)
{
printf("%d ", *(ptr + i));
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;
return 0;
}
输出结果:
C语言还提供了一个calloc
函数也可以用来动态内存分配,其原型如下:
void* calloc (size_t num, size_t size);
calloc
函数可以为num
个大小为size
的元素开辟一块空间,并且把空间的每个字节初始化为0
,这里和malloc
不会初始化空间的内容有所不同。
例:
#include
#include
int main()
{
int i = 0;
int* p = (int*)calloc(10, sizeof(int));
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
if (NULL != p)
{
for (i = 0; i < 10; i++)
{
*(p + i) = 1;
}
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
输出结果:
有时候我们会发现过去申请的空间太小了,而有时候又会觉得申请的空间过大了,那么为了合理的分配内存,C语言中还提供了一个realloc
函数来实现对动态开辟的内存大小进行调整,其原型如下:
void* realloc (void* ptr, size_t size);
关于realloc
函数,需要注意以下几点:
ptr
表示要调整的内存地址。size
表示调整之后的内存大小。ptr
为空指针,那么realloc
的功能和malloc
相同。需要特别说明的是,realloc
函数在调整内存空间时存在两种情况:
情况一:原有空间的后面还有足够大的空间。
在这种情况下, 要扩展内存就可以直接在原有内存之后追加空间,原来空间的数据不发生变化。
情况二:原有空间的后面没有足够大的空间。
在这种情况下,realloc
函数需要先在堆空间上另找一个合适大小的连续空间来使用,然后将旧空间中的数据拷贝到新空间,拷贝完成之后会释放掉旧空间,最后返回新空间的起始地址。
情况三:找不到足够大的空间扩容。
在这种情况下,realloc
函数就会返回一个空指针。如果扩容失败,由于之前的空间没有释放,而此时ptr
已被置空,这就导致我们丢失了原来那个空间的地址,进而让我们无法对这片空间进行释放,像这种无法释放已申请的内存空间的情况就被称为内存泄漏。
为了避免情况三的出现,我们使用realloc
来扩容的时候应该创建一个新的指针变量来指向扩容后的空间,如果扩容失败,那么这个指针为空也不会影响我们找到原来的空间,如果扩容成功,我们就可以把指向原来空间的指针改为指向扩容后的空间。
例:
#include
#include
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
//增加空间
int* ptr = realloc(p, 80);
if (ptr != NULL)//判断扩容是否成功
{
p = ptr;
ptr = NULL;
}
else
{
perror("realloc");//扩容失败,报错
return 1;
}
for (i = 0; i < 20; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
输出结果:
内存泄漏指的是因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。
需要指出的是,内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,从而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
在C/C++程序中,一般我们只关心两方面的内存泄漏:
当我们通过malloc
、calloc
、realloc
、new
等从堆中分配出一块内存,假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏。
当程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,就会产生系统资源泄漏。系统资源泄漏在严重时可导致系统效能减少,系统执行不稳定。
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));//开辟40个字节即10个整型的空间
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
}
void test()
{
int a = 10;
int* p = &a;
free(p);//无法释放
}
void test()
{
int* p = (int*)malloc(100);
p++;//p不再指向动态内存的起始位置
free(p);
}
void test5()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
//为了避免这种情况,可以在释放完之后就将p置空,这样哪怕重复释放free函数也不会做任何操作
void test6()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test6();
//除了函数之后,局部指针变量p被销毁,找不到原来开辟的空间,造成内存泄漏
}
//请问运行Test函数会有什么样的结果?
#include
#include
#include
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;
}
输出结果:
从输出结果可以看到,代码运行起来后崩溃了。
通过解读代码不难发现,代码的本意是想通过GetMemory
函数给str
指向的地址开辟内存空间,要实现这个功能应该将str
的地址传给GetMemory
也就是传址调用。但是由于GetMemory
的参数类型和实参str
一样同为char*
类型,导致在这个地方实际进行的传值调用,也就是只把str
中的NULL
传给了p
。虽然GetMemory
中为p
开辟了一块内存空间,但是p
是局部变量出了作用域就被销毁了,而且由于p
被销毁,我们将丢失那块内存空间的地址,也就是说还造成了内存泄漏。
从调试结果可以看到,当我们想把字符串"hello world"
拷贝到str
指向的空间时,由于str
是一个空指针所以导致程序崩溃。
//请问运行Test函数会有什么样的结果?
#include
#include
#include
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
输出结果:
其实这道题目和上面那道题有相似之处。这道题中,str
想通过GetMemory
函数得到指向字符串"hello world"
的指针p
,但是由于p
是一个局部变量出了GetMemory
函数即被销毁,所以当str
得到p
中存放的地址时p
中的内容已被销毁,也就是说此时str
拿到的是一个野指针,对野指针进行访问得到的自然就是一串乱码了。
//请问运行Test函数会有什么样的结果?
#include
#include
#include
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;
}
输出结果:
这道题和题目一相比,虽然能够打印出字符串,但是致命的一点是没有内存释放。正确的写法应该是将str
所指向的空间用free
释放后,再将str
置空。
//请问运行Test函数会有什么样的结果?
#include
#include
#include
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;
}
输出结果:
这道题目虽然输出了字符串,但是str
所指向的空间被释放后还对str
所指向的空间进行利用是不合适的,正确的做法应该是在释放掉str
所指向的空间后就将str
置空。
之前我们简单了解过C/C++程序中内存区域的划分,现在了解了动态内存后我们又可以对内存区域的划分有更深入的了解。
从上图中,可以看到C/C++程序内存分配的区域有:
在C99中,结构体中最后一个元素允许是未知大小的数组,就叫做柔性数组成员。
例:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
如果有些编译器报错,那么可以写成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
sizeof
计算含有柔性数组成员的结构体的大小,那么计算的大小将不包括柔性数组的内存。malloc
函数进行内存的动态分配,并且分配的内存大于结构体的大小,以适应柔性数组的预期大小。例:
#include
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));
输出结果:
例:
#include
typedef struct st_type
{
int i;
int arr[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));
type_a* S1 = (type_a*)malloc(sizeof(type_a) + 40);//40是分配给柔性数组的内存
if (S1 == NULL)
{
perror("malloc");
return 1;
}
printf("%d\n", sizeof(type_a));
for (int i = 0; i < 10; i++)
{
S1->arr[i] = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", S1->arr[i]);
}
printf("\n");
//扩容
type_a* S2 = (type_a*)realloc(S1, sizeof(type_a) + 60);
if (S2 == NULL)
{
perror("realloc");
return 1;
}
S1 = S2;
for (int i = 10; i < 15; i++)
{
S1->arr[i] = i;
}
for (int i = 0; i < 15; i++)
{
printf("%d ", S1->arr[i]);
}
free(S1);
S1 = NULL;
S2 = NULL;
return 0;
}
输出结果:
上面的代码展示了给柔性数组开辟空间以及扩容的过程。从输出结果可以看到,虽然我们已经给柔性数组arr
开辟了内存,但是在用sizeof
计算出的结构体大小仍为4。
其实刚才我们设计的type_a
的结构,也可以通过下面的方式来实现:
#include
typedef struct S
{
int n;
int* arr;
}type_a;
int main()
{
type_a* ps = (type_a*)malloc(sizeof(type_a));
if (ps == NULL)
{
perror("malloc->ps");
return 1;
}
ps->arr = (int*)malloc(40);
if (ps->arr == NULL)
{
perror("malloc->ps->arr");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
//扩容
int* ptr = (int*)realloc(ps->arr, 60);
if (ptr == NULL)
{
perror("realloc->ps->arr");
return 1;
}
ps->arr = ptr;
for (i = 10; i < 15; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 15; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
输出结果:
但是用柔性数组实现,具有几个好处:
方便内存释放
如果我们的代码需要嵌套在别人用的函数中,当我们在里面做了二次内存分配,并把整个结构体返回给用户。虽然用户可以调用free
来释放结构体,但是用户并不知道这个结构体内的成员也需要free
,而我们不能指望用户来发现这件事。所以,通过柔性数组我们就可以把结构体的内存以及其成员所需的内存一次性分配完成,并返回给用户一个结构体指针,这样用户只需使用一次free
就可以把所有的内存都释放掉。
利于提升访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。