椋鸟C语言笔记#30

动态内存管理(malloc、calloc、realloc、free)、查询已分配空间大小(_msize、malloc_usable_size)

Featured image

萌新的学习笔记,写错了恳请斧正。

动态内存分配的意义

直接创建变量、数组等只能在栈区空间开辟固定大小的一段空间

而我们在实际使用时对空间的需求有时不会那么死板。比方说直接创建数组的话,其长度就固定了,不可调整——如果我们需要数组的大小在程序运行时才会被确定就不行了

所以 C 语言中有动态内存分配的方法,使我们能够在需要时灵活的自己申请和释放空间

malloc(memory allocation)

malloc 是一个开辟动态内存的函数:

#include <stdlib.h>    //或者malloc.h
void *malloc( size_t size );

这个函数将会向内存堆区申请一块大小为 size(单位:字节)的连续可用的空间,并且返回指向这块空间的 void* 类型指针,如果开辟空间失败,则返回 NULL 指针

free

与 malloc 相对,free 函数用来释放我们申请的动态内存空间:

#include <stdlib.h>    //或者malloc.h
void free( void *memblock );

malloc 与 free 的使用示例

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int num = 0;
	scanf("%d", &num);
	int* ptr = NULL;
	ptr = (int*)malloc(num * sizeof(int));
	if (ptr == NULL)	//判断ptr指针是否为空
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < num; i++)
		ptr[i] = i;
	for (int i = 0; i < num; i++)
		printf("%d ", ptr[i]);
	free(ptr);
	ptr = NULL;
	return 0;
}

注意:在使用 free 函数释放动态开辟的空间后,应及时将指向这篇空间的指针置为空指针(现在这些指针指向的空间是未申请的、非法的,容易造成野指针问题),这是一个良好的习惯

calloc(contiguous allocation)

C 语言中还有 calloc 函数,也是用来动态分配内存的:

#include <stdlib.h>    //或者malloc.h
void* calloc( size_t num, size_t size );

这个函数的功能与 malloc 相似,在堆区动态开辟连续的 num 个大小为 size 字节的空间,但是这个函数会把开辟出来的空间全部初始化为 0

使用示例
#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
		printf("%d ", p[i]);
	free(p);
	p = NULL;
	return 0;
}

realloc(re-allocation)

有时我们要修改动态内存的大小,可以用 realloc 重新分配动态内存空间:

#include <stdlib.h>    //或者malloc.h
void *realloc( void *ptr, size_t new_size );

ptr 是要调整的内存的地址,new_size 是调整后的新大小,返回值是调整之后的内存起始位置

realloc 扩展内存空间时有三种情况:

realloc 收缩内存空间时只有一种情况:将多余的部分释放

若 ptr 是 NULL,则功能与 malloc 一致

若 new_size 为 0,则行为未定义,避免这样做

使用示例

由于 realloc 如果重分配失败会返回 NULL,如果我们要对 p 指针重分配,我们就不会直接拿 p 来接受 realloc 的返回值。因为如果返回 NULL,原空间不仅没有被释放,也没有指针指向原空间,就造成了内存泄露,所以我们可以这样:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int* q = (int*)realloc(p, 20 * sizeof(int));
	if (q == NULL)
	{
		perror("realloc");
		return 1;
	}
	p = q;
	free(p);
	p = q = NULL;
	return 0;
}

查询已分配空间大小

如果我们有一片分配好的动态内存,想知道之前到底分配了多大的空间时,我们可以使用_msize(Windows)和 malloc_usable_size(Linux)函数来查询:

#include <stdlib.h>    //或者malloc.h
size_t _msize( void *memblock );
size_t malloc_usable_size(void *_Nullable ptr);

这两个函数都可以返回参数指针所对应的动态内存空间实际分配的大小

注意:这个值可能比我们开辟空间时预想的大小大(具体可以看这个

动态内存分配常见错误

1. 对潜在 NULL 指针的解引用
void test()
{
    int* p = (int*)malloc(INT_MAX);
    *p = 20;    //如果p的值是NULL,就会有问题
    free(p);
}

这里我们试图给 p 分配 INT_MAX(limits.h 中定义的值,等于整型的最大值),这很有可能分配失败返回 NULL 指针,而下面对其解引用显然就是不安全的(编译器也会报警告的)

在动态分配内存后,一定要进行空指针判断

2. 对动态开辟空间的越界访问
void test()
{
    int i = 0;
    int* p = (int*)malloc(10*sizeof(int));
    if(NULL == p)
    {
        exit(EXIT_FAILURE);
    }
    for(i=0; i<=10; i++)
    {
        *(p+i) = i;    //当i是10的时候越界访问
    }
    free(p);
}

其中 exit 函数会直接终止程序并且返回其参数

而 EXIT_FAILURE 是定义在 stdlib.h 中的错误代码,表示程序未成功执行(一般值为 1)

3. 对非动态开辟内存使用 free 释放

free 函数的参数除了 NULL 只能是动态开辟的内存

4. 使用 free 函数释放动态内存的一部分

如果 free 函数的参数指向的不是动态内存的起始位置,也会报错

void test()
{
    int* p = (int*)malloc(100);
    p++;
    free(p);    //p不再指向动态内存的起始位置
}
5. 多次释放同一块内存

实际上就变成了对非动态开辟内存使用 free 释放

6. 动态开辟内存后忘记释放

造成 “内存泄露”:通过某个地址申请了空间,结果不还,而这个地址可能之后找不到了。这会导致这一片开辟的内存再也无法使用,如果多次内存泄露累计起来后果很严重。

所以动态开辟的内存一定要释放

使用时常犯错误一
#include <stdio.h>

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}

void main()
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
	return 0;
}

这段代码显然是错误的,函数传值时临时拷贝了一个形参,开辟动态内存空间后空间地址传值给了形参,而实参 str 并没有接收到动态内存空间,应该改为使用二级指针

使用时常犯错误二
void main()
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

这段代码有可能能正常的输出 world,但是实际上是错误的,str 释放后没有及时置为 NULL。后面又使用了 str,这就是典型的野指针(非法访问内存),这里可能不报错只是因为分配的那一段空间还没有被其他内容覆盖。