掌控动态内存的魅力

前言:为什么要有动态内存分配?

我们已经掌握的内存开辟方式有:

     

①  int val =10;     //通过在栈上开辟4个大小的空间

        

② char arr[10]={0};    //通过在栈空间上开辟10个字节的连续空间

通过上述代码,我们发现两个特点:

• 空间开辟的大小是固定不可变的。

• 数组在声明的时候,必须指定数组的长度,数组的空间一旦确定便不可调整和更改。

但是通常对于空间的需求,不仅仅是固定的空间,有时候我们需要根据程序的需求,不断更改内存空间的大小,而上述的方式已经不在能够满足我们的需求,此时我们需要动态内存开辟。

        

动态内存分配的核心作用:

        

①静态分配(如数组)必须提前确定大小,无法适应运行时变化的需求。

        

②动态分配允许程序运行时按需申请内存(例如用户输入决定数据量)。

        

③突破栈空间限制

 栈内存(局部变量)大小有限(通常几MB),大内存需求(如处理图像、大型矩阵)必须用堆内存(动态分配)。

        
④灵活控制生命周期

静态变量(全局/局部)生命周期固定,动态内存可手动管理(malloc申请后,直到free才释放),适合长期存储数据(如链表节点)。

        
⑤节省内存

避免静态分配“按最大可能”预占内存的浪费(如声明int arr[1000]但只用了10个元素)。

        

C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,灵活的掌控内存空间,体验掌握内存空间的魅力。

一、malloc函数

 1.malloc的简介   

参数分析:

        

         void* malloc (size_t size);

        

参数:  size_t size  表示开辟内存空间的字节大小

        

返回类型: void *  返回开辟后,指向空间的大小。   

        

函数用途:开辟 size 大小字节的空间

        

2.malloc函数的使用

代码示例: 

//malloc
void test1()
{
	//申请20个字节空间,并用整形指针接收
	int* p = (int*)malloc(20);

    //判断是否开辟成功,如果失败打印错误信息
	if (p == NULL)
	{
		perror("malloc");
	}
    //开辟成功,给开辟的整形空间赋值
	else
	{
		for (int i = 0; i < 5; i++)
		{
			*(p + i) = i;
			printf("%d ", *(p + i));
		}
	}
    
}

温馨提示:

        

①头文件包含:malloc的使用需要包含头文件<stdlib.h>

        

②如果开辟成功,则返回⼀个指向开辟好空间的指针。

        
③如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。

        
④返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。

        
⑤如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

        

二、free函数

1.free函数的简介

参数分析:

        

        void free (void* ptr);

      

参数:由动态内存开辟的空间的起始地址

        

返回类型:void(空)

        

函数用途:释放由动态内存开辟的空间

        

2.free函数的使用

由动态内存开辟的空间最好手动释放,防止内存泄漏,导致程序出现问题,通过学习free这个函数,对于上面的代码我们可以进行一定的完善。

//malloc
void test1()
{
	//申请20个字节空间,并用整形指针接收
	int* p = (int*)malloc(20);

	if (p == NULL)
	{
		perror("malloc");
	}
	else
	{
		for (int i = 0; i < 5; i++)
		{
			*(p + i) = i;
			printf("%d ", *(p + i));
		}
	}
	free(p);      //若是采用*p=i; p++;		会导致p不是起始地址
				  // free函数⽤来释放动态开辟的内存,p必须为起始位置
	p = NULL;    //因为内存已经还给操作系统了,所以p已经是野指针了,需要置空。
}

温馨提示:

        ①我们对动态内存开辟的空间,不采用 p++ 这种改变起始地址的形式进行赋值,这样会导致free的时候出现问题,所以我们一般采用(p+i) 的形式进行移动指针,从而进行赋值操作。

        

        ②对于已经free的动态内存地址p,其p指针需要进行置空,因为操作系统已经收回了该空间的使用权,而p仍然指向该空间,这样的p就成了野指针,不能进行操作使用。

        

三、calloc函数

1.calloc函数的简介

1.参数分析

        

        void* calloc (size_t num, size_t size)

        

函数用途:为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。

        

温馨提示:函数calloc与函数 malloc 的区别,只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

        

2.calloc函数的使用

        代码示例:申请5个int类型字节大小的空间

void test2()
{
	int* p = (int*)calloc(5,sizeof(int));
	if (p != NULL)
	{
		for (int i = 0; i < 5; i++)
		{
			printf("%d ", *(p+i) );
		}
	}
	free(p);		
	p = NULL;
}

四、realloc函数

1.realloc函数的简介

①:realloc函数的出现让动态内存管理更加灵活

