动态内存管理详解

1. 写在前面

在实际的开发过程中,我们处理的数据类型在大多数情况下都不是固定不变的,而是处于不确定、大小可变的情况之中;因此,允许程序在运行时根据需要分配和释放内存具有重要意义,我们将这一过程就称作动态内存管理。笔者今天的博客,主要就是对近期学习动态内存管理相关知识的总结和梳理。内容如下:

2. 为什么存在动态内存分配

 当我们在程序中定义变量的时候,就为其在内存中开辟了所需要的空间,如下就是内存开辟的方式之一

 上述开辟内存空间的特点

  • 所需开辟空间大小固定
  • 数组声明时,大小需特别指定,其所需要的内存在编译时分配

然后数据对于内存空间的需求,不仅仅只是上述的情况;有时候我们需要的空间大小在程序运行的时候才能知道,这个时候只能采用动态内存管理的方式对内存进行分配和释放,以满足程序正常运行的需求;所以就出现了动态内存管理的方式。

3. 动态内存函数

3.1 malloc

使用介绍

 功能:向内存申请一块连续的空间,返回指向该内存空间的地址,分配的内存空间未初始化

返回值类型:void* 类型的指针

参数:

  • size_t size : 所申请内存空间的大小,以字节为单位

Tips:malloc申请的内存空间,为堆区上的内存空间。

代码使用

int main()
{
    // 申请10个整型的空间
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
    
    //使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
    
    return 0;
}

运行结果

 

3.2 free

使用介绍

 功能:释放已分配的内存空间,将其归还操作系统

返回值类型: void* 类型的指针

参数:

  • void* memblock:  指向先前所申请到的内存空间的指针

代码使用

int main()
{
	

	// 申请10个整型的空间
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	
    // free对申请到的内存空间进行释放,并将指针置空
	free(p);
	p = NULL;
	return 0;
}

Tips : 程序向内存申请开辟的空间,使用结束之后一定要进行释放,归还操作系统,否则会造成内存泄漏(这一点后面也会讲到)!!!

3.3 calloc

使用介绍

 功能:向内存申请一块连续的空间,并将其初始化为0,返回指向该内存空间的地址

返回值类型:void* 类型的指针

参数:

  • size_t num :  所需申请空间中的元素个数
  • size_t size  :  每个元素的大小,以字节为单位

代码使用

int main()
{
	int* p = (int*)calloc(10, 4);
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	printf("\n");
	return 0;
}

运行结果

 

3.4 realloc

使用介绍

 功能:调整已分配内存的大小,可以动态扩大或缩小内存空间

返回值类型:void* 类型的指针

参数:

  • void* memblock:指向所需调整空间大小的指针
  • size_t size:   所需新空间的大小

代码使用

