【C语言】--- 动态内存管理详解
动态内存管理
- 1.为什么需要动态内存分配
- 2.malloc 和 free
- 2.1 malloc
- 2.2 free
- 3. calloc和realloc
- 3.1calloc
- 3.2 realloc
- 4.常见的动态内存的错误
- 4.1对空指针解引用的操作
- 4.2 对开辟的空间越界访问
- 4.3 对非动态开辟内存使用free函数
- 4.4 使用free函数释放动态开辟空间的一部分
- 4.5 对同一块动态内容多次释放
- 4.6 动态开辟内存忘记释放
- 5.动态内存经典笔试题分析
- 5.1 题目1:
- 5.2 题目2:
- 5.3 题目3:
- 5.4 题目4:
- 6. 柔性数组
- 6.1 柔性数组的特点
- 6.2 柔性数组的使用
- 6.3 柔性数组的优势
- 7.C/C++程序中的内存区域划分
1.为什么需要动态内存分配
int main()
{
int a = 10;//开辟4个字节
int arr[10] = { 0 };//开辟40个字节
return 0;
}
上述两种开辟空间的方式有两个特点:
1.空间的开辟大小是固定的。
2.数组在声明的时候,必须指定数组的长度,数组空间大小一旦确定不能调整。
对于上述静态分配的数组,一旦空间占满,再分配新数据就会导致程序崩溃,当我们为了避免程序崩溃,故意开辟了较多的空间的时候势必会造成空间的浪费。C语言引入了动态内存开辟,让程序员可以自己申请和释放空间,这样就非常灵活了。
2.malloc 和 free
2.1 malloc
malloc是C语言提供的一个动态内存开辟的函数,可以申请一块连续可用的空间,并返回指向这块空间的指针。
void* malloc (size_t size);
1.size_t 是无符号整型
2.size的单位是字节(Byte)。
3.返回值的类型是 void* ,这是因为malloc函数不关心开辟空间的类型,具体使用的时候需要使用者自己来决定。
4. 如果size的大小是0,那么malloc的行为标准是未定义的,取决于编译器的具体实现。
int main()
{
int* p1 = (int*)malloc(40);
char* p2 = (char*)malloc(40);
return 0;
}
我们可用根据自己的需要把void*类型强制转换为自己需要的类型。
int main()
{
//开辟空间
int* p1 = (int*)malloc(40);
//使用开辟的空间
for (int i = 0; i < 10; i++)
{
*(p1 + i) = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p1 + i));
}
return 0;
}
上述代码是使用malloc函数申请空间并使用的过程。
但是当malloc申请失败,就会返回NULL指针,所以我们需要对返回值进行检查。
int* p1 = (int*)malloc(INT_MAX); //x86环境下,程序就会崩溃, x64需要申请更大的空间。
if (p1 == NULL)
{
perror("malloc");
return 1;
}
)
2.2 free
C语言提供了free函数,用来做动态内存的释放和回收。
void free (void* ptr);
- 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数ptr是NULL指针,则函数什么事都不做。
free函数需要和malloc这样的内存申请函数搭配使用,申请空间后,一定要配合free函数释放,不然会造成内存泄露。
int main()
{
//开辟空间
int* p1 = (int*)malloc(40);
if (p1 == NULL)
{
perror("malloc");
return 1;
}
//使用开辟的空间
//....
//释放开辟的空间
free(p1);
p1 = NULL;
return 0;
}
释放 p1指针后,把p1置为空是一种规范,因为free(p1)
并没有修改p1的内容,这样就可能导致后续错误的使用p1指针访问到没有权限的空间导致程序的错误或崩溃。
3. calloc和realloc
calloc和realloc也是动态内存分配的两个函数
3.1calloc
void* calloc (size_t num, size_t size);
calloc函数的功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节初始化为0。
int main()
{
int* p = (int*)calloc(10, 4);
if (p == NULL)
{
perror("calloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
那么把上述代码的calloc函数改为malloc函数,结果如何呢?
我们可用看到打印了一些随机值,所以malloc函数并没有对申请的空间做处理。calloc函数和malloc函数的本质区别就是calloc函数回再返回地址之前把申请的空间的每个字节全部初始化为0。所以如果我们对申请的内存空间的内容要求初始化,就可以使用calloc函数,剩下的情况使用malloc函数即可,malloc函数没有初始化步骤效率相对较高。
3.2 realloc
realloc函数可以对已经申请的空间做处理,实现对已申请空间的扩大或缩小。
void* realloc(void* ptr,size_t size);
- ptr是需要调整的内存地址
- size是调整后新大小。
realloc函数再使用的过程中会出现以下三种情况。
1.原有空间之后有足够大的空间,这时候可在原空间后直接追加空间,原来空间的数据不发生变化。
2.原来空间之后没有足够大的空间,这时候realloc函数实现的步骤如下。
1.重新在堆空间上找到一个合适大小的连续空间。
2.把原空间的数据拷贝到新空间。
3.释放原空间
4.返回指向新空间的指针。
3.原来空间之后没有足够大的空间并且堆空间上也没有合适的连续空间,返回NULL。
综合上诉三种realloc的使用情况,下面给出realloc函数的标准使用代码。
int main()
{
//开辟空间
int* p = (int*)malloc(20);
if (p1 == NULL)
{
perror("malloc");
return 1;
}
//调整空间
int* p1 = (int*)realloc(p, 40);
if (p1 != NULL)
{
p = p1;
}
else
{
perror("realloc");
return 1;
}
//使用空间
//释放空间
free(p);
p = NULL;
return 0;
}
我们看到使用realloc的时候需要做错误处理,使用中间变量p1缓冲。因为如果直接使用p接受返回值的话,当遇到第三种情况,p原来的值就会被覆盖为NULL,造成程序的严重错误。
这里额外提到一点,当realloc函数接收的第一个参数是NULL的时候,这里realloc函数的功能就和malloc相同。
int* p0 = (int*)realloc(NULL,40);
int* p1 = (int*)malloc(40);
realloc函数的出现让动态内存管理更加灵活
4.常见的动态内存的错误
4.1对空指针解引用的操作
int main()
{
int* p = (int*)malloc(INT_MAX);
*p = 20;
free(p);
p = NULL;
return 0;
}
这里本质是申请空间失败返回空指针,但是没有做判空处理。
int* p = (int*)malloc(INT_MAX);
if(p == NULL)
{
perror(malloc);
return 1;
}
4.2 对开辟的空间越界访问
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i <= 10; i++)
{
*(p + i) = i + 1; //当i为10的时候越界访问
}
free(p);
p == NULL;
return 0;
}
申请的空间可用认为是 长度为10的整形数组,合法下标是0-9,当i为10的时候这里就是越界写,程序就会发生报错。
4.3 对非动态开辟内存使用free函数
int main()
{
int a = 10;
int pa = &a;
free(pa);
pa = NULL;
return 0;
}
这里a申请的空间是在栈上,而动态申请的空间都是在 堆上申请的,free也是释放堆空间。只有动态开辟的空间需要使用free函数来释放。
4.4 使用free函数释放动态开辟空间的一部分
int main()
{
int* p = (int*)malloc(40);
p++;
free(p);
return 0;
}
这里由于修改了p,导致free无法正确的释放,所以我们尽量不要修改malloc函数返回的指针,如果需要可用定义一个临时变量,对临时变量做出修改来完成程序。
4.5 对同一块动态内容多次释放
int main()
{
int* p = (int*)malloc(40);
free(p);
free(p);
return 0;
}
同一块动态的空间不允许多次释放,上述两次free(p)
可能在实际的程序中离得很远很难注意到,这样就导致了程序崩溃,但是当我们遵守规范在free(p)
后写了p = NULL
的时候,第二次free(p)
不会有任何效果,程序就不会崩溃了。
4.6 动态开辟内存忘记释放
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄露,所以动态内存函数要和free函数成对的使用。
void test()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
perror("malloc");
return 1;
}
if(1) return;
free(p);
p = NULL;
}
在free(p)
之前test函数返回,所以即使规范使用free函数也可能会造成内存泄露的问题,这也是C/C++一直被诟病的原因,使用者的权限自由的同时也导致程序容易出错。
5.动态内存经典笔试题分析
5.1 题目1:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这个题目的问题有内存泄露和空指针拷贝。
形参p是实参str的一份拷贝,两者拥有不同的地址,在GetMemory函数中给p指针开辟的空间和str没有任何关系,函数结束后局部变量p销毁,开辟的空间没有释放造成了内存泄露,同时str没有改变仍然是NULL,程序执行strcpy函数的时候就崩溃了。
5.2 题目2:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这个题目的问题是使用了被释放的局部变量。
GetMemory函数执行完毕后,其函数栈帧被销毁,资源还给了操作系统,数据是未知的,但是str接收到了局部变量p,仍然尝试输出的话,只能输出一些随机的值了。
5.3 题目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);
}
这个题目没有问题可以正常输入"hello",可以和题目5.1对比分析。
题目5.3使用了二级指针和指针解引用成功为str申请了一块内存地址。
5.4 题目4:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这个程序虽然可以正常执行但是存在越界写的问题,这是程序的隐含问题,正常执行的程序也可能会有问题,free(str)
后遵守规范str = NULL
程序自然就不会正常运行了。
6. 柔性数组
C99标准引入了柔性数组的概念,结构体中最后一个元素允许是未知大小的数组,这就叫柔性数组成员。
struct S
{
int i;
int a[0];
}
有些编译器会报错无法编译可以改成如下方式
struct S
{
int i;
int a[];
}
6.1 柔性数组的特点
- 结构体的柔性数组成员前面至少有一个其他成员。
- sizeof返回的结构大小不包括柔性数组。
struct S
{
int i;
int arr[];
};
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
3. 对包含柔性数组成员的结构用malloc等函数进行内存的动态分配的时候,应分配大于结构体大小的内存以适应柔性数组的预期大小。
6.2 柔性数组的使用
int main()
{
struct S* s = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 5);
if (s == NULL)
{
return 1;
}
s->i = 1;
for (int i = 0; i < 5; i++)
{
s->arr[i] = i + 1;
}
for (int i = 0; i < 5; i++)
{
printf("%d ", s->arr[i]);
}
printf("\n");
struct S* tmp = (struct S*)realloc(s, sizeof(struct S) + sizeof(int) * 10);
if (tmp == NULL)
{
return 1;
}
else
{
s = tmp;
}
for (int i = 5; i < 10; i++)
{
s->arr[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", s->arr[i]);
}
free(s);
s = NULL;
return 0;
}
这里就是柔性数组的使用。
6.3 柔性数组的优势
struct S
{
int i;
int* arr;
};
int main()
{
struct S* s = (struct S*)malloc(sizeof(struct S));
if (s == NULL)
return 1;
s->i = 1;
s->arr = (int*)malloc(sizeof(int) * 5);
if (s->arr == NULL)
return 1;
for (int i = 0; i < 5; i++)
{
s->arr[i] = i + 1;
}
int* tmp = (int*)realloc(s->arr, 10 * sizeof(int));
if (tmp == NULL)
{
return 1;
}
else
{
s->arr = tmp;
}
for (int i = 5; i < 10; i++)
{
s->arr[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", s->arr[i]);
}
free(s->arr);
s->arr = NULL;
free(s);
s = NULL;
}
上述代码和 6.2代码完成了同样的功能,但是6.2的代码有两个好处:
1.方便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
2.有利于提高访问速度
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。
7.C/C++程序中的内存区域划分
栈区:函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区:一般由程序员释放,若程序员不释放,程序结束时可能由OS回收。
数据段/静态区:存放全局变量、静态数据,程序结束后由系统释放。
代码段:存放二进制代码和只读常量。