②:有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的时候内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。

参数分析:

        

        void* realloc (void* ptr, size_t size);

        

参数一:ptr 是要调整的内存地址

        

参数二:size 调整之后新⼤⼩

        

返回值:返回值为调整之后的内存起始位置

        

函数功能:这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。

        

注意事项:realloc在调整内存空间的是存在两种情况


◦ 情况1:原有空间之后有⾜够⼤的空间

 ①拥有足够的空间进行调整,此时整形指针tmp的地址仍然为ptr所开辟的起始地址。


◦ 情况2:原有空间之后没有⾜够⼤的空间

②缺乏足够的空间进行调整,此时整形指针tmp就会在内存中,另找一块连续且充足的空间,原空间所存储的值不会因为,地址重新分配而发生改变,仍然为原来的值,此时的tmp将不同于ptr,发生改变。

2.realloc函数的使用

        对于calloc开辟的空间我们可以,通过realloc进行扩充,但需要注意扩充失败。

代码示例:

//realloc  用于调整内存空间的大小
void test3()
{
	int* p = (int*)calloc(5, sizeof(int));
	if (p != NULL)
	{
		for (int i = 0; i < 5; i++)
		{
			printf("%d ", *(p + i));
		}
	}

	// p = (int*)realloc(p, 40);
	// 如果无法扩充空间,“realloc”可能返回 null 指针。
	// 将 null 指针赋给“p”, 导致不能访问到原来开辟的空间,从而将导致原始内存块泄漏
	
    
    //通过使用新的指针ptr进行接收,并判断是否为空指针。
    //如果为空指针则打印错误信息,反之将ptr赋值给p,继续利用p进行维护开辟的空间
	int *ptr= (int*)realloc(p, 40);
	if (ptr != NULL)
	{
		p = ptr;  //开辟空间成功,继续利用p指针维护
		ptr = NULL;
		//调整成功,使用开辟的空间
		for (int i = 5; i < 10; i++)
		{
			*(p + i) = i;
			printf("%d ", *(p + i));
		}
	}
	else
	{
		perror("realloc");
		//调整失败,通过p指针使用调整前的空间
	}

	free(p);
	p = NULL;
}

        

五、常⻅的动态内存的错误

1.对NULL指针的解引⽤操作。

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}

由于malloc 申请了过大的字节空间,导致动态内存开辟失败,从而p接收到了NULL(空指针)。

        

对空指针进行解引用操作,从而导致程序崩溃。

        

2.对动态开辟空间的越界访问

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		perror("p");
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
}

因为该程序仅向内存申请了10个字节大小的空间,当i=10的时候,p指针已经指向了未知空间,对其解引用操作将导致越界访问的同时,还修改了内存中存储的其他值。

        

如下图所示:

        

3.对⾮动态开辟内存使⽤free释放

void test()
{
	int a = 10;
	int* p = &a;
	free(p);
}

free函数只能对动态内存开辟的空间进行释放,若对非动态内存开辟的空间进行释放,将会导致程序崩溃。

        

4.使⽤free释放⼀块动态开辟内存的⼀部分

void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}

由于p指针发生偏移,导致p指针不在指向,动态内存开辟的地址,若此时对其free,会导致仅释放动态内存的一部分,而导致内存出现泄露。

        

5.对同⼀块动态内存多次释放

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

当第一次调用free(p)后,p所指向的内存空间已经被释放,此时p成为了野指针。再次对野指针执行free操作是非法的,会破坏内存管理系统的状态。

        

6.动态开辟内存忘记释放(内存泄漏)

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

当 test() 函数执行结束后,指针 p 的作用域消失(局部变量),但它指向的动态分配内存并未被释放。这部分内存会一直被程序占用,无法被系统回收,造成内存泄漏。尤其在长期运行的程序中,内存泄漏会逐渐消耗系统资源,可能导致程序性能下降甚至崩溃。

        

六、练习题

1.练习一:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
 
void Test()
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}
 
int main()
{
	Test();
	return 0;
}

解析:

       ① str传递给Getmemory函数的时候,采用的值传递,形参变量p是str的一份拷贝,当我们把malloc申请的空间的起始地址放在p中时,我们不会修改str,str仍然是NULL,所以当getmemory函数返回后,再去调用strcpy函数,将“hello world"拷贝到str指向的空间时,程序崩溃。

        

       ②未使用free, 及时的对开辟的动态内存及时释放。

        

那么,如何对这段代码进行修改呢? 

①这段代码的核心问题是出现在值传递,如果Getmemory函数通过地址传递,就能正确的指向动态内存所开辟的空间。

