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