int main()
{
	//int* p = (int*)malloc(40);

	// 申请10个整型的空间
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	printf("\n");
	int* ptr = (int*)realloc(p, 80);
	if (ptr != NULL)
	{
		p = ptr; // 扩容成功
	}
	for (i = 10; i < 20; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	printf("\n");
	free(p);
	p = NULL;
	return 0;
}

运行结果

 

4. 常见的动态内存错误

4.1 对NULL指针进行解引用操作

代码示例

int main()
{
	int* p = (int*)malloc(1000);  --- 1
	int i = 0;
	
	for (i = 0; i < 250; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	return 0;
}

解析:
1. 未对指针p是否为NULL,进行判断,因此代码可能会对NULL进行解引用操作

4.2 对动态开辟空间进行越界访问

代码示例

int main()
{
	int* p = (int*)malloc(100);
	int i = 0;
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (i = 0; i < 26; i++)  --- 1
	{
		*(p + i) = i;
	}
	return 0;
}

解析:
1. 在for循环遍历的过程中,i的取值,超出了固有的内存空间,造成了越界访问

4.3 对非动态开辟空间进行free操作

代码示例

int main()
{
	int a = 10;
	int* p = &a;

	free(p); --- 1
	p = NULL;
	return 0;
}

解析:

1.  变量a在栈区上开辟静态内存空间,而只有使用动态内存函数开辟的动态内存空间,才能够使用free函数进行 
    内存释放。

4.4 使用free释放一块动态开辟内存中的一部分

代码示例

int main()
{
    int* p = (int*)malloc(100);
    p++;  --- 1
    free(p);//p不再指向动态内存的起始位置
    return 0;
}

解析:

1. ++ 操作改变了p的指向位置,此时的p不再指向动态内存的起始位置,因此释放p,就相当于只释放了动态内存中的一部分

4.5 对同一块动态内存多次释放

代码示例

int main()
{
    int *p = (int *)malloc(100);
    free(p);
    free(p);//重复释放  --- 1
    return 0;
}

解析:

1. 对动态内存进行重复释放

4.6 动态开辟内存忘记释放

代码示例

void test()
{
    int *p = (int *)malloc(100);  --- 1
    if(NULL != p)
    {
        *p = 20;
    }
}

int main()
{
    test();
    while(1);
}

解析:

1. 在test函数内部,进行动态内存申请后,没有释放的操作

特别需要我们注意的是:忘记释放不再使用的动态开辟的空间会造成内存泄漏 !!!因此在开辟动态内存空间之后一定要释放,且要正确的释放!

5. 柔性数组

C99标准规定,结构体中最后一个成员,允许是未知大小的数组,这就称作“柔性数组”成员

柔性数组在结构体中的定义

 柔性数组的特点

  • 结构体中柔性数组成员前必须至少有一个其它成员;
  • sizeof返回的该种结构体类的大小不包括柔性数组的内存;
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。

代码使用

struct S1
{
	int i;
	char ch;
	int arr[];//柔性数组成员
};

int main()
{
	int i = 0;
	struct S1* p = (struct S1*)malloc(sizeof(struct S1) + 10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->i = 100;
	for (i = 0; i < 10; i++)
	{
		p->arr[i] = i;
		printf("%d ", p->arr[i]);
	}
	printf("\n");
	free(p);
	return 0;
}

运行结果

 柔性数组的优势

而上述的柔性数组的功能,也可以由如下结构设计实现:

 代码使用

struct S1
{
	int i;
	int* pa;
};

int main()
{
	struct S1* p = (struct S1*)malloc(sizeof(struct S1));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->i = 10;
	p->pa = (int*)malloc(p->i * sizeof(int));
	if (p->pa == NULL)
	{
		perror("malloc");
		return 1;
	}

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p->pa[i] = i;
		printf("%d ", p->pa[i]);
	}
	printf("\n");
	//释放空间
	free(p->pa);
	p->pa = NULL;
	free(p);
	p = NULL;
}

运行结果

 柔性数组的功能,可以完全由另一种结构设计所代替,那为什么还要继续使用柔性数组呢?

因为,柔性数组具有后者独有的优势

  • 方便内存释放:使用柔性数组只需进行一次内存分配,使用结束后方便内存的释放。
  • 提高访问速度:柔性数组只进行一次内存申请,所以其内存空间是连续存放的,而连续的内存有益于提高访问速度,也有益于减少内存碎片。

6. 经典编程例题详解

6.1 例题一

题目内容

代码详解

void GetMemory(char *p)
{
	p = (char *)malloc(100); 
}
void Test(void)
{
	char *str = NULL;
	GetMemory(str); --- 1
 	strcpy(str, "hello world");  --- 2
 	printf(str);
}

解析:
1. 传入GetMemory的str指针为实参,指针p为形参,而形参只是实参的一份临时拷贝;即使在函数内部
   申请了一块内存空间,当函数运行结束,形参即销毁,因此str仍为NULL。
2. str为NULL,当其传入strcpy函数内部时,strcpy函数对其进行解引用操作,即是对NULL进行解引用

程序运行结果:程序将会崩溃,没有打印内容。

运行结果

 

6.2 例题二

题目内容

代码详解

char *GetMemory(void)
{
	char p[] = "hello world"; --- 1
	return p;
}
void Test(void)
{
	char *str = NULL;
	str = GetMemory();  --- 2
	printf(str);
}

解析:
1. 在GetMemory函数中,创建了字符数组p存储了字符串"hello world",在内存中开辟了一段连续的空间;
   本质上,字符数组的数组名p,指向了"hello world"字符串中首元素的地址;最后函数运行结束后,返回了 
   指针p;
2. 指针str,接收了GetMemory函数返回的指针p;但函数运行结束,其内部为局部临时变量开辟的空间也将被销        
   毁,因此str此时指向的内存空间是不确定的,即str此时为野指针。

程序运行结果: 打印随机值,因为str所指向的内存空间此时是不确定的、随机的。

运行结果

 

6.3 例题三

题目内容

代码详解

void GetMemory(char **p, int num)
{
    *p = (char *)malloc(num); --- 2
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);  --- 1
    strcpy(str, "hello");
    printf(str);
}

解析:

1. 因为传入GetMemory函数中的参数,为str指针的地址,于是,函数内部的局部临时变量二级指针p解引用后便与str在内存中指向同一块空间,即传址,使得函数内部的变量与外部的变量建立的联系;
2. 在GetMemeroy函数,malloc申请了一块空间之后,赋值给了*p,即赋值给了str;因此,str此时指向了内存当中一块连续的空间

程序运行结果: 打印字符串"hello"

运行结果

 

6.4 例题四

题目内容

代码详解

void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, "hello");
    free(str);  --- 1
    if(str != NULL) --- 2
    {
        strcpy(str, "world");  
        printf(str);  
    }
}

解析:
1.  函数malloc申请了一块空间,并让指针str指向了这块空间;strcpy函数,将字符串"hello",拷贝到了
    这块空间中,之后对str指向的空间进行释放,但没有对str进行置空操作;
2.  因为str不为空,仍指向内存空间中的一块随机区域,在进入到if语句内部之后,strcpy将字符串"world"
    拷贝到了str指向的内存空间中。

程序运行结果: 打印字符串"world" (但这样的程序是错误的,str未置空即相当于野指针)

运行结果

 

7. 小结

动态内存管理使得程序能够根据实际需求分配内存,从而避免浪费内存资源;但它也带来了一些挑战,如内存泄漏、内碎片和指针错误等。因此,作为开发人员,我们需要熟练掌握动态内存管理的技巧,并使用工具技术来检测和修复内存错误,以确保程序的正确性和稳定性。好啦,今天的博客就到此为止了,希望能够给正在阅读的你带来收获!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值