② 及时进行free,释放动态开辟的内存。    

//修改程序:将传值调用改为传址调用,形参的改变可以影响实参
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
 
void Test()
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
 
int main()
{
	Test();
	return 0;
}

        

练习二:

#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
	char p[] = "hello world";
	return p;
}
void Test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

解析:

        由于GetMemory函数中创建的数组是在栈区上创建的,当调用完Getmemory函数后,p数组将会被操作系统回收,这块空间将不可被访问,而str所接收的地址,虽然指向了这块空间,但是我们不能进行访问,导致其变成了野指针。

        

那么,如何对这段代码进行修改呢? 

这段代码的核心是:函数中的数组p被操作系统所回收,如果将其变为静态变量,则该数组在函数调用完后,不会被操作系统所回收,通过str所接收的地址,仍然能访问该空间,从而打印出正确的结果。

方法一:

//修改代码:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
	static char p[] = "hello world";//static修饰的变量是放在静态区中的
	//所以函数栈桢销毁的时候,这块空间仍然保留
	return p;
}
void Test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

方法二:

//修改代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char * p)
{
    //将字符串复制到p变量中
	strcpy(p,"hello world");
}
void Test()
{
	char str[20];   //定义在栈区的缓冲区
	GetMemory(str);
	printf(str);
}
int main()
{
	Test();
	return 0;
}

        

七、柔性数组

1.柔性数组的简介:

        也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。

代码示例:  

struct st_type
{
	int i;
	int a[0];//柔性数组成员
};

        

有些编译器会报错⽆法编译可以改成:

typedef struct st_type
{
	int i;
	int a[];//柔性数组成员
}type_a;

        

2.柔性数组的特点

• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。

        
• sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。

        
• 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构⼤⼩,以适应柔性数组的预期⼤⼩。

        

思考一下为什么柔性数组成员前面必须至少一个其他成员?

        如果在结构体中只有一个柔性数组的话,那么sizeof(结构体名)的大小又会是多少呢,显然无法计算,因而柔性数组前至少有一个其他成员。

        

此外,sizeof(结构体名)不会计算柔性数组,而计算的是除它外其他成员的大小。

代码示例:

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4,不包括柔性数组的大小
	return 0;
}

        

3.柔性数组的使用

        结构体的大小并不会包括柔性数组的大小,这也就意味着,我们在使用含有柔性数组的结构体时,不会按照下面的方法创建变量:

#include<stdio.h>

struct S
{
	int i;
	int arr[];
};

int main()
{
	struct S s;//一般不会这么写:这样写的话并不会为数组分配空间
	return 0;
}

        

       通常需要利用malloc函数为结构体开辟空间,并用结构体指针进行维护。

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

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;

int main()
{
	int i = 0;

    //分为两部分,第一部分为结构体其他成员开辟空间,第二部分为柔性数组开辟100个整形大小的空间
	type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));

	//业务处理
	p->i = 100;
	for (i = 0; i < 100; i++)
	{
		p->a[i] = i;
	}

	free(p);
	return 0;
}

        

其实可以不通过柔性数组的方式,也能达到相同的效果.

代码示例:

        

#include<stdio.h>
#include<stdlib.h>
typedef struct S
{
	int i;
	int* arr;
}type_s;
	
int main()
{
	type_s* p = (type_s *)malloc(sizeof(type_s));	
	if (p == NULL)
	{
		perror("p");
		return 1;
	}
	p->i = 100;
	//为arr指针 开辟动态内存分配
	p->arr = (int *)malloc(10 * sizeof(int));

	//判断是否开辟成功
	if (p->arr == NULL)
	{
		perror("p->arr");
		return 2;
	}

	//开辟成功,给开辟的空间进行赋值操作
	for (int i = 0; i < 10; i++)
	{
		*(p->arr + i) = i;
		printf("%d ", *(p->arr + i));
	}
		
	//利用realloc 进行调整空间	
	int* tmp = (int*)realloc(p->arr, 20* sizeof(int));
	if (tmp == NULL)
	{
		perror("tmp");
		return 3;
	}
	p->arr = tmp;


	//进行free操作
	//优先对arr进行释放,若提前对结构体指针进行,结构体指针释放后,将找不到arr指针。
	//以从 里 向 外 的方式进行释放
	free(p->arr);
	p->arr = NULL;
	free(p);
	p = NULL;

	return 0;
}

        

八、C/C++中程序内存区域划分

        对于C/C++程序内存的区域,大致如下分布:

        

温馨提示:

1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。

        
2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅式类似于链表。

        
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

        
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。

        

既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

评论 10
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值